SchemeプログラマがCommon Lispでハマりそうなところ


SchemeとCommon Lispではっきりと違うところは、それ程問題にならないかと思いますが、名前が似ているのに結果や引数が微妙に違ったりするところはハマりどころです。

CLではstringはvectorのsubtypeである

(vectorp "foo")
;=>  T

(vector? "foo")
;=>  #f

こういうことをしているとはまります

(cond ((vectorp obj)...)
      ((stringp obj)...)
      ...)
3.141592と書いた場合single-floatになる

R5RS Schemeでは基本double-floatですが、CLでは、デフォルトでは、single-floatとして読み取られます。
double-floatで読む込むには*read-default-float-format*に、double-floatを設定します。もしくは、3.141592d0と書き直します。

1.と書いた場合、1.0の略記ではない

CLは、10進表記の1ということになります。*read-base*が10以外の時などに利用したり 10進であることを強調する場合に用いられます。

&rest引数が必ずしもコピーされない

Schemeでは、(define (foo . xs) ...) と定義して (apply foo ys) と呼び出した場合、xsは常に新たにアロケートされたリスト(ysはコピーされる)であることが保証されています (R5RS 4.1.4)。CLでは(defun foo (&rest xs) ...) と定義して (apply #'foo ys)と呼び出した場合に、処理系はysをそのまま(コピーせずに)xsに渡して良いことになっています (CLHS 3.4.1.3)。コピーする処理系もありますが、ポータブルなコードでは&rest引数を破壊する場合は自分でコピーしなければなりません。
&rest引数を破壊するなんて普通しないと思うかもしれませんが、delete-ifとか sortなどをうっかり使ってしまう、ってことはあります(実話)。

doマクロは繰り返し変数を上書きする

Schemeではdoマクロは再帰に展開されるので、繰り返し変数は繰り返しの度に新たに作られます。繰り返しが進んでも、一度クロージャで取り込んだ値は変わりません。

(do ((x 0 (+ x 1))
     (r '() (lambda () x)))
    ((= x 1) (r))
  )
;=> 0  ;; (lambda () x)が閉じ込めるxはx=0の時のxで、x=1になるxとは別

CLではこうなります。

(do ((x 0 (+ x 1))
     (r '() (lambda () x)))
    ((= x 1) (funcall r))
  )
;=> 1  ;; (lambda () x)が閉じ込めるxが繰り返しによって上書きされx=1になっている

繰り返し変数のその時の値を捕捉したい場合は、(let ((x x)) (lambda () x))などとする必要があります。

map系の名前でも返り値を利用しないものがある

Schemeでは、map系の名前は、要素に関数を適用して、新しい集合を作って返すもの、for-each系の名前は、mapに似ているけれども、副作用が目的で、返り値は利用しないもの、という命名規約がありますが、CLでは、mapから始まっている名前だからといって返り値を利用するとは限りません。
mapから始まる名前で、返り値を利用しないことが慣習である(もしくは返り値が有用でない)関数には、
maphash, mapc, mapl があります。

listpは、list?と動作が違う

CLのlistpは、consか、nilであれば、真正リストかどうかは関係なくTを返します。

(listp '(car . cdr))
;=>  T

(list? '(car . cdr))
;=>  #f
シンボルのreadは*package*の影響を受ける

S式でデータをセーブ/ロードしたりネットワーク越しに送ったりするのはLisperの常套手段ですが、 Common LispではS式を読み込む時にシンボルが「その時点での*package*」にinternされます。 全く同じに見えるS式を読んでいても*package*が異なると読んだものが同じになりません。 Schemeでは名前空間はシンボルとは別の層で管理されてて、シンボル自体は同じに見えればeq?なので、 送ってるデータをダンプしていくら睨んでも原因がわからず途方に暮れることが。