🚀

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

2023/10/08に公開

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

スペシャル変数は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