Common Lispプログラミングにおいて、lambdaは欠かすことのできない重要なパーツの一つですが、このlambdaにはいくつか知っておくべき慣習や決まり事があります。この記事では、lambdaに関する簡単な背景をふまえた上で、知っておきべき知識を簡潔にまとめようと思います。
ラムダ式
以下の形式のフォームをラムダ式と呼びます。ラムダ計算においては厳密にはラムダ抽象と呼ばれるのですが、Common Lispではラムダ式と呼びます。
|
1 2 |
(lambda (...) ...) |
プログラム内で、そのまま(lambda (...) ...)と書くと、コンパイラによって別物(後述)に変換されるので、通常はquoteを使ってラムダ式を表現します。
|
1 2 3 |
CL-USER> '(lambda (x) x) (LAMBDA (X) X) |
ご覧の通り、ラムダ式は普通のリストと何ら変わりがありません。
ラムダ式のfuncall
せっかくラムダ式を定義したのですから、ラムダ計算の最も重要な操作であるβ簡約(ここではfuncall)を定義しましょう。
|
1 2 3 4 5 6 7 8 9 10 11 12 |
(defun my-funcall (lambda-form &rest args) (if (and (consp lambda-form) (eq (car lambda-form) 'lambda)) (let* ((lambda-list (cadr lambda-form)) (lambda-body (cddr lambda-form)) (form `(let ,(loop for formal in lambda-list for arg in args collect `(,formal ,arg)) ,@lambda-body))) (eval form)))) |
Common Lispに詳しくない人のために簡単に説明しますと、このmy-funcall関数は、ラムダ式の引数リストと、my-funcallに渡された引数とのリストで、let束縛を持つフォームを生成し、そのフォームをevalすることでβ簡約(funcall)をエミュレートしています。例えば次の式を考えてみます。
|
1 2 3 |
CL-USER> (my-funcall '(lambda (x y) (+ x y)) 1 2) 3 |
この時、my-funcallは
|
1 2 |
(let ((x 1) (y 2)) (+ x y)) |
というフォームを生成してevalします。その結果、funcallと同じような効果が得られるわけです。
第一級オブジェクトとfunarg問題
ところで、他の関数にラムダ式を渡したり、関数の返り値としてラムダ式を返せたら便利だと思いませんか?あるプログラミング言語がこのような性質を備えているとき、ラムダ式は第一級オブジェクトと呼ばれます。また、ラムダ式を受け取ったり返したりする関数を高階関数と呼びます。当然ながらCommon Lispもこの重要な性質を備えています。
ラムダ式を第一級オブジェクトとして扱うにあたって考えなければならないのが、ラムダ式の自由変数を誰が補足するか、という問題です。例えば次の例を考えてみます。
|
1 2 3 4 5 6 7 |
(defun make-adder (x) '(lambda (y) (+ x y))) (let ((x 1)) (let ((adder (make-adder 2))) (my-funcall adder 3))) |
make-adderは整数を受け取って、その整数に別の整数を足す関数を返します。二つ目の式はxを1に束縛した後、2を足すadder生成し、そのadderに3を渡しています。この式は実行可能な正しい式ではありませんが、重要なのは、(my-funcall adder 3)の時にどのxが参照されるべきか、という点です。make-adderのxを無視するなら、評価結果は4になりますし、(let ((x 1))のxを無視するなら、評価結果は5になります。このように、ラムダ式をデータとして表現したときに発生する、自由変数の補足(環境)の曖昧さの問題を、funarg問題と呼びます。
functionスペシャルフォーム
funarg問題を適切に解決するためには、評価時にラムダ式を関数クロージャ(または単にクロージャ)に変換する必要があります。関数クロージャには、関数クロージャが生成されたときの環境が記録されているため、上記したfunargの問題が発生しません。その変わり、ラムダ式を関数クロージャに変換する特別な構文規則が必要になります。Common Lispでは、functionスペシャルフォームがその役割を持っています。functionスペシャルフォームは、与えられたラムダ式を、現在の環境を閉じこめた関数クロージャに変換します。
|
1 2 3 |
CL-USER> (function (lambda (x) x)) #<Anonymous Function #x30200111920F> |
上記したfunarg問題の例をfunctionスペシャルフォームを使って書き直してみましょう。
|
1 2 3 4 5 6 7 |
(defun make-adder (x) (function (lambda (y) (+ x y)))) (let ((x 1)) (let ((adder (make-adder 2))) (funcall adder 3))) |
評価結果は5になります。funarg問題はちゃんと解決されているようです。
#'とlambdaマクロ
ラムダ式をいちいちfunctionスペシャルフォームで囲わないといけないのは面倒ですよね。そこでもっと簡潔に書くために#'リーダーマクロが導入されました。例えば
|
1 2 |
#'(lambda (x) x) |
は
|
1 2 |
(function (lambda (x) x)) |
に展開されます。
ただ、#'を書くのも面倒ですし、#'を忘れてしまうとエラーになってしまいます。そこでlambdaマクロが導入されました。例えば
|
1 2 |
(lambda (x) x) |
は
|
1 2 |
#'(lambda (x) x) |
に、つまり
|
1 2 |
(function (lambda (x) x)) |
に展開されます。仕様書には次のように記載されています。
|
1 2 3 4 |
(lambda lambda-list [[declaration* | documentation]] form*) == (function (lambda lambda-list [[declaration* | documentation]] form*)) == #'(lambda lambda-list [[declaration* | documentation]] form*) |
最初のlambdaはlambdaマクロを持つフォームであって、ラムダ式ではないことに気をつけてください。後ろの二つはどちらもラムダ式です。
実際の歴史が上記したストーリーを経ているかは、僕は確たる証拠を持っていません。Webで見つけた情報を適当に編纂しただけですので、間違いがあったら教えてください。
#'(lambda (...) ...)
ところで、仕様書にlambdaマクロが含まれているのにも関わらず、多くのライブラリのソースコードで
|
1 2 |
#'(lambda (...) ...) |
と記述されているのを目にします。本質的には、この#'は必要ないはずなのですが。
噂では、lambdaマクロを持っていない古い処理系に対応するためらしいですが、それならlambdaマクロを別途定義しちゃえばいいじゃん、と思ってしまいます。また、ポール・グレアムは上記の記法を推奨しているという話も耳にはさみました。ソースが分からないので、その理由はまだ確認していません。とにかく、僕には今のところ、この#'を書くべき理由が見つかりません。
シンボルに#'をつけるべきか
Common Lispのややこしいところは、#'を付けなくてもfuncallできたりするところです。
|
1 2 3 |
(funcall '+ 1 2) (funcall #'+ 1 2) |
どちらも3を返しますが、この両者に確たる違いはあるのでしょうか。実はこの二つでは、シンボルの関数セルから関数を取り出すタイミングが異なります。'+のほうはfuncallが+から関数を取り出しますが、#'+のほうはfunctionスペシャルフォームが+から関数を取り出します。そのため、以下のように高階関数に関数を渡すときは#'を付けるほうが、一般的にルックアップのコストが低くなります。
|
1 2 3 |
(find-if 'stringp '(1 2 "a" 3)) ; 少し遅い (find-if #'stringp '(1 2 "a" 3)) ; 少し速い |
また、#'を付けておけば構文的にその評価結果が関数であることが分かるので、コンパイラによる最適化が望めます。つまるところ、シンボルを関数として利用する場合は、基本的に#'をつけておくべき、というのが結論になります。
ラムダ式の特別ルール
予備知識として知っておいて欲しいのですが、ラムダフォームと呼ばれる次の形式
|
1 2 |
((lambda lambda-list . body) . arguments) |
は、構文的に次の関数フォームと同じです
|
1 2 |
(funcall #'(lambda lambda-list . body) . arguments) |
つまり、関数フォームの関数部分にラムダ式が指定されている場合のみ、特別にfuncallが補われるということになります。これはマクロを書くときに重宝するので是非覚えておいてください。
まとめ
今回はCommon Lispのlambdaにまつわる話題を提供しました。記事では触れられていない話題もあると思いますので、もし詳しい方がいらっしゃればコメントで教えていただければ幸いです。なおこの記事は、同僚の深町さんと一緒に、一週間ごとに交代で書くことになったCommon Lispシリーズの記念すべき初エントリーです。深町さんは文筆家なので、僕などボコボコにされるのがオチだと思いますが、楽しみにしていただければと思います。
最近のコメント