- Common Lispでは変数と関数の名前空間が別々に存在し、
defunで定義した関数はシンボルの関数スロットに格納される。 - 引数位置に
factと書くと変数スロットを参照してエラーになるため、関数スロットから関数オブジェクトを取り出す#'factが必要になる。 - ただし、
funcallやmapcarはシンボルを受け取ると内部で関数スロットを引く自動解決を行うため、'factでも動く場合がある。
1. どうして「関数名」を高階関数に渡せないの?
Common Lispでは、関数を定義して、使うことができます。
たとえば、fact(階乗)は、
(defun fact (n)
(if (<= n 1)
1
(* n (fact (- n 1)))))Code language: Lisp (lisp)
ただ、この fact を使っていると、こんな疑問に直面します。
(mapcar fact '(1 2 3 4 5)) ; エラー
(mapcar #'fact '(1 2 3 4 5)) ; → (1 2 6 24 120)Code language: Lisp (lisp)
mapcar に関数名fact をそのまま渡すとエラーになる。
わざわざ #'fact と書かないといけません。
(mapcar (lambda (x) (fact x))
'(1 2 3 4 5)) ; → (1 2 6 24 120)Code language: Lisp (lisp)
ちなみに、lambda を使えば fact を呼べます。
1.1. REPLで「関数名」が参照できない?
REPLを使っていると、もう一つの疑問が浮かびます1。
* fact
; Evaluation aborted on #<UNBOUND-VARIABLE FACT {12018E7063}>.
* 'fact
FACT
* #'fact
#<FUNCTION FACT>Code language: Lisp (lisp)
defun で定義したはずの fact なのに、未定義のエラーになります。#'fact と書いて初めて関数オブジェクトが返ってきます。
この二つの疑問は同じ背景から来ています。
それは、Common Lispがシンボルの名前空間を二つ持つ言語だ、ということです。
2. シンボルとスロット
実は、Common Lispでは、fact と書いても「関数名」ではありません。
「シンボル」というオブジェクトです。
シンボルはインタープリタ起動時から管理されているシンボルテーブルに登録されます2。
ここで大事なのは、シンボルは複数のスロットを持っていることです。
名前スロット、変数スロット、関数スロット、プロパティスロット、パッケージスロットです3。
defun で関数を定義するとは、シンボル FACT の関数スロットに関数オブジェクトを格納することです。
このとき、変数スロットは空のままです。
つまり、冒頭で定義した fact の場合、定義後のシンボルの状態はこうなっています。
シンボル FACT
名前スロット : "FACT"
変数スロット : 未束縛
関数スロット : #<FUNCTION FACT>Code language: PHP (php)
変数スロットの定義をするなら、defvarやdefparameter、letなどを使います。
2.1. シンボルの評価とスロット
シンボルが式の中に配置されると、その中身が「評価」されます。
ただし、どのスロットを参照するかは位置によって変わります。
通常 fact を式として評価すると、Common Lispは変数スロットを参照します。
つまり、基本は「変数名」として扱われます。
例外はS式の先頭に置かれた場合で、(fact 5) のように書くとコンパイラは関数スロットを参照します。
このため、普通に関数を呼び出すときには、自動的に「関数名」として扱われ、シンボルと関数の違いを意識しないで済みます。
2.2. functionと関数オブジェクト
ところが、問題になるのは引数位置に「関数名」を書いたときです。
定義した関数を高階関数に引数として渡そうとすると、そのままでは引数位置のシンボルは変数スロットを参照してしまうため、エラーになります。
fact ; 変数スロットを参照、未束縛なのでエラー
'fact ; シンボルオブジェクトそのものを返す
#'fact ; 関数スロットを参照、関数オブジェクトを返す
(function fact) ; 関数スロットを参照、関数オブジェクトを返すCode language: Lisp (lisp)
そこで、引数位置で関数を指すには、関数オブジェクトを取り出す必要があります。
シンボルを与えると関数オブジェクトを返す特殊形式 function があり、#' はその略記です4。mapcar に渡せるのは関数オブジェクトなので、#'fact が必要になります。
このように、Common Lispは変数の名前空間と関数の名前空間を別々に持つ言語で、これをLisp-2と呼びます5。
ちなみに、SchemeはLisp-1で、変数と関数が同じ名前空間にあります。
Schemeなら fact と書くだけで関数オブジェクトが返りますが、Common Lispはそうではありません。
3. シンボルが必要か、関数オブジェクトが必要か?
3.1. 裸のシンボル:マクロの引数
マクロは引数を評価する前に受け取るので、引数はシンボルそのものです6。defun の関数名や typecase の型指定では、裸のシンボルをそのまま書きます。
(defun fact (n) ...) ; factは評価されない
(typecase x
(fixnum (print "整数")) ; fixnumは評価されない
(string (print "文字列")))Code language: Lisp (lisp)
'fact と書いても動きますが、慣習として書きません。
3.2. #' 関数オブジェクト:高階関数への引数
高階関数には、関数オブジェクトを与えるため #' を使います。
(mapcar #'fact '(1 2 3 4 5)) ; → (1 2 6 24 120)
(funcall #'fact 5) ; → 120
(apply #'fact '(5)) ; → 120Code language: Lisp (lisp)
たとえば、mapcarは、関数オブジェクトを与えるので、lambdaによって関数オブジェクトをその場で作って与えることもできるのです。
3.3. クォートしたシンボル:シンボルを値として渡す
一部の関数には、シンボルをクォートして渡す設計のものもあります。
たとえば、typep 関数は受け取ったシンボルから内部で型情報を取得します。
(typep 42 'fixnum) ; → TCode language: Lisp (lisp)
typep は関数なので引数が評価されます。'fixnum と書いてシンボルオブジェクトを渡すと、内部でそのシンボルから型情報を引きます7。
4. 'fact でも動く(自動解決)
#'fact と 'fact のどちらでも動く関数もあります。
これは、自動解決です。
funcall や mapcar はシンボルを受け取ったとき、内部で symbol-function を呼んで関数スロットを引いてくれるからです8。
(funcall 'fact 5) ; → 120(動く)
(mapcar 'fact '(1 2 3 4 5)) ; → (1 2 6 24 120)(動く)Code language: Lisp (lisp)
本来は「シンボルと関数オブジェクトは別物」ですが、自動的に変換してくれるため仕組みが見えにくくなります。
4.1. #' と ' の差が出る場面
#'fact と 'fact のどちらでも動く場合でも、微妙に違いがあります。
変数に関数オブジェクトやシンボルを束縛してから、defun で関数を再定義すると、挙動が違うことがわかります。
(defun fact (n)
(if (<= n 1) 1 (* n (fact (- n 1)))))
(defparameter *fn-obj* #'fact) ; 関数オブジェクトを保存
(defparameter *fn-sym* 'fact) ; シンボルを保存
;; factを別の実装で再定義
(defun fact (n) 0)
(funcall *fn-obj* 5) ; → 120(再定義前の関数オブジェクトを保持)
(funcall *fn-sym* 5) ; → 0(シンボルを経由するので再定義後の関数が呼ばれる)Code language: Lisp (lisp)
#'fact は、評価した時点の関数オブジェクトを確定させます。'fact は、呼び出し時にシンボルテーブルを引きます9。
基本的には #'fact を使う方が意図が明確で、コンパイラの最適化も効きやすい場面があります。
再定義後に新しい実装に切り替わるようにしたい場合に 'fact が向いていることになります。
5. まとめ
Common Lispでシンボルと関数オブジェクトが別物である理由は、変数と関数の名前空間が分かれているLisp-2の設計にあります。defun は関数スロットに書き込み、変数スロットには触れません。
だから裸の fact は変数スロットを参照してエラーになり、#'fact と書いて初めて関数オブジェクトが取り出せます。
funcall や mapcar が 'fact でも動くのは、それらが内部でシンボルを解決しているからです。
仕組みを理解した上で #'fact を使うと、意図が伝わりやすいコードになります。
- REPLはRead-Eval-Print Loopの略で、入力を読み取り、評価し、結果を表示するインタラクティブな実行環境です。Common Lispの主要な実装であるSBCL、CCL、ECLなどがREPLを提供しています。 – Common Lisp Cookbook – Getting Started
- シンボルをテーブルに登録する操作をinternと呼びます。
'factと書いたとき、リーダーは内部で(intern "FACT")に相当する処理を行い、既存のシンボルがあればそれを返し、なければ新規作成します。 – CLHS: Function INTERN - パッケージはシンボルの名前空間を管理する仕組みで、異なるパッケージの同名シンボルは別オブジェクトです。 – CLHS: System Class SYMBOL
functionはスペシャルオペレータです。マクロやスペシャルオペレータを表すシンボルに対して使うとエラーになります。#'はリーダーマクロとして定義されており、#'factは読み取り時に(function fact)に展開されます。 – CLHS: Special Operator FUNCTION- Lisp-1/Lisp-2という用語は、Richard P. GabrielとKent M. Pitmanによる論文で整理されました。Schemeが変数と関数を同一の名前空間に置くLisp-1であるのに対し、Common Lispは別々の名前空間を持つLisp-2として設計されています。 – Gabriel & Pitman, “Technical Issues of Separation in Function Cells and Value Cells”, Lisp and Symbolic Computation, 1:1, 1988
defun自体もマクロです。引数を評価せずに受け取るため、関数名を裸のシンボルで書きます。評価されないので'factと書く必要がなく、慣習としても書きません。 – CLHS: Macro DEFUNtypepの第二引数は型指定子(type specifier)で、シンボル、クラスオブジェクト、またはリスト形式を受け取ります。シンボルとして渡すことで、型名をランタイムに決定できます。 – CLHS: Type Specifiers- HyperSpecの
funcallの仕様には「引数がシンボルの場合、グローバル環境でその関数値を見つけることにより関数に変換される」と記述されています。ローカルのfletやlabelsで定義した関数はこの解決では参照されません。 – CLHS: Function FUNCALL - この挙動はゲームや可視化ツールのような、ループ中に関数定義をライブ更新したい場面で
'fact形式が有効になります。一方、特定の実装を固定してテストしたい場合は#'factが適しています。 – Common Lisp Cookbook – Functions