🚀

2024年のCommon LispのFAQではない質問

2023/10/08に公開

ANSI Common Lisp規格のページ数の多さは規格として失敗でしたか?

ページ数だけにこだわるのはナンセンスだという主張は規格誕生の頃からありますが——

——Common Lispプログラマは常にANSI Common Lisp(古くはCLtL)をリファレンスとしてプログラミング時に活用するという実態が40年も継続しており、コミュニティでの議論は常にANSI Common Lisp規格ではどうなっているかというところで議論が展開されています。
1984年から1994年までの間に徹底的な議論やユーザーからのフィードバックを反映してか、仕様に抜けや漏れが非常に少なく、1994年以降の改訂がほぼ0にもかかわらず、2024年時点でもユーザーに利用されています。
Common Lisp処理系もANSI Common Lispに準拠することを基本と考えており、準拠していない(する意向のない)Common Lispは皆無といっていいでしょう。

少なくともドキュメントとしてのANSI Common Lisp規格は恐らくかなり成功したドキュメントで、形骸化していない規格として成功している方でしょう。

Common Lispは様々な方言のLispを統合した結果巨大な仕様になったのですか?

まず、Common Lispに先行するZetalispは1980年代初頭の時点でGUIインターフェイスを持つOSを記述しており、これをそのまま規格化すればANSI Common Lispの何倍も規模の大きいものになります。

Common Lisp(1984)では既に実績のある基本的な部分を仕様をまとめたものになり、ANSI Common Lisp(1994)ではこれにオブジェクトシステムや、エラーハンドリング(コンディションシステム)、プリティプリンタ等々が加えられましたが、これらも既にZetalispに含まれていました。
Zetalisp(Lispマシンベンダーなど)からみれば様々な機能を加えたANSI Common Lispでもサブセットであるという認識でした。

また、Common LispはLisp方言の統一の規格ではありませんでした(結果的に他の方言が衰退したのは別の話として)

Common Lispでコマンドを作ることはできないのでしょうか。

コマンドというものが何を指しているかは環境や時代によって様々ですが、コンピュータの対話環境で立ち上げるアプリのようなものは作成可能です。また、スクリプト形式での実行も可能です。
ウェブブラウザからForth処理系のようなものまであります。

Common LispではLOOPを使うべきではないのでしょうか? ポール・グレアムも使わないとききました。

ポール・グレアムはLisp界の有名人ですが、ポール・グレアムのloopについてのアドバイスに従うCommon Lispプログラマは昔も今も殆どいないようです。
筋金入りのloop嫌いの人は0ではありませんが、Quicklispに収録されているライブラリをみてもloopを使っていないライブラリは、ほぼ0です。
つまりどんなに嫌いでも他人のライブラリを読むことを考えるとある程度の書法は習得せざるを得ません。
恐らく、doを使わない人よりも、loopを使わない人の方が少ないでしょう。
なお、ポール・グレアムはもうCommon Lispは書いておらず自作Lispで何かしているようです。

一次元配列の要素を別の配列にコピーする方法はCommon Lispでは繰り返し構文での実現になりますか?

ある一次元配列aの要素を、既存の配列bにコピーする——

(let ((a (vector 1 2 3 4))
      (b (vector 0 0 0 0)))
  (dotimes (i (length a))
    (setf (aref b i) (aref a i)))
  (list a b))(#(1 2 3 4) #(1 2 3 4))

——のようなものは、replaceを使うことで簡潔に記述可能です。

(let ((a (vector 1 2 3 4))
      (b (vector 0 0 0 0)))
  (replace b a)
  (list a b))(#(1 2 3 4) #(1 2 3 4)) 

replaceではコピーする範囲を指定することも可能です。

(let ((a (vector 1 2 3 4))
      (b (vector 0 0 0 0)))
  (replace b a
           :start1 0 :end1 3
           :start2 1 :end2 3)
  (list a b))(#(1 2 3 4) #(2 3 0 0))

また、範囲を指定する場合は、subseqでも記述可能です。

(let ((a (vector 1 2 3 4))
      (b (vector 0 0 0 0)))
  (setf (subseq b 0 3) (subseq a 1 3))
  (list a b))(#(1 2 3 4) #(2 3 0 0))

さらに、subseqで全範囲のコピーも可能です。
しかし、この場合は、素直にreplaceを使用する方が簡潔でしょう。

(let ((a (vector 1 2 3 4))
      (b (vector 0 0 0 0)))
  (setf (subseq b 0) (subseq a 0))
  (list a b))(#(1 2 3 4) #(1 2 3 4))

Common LispはSchemeと違って末尾呼び出しの最適化の義務が仕様にないので末尾呼び出しは遅くなりますか?

Common Lispの処理系でも末尾呼び出し(や末尾再帰)の最適化は実施するコンパイラは多く、出力されるコードの速度自体は遅くありません。
しかし、Common LispにはSchemeには存在しないスペシャル変数など、関数呼び出し以外でもスタックを消費する要因が存在しており、最適化することが難しい場合があります。
Schemeよりも高速なコードを生成するコンパイラはCommon Lispには多く存在しますが、スタック消費の最適化が上手く実施されていない場合は、実行は高速でもスタックオーバーフローするようなコードが生成されます。
スタックオーバーフローするようなコードが生成されていないかどうかは、コードをdisassembleなどして確認しますが、ここまでするのは手間であるため、結局は繰り返し構文で書くことが多くなります。

標準関数と同名のローカル関数を定義したり、tnilを変数として利用しようとすると警告やエラーになります。不便過ぎではないでしょうか?

既存のシンボルを使い回しているのが原因なので、パッケージを定義してシンボルを使い回さないようにします。
しかし、普通のCommon Lispプログラマにとっては把握し難いコードになることに留意する必要があるでしょう。
なお、Common Lispに慣れすぎると他の言語でもtを変数名に使うことを無意識に避けるようになります。

;; 既存の定義が存在するシンボルを使い回してエラーになる
(in-package cl-user)

(defun list-size (list)
  (let ((t 0))
    (labels ((count ()
               (incf t)))
      (dolist (x list)
        (count)))
    t))
...
; caught ERROR:
;   Lock on package COMMON-LISP violated when binding COUNT as a local function
;   while in package COMMON-LISP-USER.
;   See also:
;     The SBCL Manual, Node "Package Locks"
;     The ANSI Standard, Section 11.1.2.1.2
...
;; 新規のシンボルを使うようにパッケージを定義
(defpackage foo
  (:use cl)
  (:shadow count t nil))

(in-package foo)

(defun list-size (list)
  (let ((t 0))
    (labels ((count ()
               (incf t)))
      (dolist (x list)
        (count)))
    t))

(list-size '(0 1 2 3))4

なお、ローカル関数名が標準関数名をシャドウする(同じシンボルを使い回す)場合の問題として、シャドウされた環境内で展開されたマクロの動作が意図しないものになることが挙げられます(展開結果の関数がローカル関数に差し替わってしまうため)。
Common Lisp規格では標準関数のシャドウの結果は未定義としており、不正なコードということになるため、警告やエラーとする処理系が多いようです。

文字列中に改行を含めたいのですが

Common Lispは文字列中に改行をそのまま含めることが可能です。

(list "いろはにほへと
ちりぬるを
わかよたれそ
つねならむ")("いろはにほへと
ちりぬるを
わかよたれそ
つねならむ")

なお"を含める場合は、"\""のようにバックスラッシュでエスケープします。
バックスラッシュ単体を含めたい場合にも"\\"のようにエスケープが必要です。

スペシャル変数でも定数でもない大域変数を定義したいのですが

スペシャル変数はlet等で再束縛も可能ですが、プログラムの実行時に値が結合するという特性上、クロージャーとして閉じ込めることができません。
定数は、クロージャーとして閉じ込めることは可能ですが、let等で再束縛することはできず、値を変更することもできません。
再束縛も可能で、クロージャーとしても閉じ込められる大域変数を直接定義する方法はCommon Lispには存在しませんが、大域定義のシンボルマクロで代用することが可能です(シンボルマクロの識別子がレキシカルな振舞いであるため)

(define-symbol-macro foo (symbol-value '#:foo))
(setq foo 42)

(defun make-foo ()
  (lambda () foo))

(let ((foo 0))
  (declare (special foo))
  (list (funcall (make-foo))
        (symbol-value 'foo)))(42 0)

上記のようなコードを定義するための構文が、deflexのような名前でユーティリティライブラリに収録されていることもあります。

なお、On Lispでは大域のレキシカル変数のようなものを想定したコードがありますが、同書中に正しい書法の解説は含まれていないため、読者はこの問題に遭遇するようです。

Common LispのcondではSchemeの=>が使えないのですか?

はい、使えません。
The Revised Report on SCHEME: A Dialect of LISP(1978)を眺めると、当時のMACLISP(Common Lispの先祖)で常套句となっていた、

(cond ((setq it (get 'x 'property)) (hack it)))

のようなものが、=>を使って

(cond ((get 'x 'property) => (lambda (it) (hack it))))
or
(cond ((get 'x 'property) => hack))

のように一時変数を使うことなく書けるとあります。

1980年代以降のLispで述語部でsetqを使うという書法はあまり見掛けませんが、Schemeのcond/=>を翻訳する場合に、先祖返りして書いてみるというのも趣があるでしょう。
近年のCommon Lispではif-let系であったり、アナフォリックマクロ系のライブラリの構文がよく使われます。

デバッグが困難になる原因の筆頭はマクロが適切に使われていないことですか?

まず、Common Lispのマクロについていうと、マクロ展開関数は、単なるリスト操作であり、デバッグについては、ある式を表現するリストからリストへの変換関数のデバッグにほぼ等しいものになります。
つまり、リスト操作関数のデバッグ以上のものはまずないといえます。

マクロによって引き起されるエラーは、変換後のリストが正しい式を構成していないことに起因することが殆どであるため、展開前の式と展開後の式を対話的に確認しつつデバッグやプログラムができれば、特に困難なことはありません。

また、Common Lispでのマクロ展開の確認には言語に組込まれているmacroexpandという関数を使いますが、開発環境がこれらの対話的操作を向上させるような支援機能が豊富に用意されています。(slime-macroexpand-allなど)

逆にいうと変換元から変換先の式が対話的に確認できないようなケースでは、デバッグが困難であるともいえます。
Common Lispでは、コードウォーカーを駆使したマクロなどで展開結果を確認することが困難である場合があります。

しかし、デバッグ時にデバック対象のコードのマクロの展開結果がそのまま表示されてしまうなどの問題はあるのでは?

その通りですが、単に展開結果をデバッグすれば良いだけなので大した問題ではありません。
Common Lispでは、変換元の式が、正しいプログラムとして展開されているか、次に意図したプログラムになっているかを確認するというサイクルを回しながら、式の変換関数(マクロ)を作成します。

Racketのようにマクロをステップ実行する機能があるCommon Lisp処理系もありますが(Allegro CLなど)、ミス変換の結果生じた不正なプログラムをステップ実行できてもあまり意味のあることにはなっていない(人気もない)のが現状です。

また、マクロの展開結果がそのまま表示されてしまうという問題意識の一つとして、恐らく低レベルの構文要素だけで構成されるCommon Lispプログラムが読めないということがあるかと思います(スペシャルオペレーターだけで構成されたCommon Lispのコードが読めないなど)
これについては、単純に慣れれば読めるようになるでしょう(標準マクロは展開しないように表示する支援環境もあります(Allegro CLなど))。

fletなどのローカル束縛構文で、Common Lisp標準の関数名などをシャドウすると処理系や、コンパイルの前後で結果が違ったりします。これは処理系のバグでしょうか。

(flet ((cons (head tail)
         (vector head tail)))
  (cons 1 2))
→ #(1 2)
or → (1 . 2)
or error

言語組み込みの標準パッケージであるCOMMON-LISPのシンボルをシャドウした結果は規格上未定義動作のため処理系のバグとはいえません。

厳密にいうと、既存の変数名を変数名でシャドウ、関数名を新たな関数名でシャドウするのでなければ問題はなく、listなどは変数名としてよく使われています。

;; OK
(let ((list '(0 1 2)))
  list)(0 1 2)

;; NG
(flet ((list (head tail)
         (vector head tail)))
  (list 1 2))
→ 未定義

どうしても標準関数名をシャドウしたければ、シャドウが発生しないようにパッケージを作成してコーディングするのがよいでしょう。ただし、コードをコミュニケーションツールとして考えているならば控えたほうがよいというのが一般的なCLerの感想でしょう。

変数名の両端に+が付いたものがあるのは何故ですか?

大域定数を判別しやすくするための慣習です。スペシャル変数のように言語仕様内でも使用されている命名規約ではなく、言語仕様内では、大域定数には特に何も付加しません(most-positive-fixnum など)。
CLIM発祥の命名規約といわれています。

RMSはCommon Lisp の言語仕様をとりまとめた主要4人のうちの一人ですか?

RMSは偉大ですが、Common Lisp仕様の議論には参加してるものの、主要人物ではありませんでした。
また、主要人物は4人ではなく、5人でした。(Guy Steele, Scott Fahlman, David Moon, Daniel Weinreb, Richard Gabriel)

変数名の両端に*が付いたものがあるのは何故ですか?

Common Lispでは、実行時に結合(束縛関係を確立)する変数があり、特殊変数と呼ばれます。
特殊変数は、大域変数としても利用されますが、束縛構文などによってローカルにシャドウすることも可能になっています。
これらの性質により、特殊変数は競合しやすいのものとなっているため命名規約で競合を防ぐという慣習があり、規格内で定義されている特殊変数はすべてこの命名規約に従っています。(*standard-output*など)

Schemeプログラマなどもこの規約を真似して書くことがありますが、Schemeの仕様内にこのような命名規約はなく、大域変数の目印という意味以上のものではないようです。

参照

大き目のリストをapplyしたらエラーになりました

(apply #'+ (make-list 2048 :initial-element 1))
>>>Error: Last argument to apply is too long: 2048

Common Lisp規格では処理系毎に引数の上限が決まっており、 call-arguments-limit の値で確認することが可能です。
最近の64bit処理系では、2^32〜2^62程度の大きさですが、案外小さな値の処理系も存在します。

call-arguments-limit
→ 2047 ;lispworks
or→ 65536;ecl/clozure cl
or→ 64;gcl

エラーを回避する書き方については、引数リスト内の値の並び順を考慮する必要がないのであれば、reduceなどで書き直すことが可能です。

(apply #'+ (make-list 2048 :initial-element 1))(reduce #'+ (make-list 2048 :initial-element 1))  

しかし、引数リスト内の値の並び順を考慮する必要がある場合は、結果がapplyと同じになるように都度対処する必要があります。

プログラム実行時にレキシカル変数を外部から差し込んだり操作したりすることは可能ですか?

レキシカル変数はプログラムの字面上の文脈だけからスコープを決定する方式ですが、スコープは静的に決定できるため、プログラムの意味が変らない条件下でコンパイラは変数についての余計な情報は削除してしまったり、変数を即値に展開して埋め込んでしまう等の最適化を実施します。
対話的な操作に馴染みのあるLispではレキシカル変数の変数名などの情報も実行時には利用できて然るべきと考えがちですが、コンパイラ指向のCommon Lispではこのようなことはできないようになっています。
処理系によっては、インタプリタ動作時限定であったり、デバッグオプションの設定でレキシカル変数名が取得できたりしますが、飽く迄デバッグの補助の為の機能と考えた方がよいでしょう。

実行時に変数を操作したいような場合には、Common Lispではスペシャル変数を活用します。
Common Lispのスペシャル変数は十分高速です。コンパイル時に難解なコードウォーキングをするマクロを駆使して、どうにかレキシカル変数を操作しているようにみせかけることに腐心するより、スペシャル変数を活用した方が問題を簡単に解決できることが多いでしょう。

(defun fib (n)
  (declare (optimize (speed 3) (safety 0) (debug 0)))
  (declare (type fixnum n))
  (the fixnum
       (if (< n 2)
           n
           (+ (fib (1- n))
              (fib (- n 2))))))

(fib 40)
;=> 102334155
#|------------------------------------------------------------|
Timing the evaluation of (FIB 40)

User time    =        0.872
System time  =        0.000
Elapsed time =        0.873
Allocation   = 624 bytes
0 Page faults
GC time      =        0.000E
 |------------------------------------------------------------|#
;;; スペシャル変数多用版fib
(defvar *n*)
(defvar *sym*)

(defun *fib* ()
  (declare (optimize (speed 3) (safety 0) (debug 0)))
  (declare (type fixnum *n*)
           (type symbol *sym*))
  (the fixnum
       (if (< *n* 2)
           *n*
           (+ (let ((*n* (1- (symbol-value *sym*)))) ;シンボルの名経由で値を取得している
                (*fib*))
              (let ((*n* (- (symbol-value *sym*) 2)))
                (*fib*))))))

(let ((*n* 40)
      (*sym* '*n*))
  (*fib*))
;=> 102334155
#|------------------------------------------------------------|
Timing the evaluation of (LET ((*N* 40) (*SYM* '*N*)) (*FIB*))

User time    =        1.552
System time  =        0.000
Elapsed time =        1.554
Allocation   = 6528 bytes
0 Page faults
GC time      =        0.000E
 |------------------------------------------------------------|#

fixnum型のデータをシリアライズするとポータブルでなくなりますか?

ANSI規格では、fixnum(signed-byte 16)の上位型であることが要求されています。 つまり、-32768から32767の範囲のintegerなら、どのようなCommon Lispでも(typep i 'fixnum) → Tですが、それなら直接(signed-byte 16)を指定した方が早そうです。 また、fixnumを越えた範囲はinteger` (bignum)へ自動で変換されるので特別のことでもない限り気にする必要はなさそうです。

Common Lispにはexitは存在しないのでしょうか

実行中の処理系から抜ける命令は用意されているのですが、規格で定められてはいません。歴史的には、quitというものが多く、exit 1のようなものは、(quit 1)か、(exit 1)のように書くことが殆どです。LispマシンのOSがLispだった頃の名残りの仕様と考えられています。

CLOSはCommon Lispの方言なのでしょうか?

最初のCommon Lisp(1984)の仕様にオブジェクトシステムは含まれておらず、ANSI規格(1994)になる際に後付けされた格好ですが、オブジェクトシステムの開発の中心となっていたXEROXなどでは、オブジェクトシステムが加わった新しいCommon LispをCLOSというLisp方言として呼称することが多かったようです。
この状況に影響されてか1990年代のオブジェクト指向関係の書籍(C++関連など)でCommon Lispが取り上げられる際には、CLOSという名前で言及されていることが殆どです。
また、Franzなども1990年代はマーケティングのためかCLOSという呼称をプッシュしていたようです。
現在では、主にCommon Lispのオブジェクトシステムという意味で使われていますが、殊更オブジェクトシステムだけに名前をつけて呼称する必要性もなく慣習的なものとなっています。
ちなみにANSI Common Lisp規格書の中にCLOSという用語は一度も出現しません。
いずれにせよ「CLOS」、「CLOS風の○○」という言及をしている人は1990年代を引き摺っていることは確かでしょう。

メソッドコンビネーションの+maxはどのような局面で使うのでしょうか?

メソッドコンビネーションという概念が誕生したFlavorsの時代から+maxが使われることはまずなかったようです。
通常のプログラミングにおいて一連のメソッド呼び出しの結果を足し算したいという局面には、ほぼ遭遇しないからではないでしょうか。
prognorや、ユーザー定義のメソッドコンビネーションの用例はみつけることができます。

Smalltalkでいうbecomeに相当するものはCommon Lispに存在しないのでしょうか?

インスタンスのIDを保持したまま別のクラスのインスタンスに変化するということが目的の本質であるならchange-classが該当します。
但し、変更可能なインスタンスは、standard-objectのサブクラスである必要があり、IDを保持したまま01にするようなことはできません。

prog2はどのような局面で使うのでしょうか?

副作用目的の前処理と後処理を従えた処理のような場合に使えますが、prog1prognの組み合わせで用が足りるので忘却された存在です。
メソッドコンビネーション定義ではこのような処理の頻度が高いため、そのためのユーティリティとして、multiple-value-prog2というものが提案されていたこともありますが規格には取り入れられていません。

(prog2
  (print "前処理")
  (+ 1 1)
  (print "後処理"))
;>>  
;>>  "前処理" 
;>>  "後処理" 
;=>  2

(progn
  (print "前処理")
  (prog1
    (+ 1 1)
    (print "後処理")))
;>>  
;>>  "前処理" 
;>>  "後処理" 
;=>  2

Common Lispでは、publicとprivateの違いをパッケージの外部シンボルか内部シンボルかで表現しますか

Common Lispのパッケージの外部シンボルは、外部APIであるという表明程度のもので、保護機能等はありません。
スコープを区切ることにより保護することも可能ですが、このようなことは慣習化されているとはいえません。

(labels ((foo/private (x) x)
         (bar (x) x)
         (baz (x) (foo/private x)))
  (setf (fdefinition 'bar) #'bar)
  (setf (fdefinition 'baz) #'baz))

(defun foo/private (x) (list x x))

(foo/private 42)
;=>  (42 42)

(baz 42)
;=>  42

配列のコピーは手続的にループで書く方法しかありませんか?

copy-seqを使います。

(type-of (copy-seq (make-array 10 :element-type '(unsigned-byte 8))))
;=>  (simple-array (unsigned-byte 8) (10))

日本人なら国産Common Lispを作るべきですか?

インターネットとOSS全盛の時代に国粋主義というのも恐らく完全なる時代錯誤かと思いますが、国粋主義でいうならCommon Lispはアメリカの規格なので、日本独自規格の言語の処理系を作るのが順当ではないでしょうか。
ちなみに、1980年代の国際AI競争の枠組みでいうと日本では第五世代プロジェクトがありましたが、研究されていた言語はCommon Lispではありませんでした。

メソッドを再定義する際にはfmakunboundするしかありませんか?

fmakunboundすると総称関数ごと消えてしまいます。例えば、initialize-instancefmakunboundすると大惨事になるので実行することは現実的ではありません。開発環境が用意している機能をマニュアルで確認しましょう。

LispWorksにはエディタにUndefineという定義削除のコマンドがあります。Allegro CLのようにfmakunboundが拡張されていることもあります。

(defmethod foo ((x integer)) x)
(defmethod foo ((x cons)) x)

(fmakunbound '(method foo (integer)))
;=> (METHOD FOO (INTEGER))

開発環境に機能が存在しない場合には、remove-methodを直接実行することになります。

(remove-method #'foo (find-method #'foo () (mapcar #'find-class '(integer))))

Common LispはLispの核にオブジェクトシステムを付加したものなので純粋なオブジェクト指向言語とはいえないのでは?

Common Lispの設計のゴールに「純粋なオブジェクト指向言語とはいえるかどうか」は入っていないようです。
※Common Lispの設計のゴールはCLtL1及び2を参照のこと

Common Lispのオブジェクトシステムは、それ自体がCommon Lispで実装されていますか?

その通りですが、オブジェクトシステムに限らず大抵のCommon Lisp処理系の殆どの部分がCommon Lisp自身で書かれています。

多数の方言が存在したLisp界の天下統一を果したのがCommon Lispですか?

1980年初頭にAI研究に予算を投じていたDARPAでしたが、研究所ごとに互換性のないシステム(=Lisp)に研究費を投じるのは無駄なので統一して欲しいという要求に応えたのがCommon Lispです。
当時、予算を獲得していた研究所で主に使われていたのはMACLISP方言だったので、ほぼMACLISP方言を統一する規格のような形になりました。
さらに、Strategic Computing Initiativeなどもこの流れを推進します。
つまり、納品先が指定する仕様に合せる必然性があっただけで、世界的にLisp規格を統一しようという漠然とした話ではありませんでした。

eqはどのような場所で使うのでしょうか?

同一のオブジェクトかどうかを判定する場合に使いますが、シンボルの比較にeqを使う人が多いようです。

Common Lispが遅延評価でないのは何故ですか?

世の中のプログラミング言語の評価戦略の大半が遅延評価になってから検討し始めても遅くなさそうです。

Common Lispに複数行コメントアウトが無いのは不便だと思いませんか?

Common Lispにはあります。Emacs Lispと混同しているのではないでしょうか。

#|

複数行

コメントアウト
#|ネストもできる|#

|#

ローカルにsetfは定義できないのでしょうか

そのまま書けばいいだけです。

(flet (((setf 1st) (val list)
         (setf (car list) val)))
  (let ((x (list 0 1 2)))
    (setf (1st x) 'car)
    x))
;=>  (car 1 2)

generalized boolean とはどういうものですか

Common Lispではnil以外は真偽値として真です。これを利用して、trueとしてTの代わりに有用そうな値を返すことがあります。
例えば、(zerop 0)は、Tの代わりに0を返しても構いません(有用かはさておき)
ちなみに、memberは、generalized booleanではなくリスト返す関数です。

解説

Discussion