15
7月
2007

関数とクロージャ

Common Lispでの関数の詳細についていろいろと試してみた.
関数の基本
(setf 関数名)という関数名
大域関数と局所関数
関数のパラメータ
レキシカル変数と関数とクロージャ
動的スコープ

関数の基本

関数は基本的にdefunで定義する.
(defun 関数名 パラメータリスト 本体)
引数に1を加えて返す関数は以下のようになる.
> (defun hoge (x)
(+ x 1))
HOGE
> (hoge 1)
2
関数の返り値は,本体の最後の式の返り値,あるいは
(return-from 関数名 値)
で指定する値である.以下の関数はreturn-fromにより固定値5を返す.+式やformat式は無意味である.
> (defun fuga (x)
(+ x 1)
(return-from fuga 5)
(format t "fuga~%"))
FUGA
> (fuga 1)
5
fboundpを使用すると,指定したシンボルを関数名にもつ関数があるかどうかを確認できる.存在すれば真,存在しなければ偽を返す.
> (fboundp 'hoge)
T
> (fboundp 'fuga)
T
> (fboundp 'foo)
NIL
symbol-functionを使用すると,引数に指定したシンボルを関数名にもつ関数(の実体)を返す.
(symbol-function シンボル)
以下は変数fooに関数hogeの実体を格納し呼び出している.
> (setf foo (symbol-function 'hoge))
#lgt;CLOSURE HOGE (X) (DECLARE (SYSTEM::IN-DEFUN HOGE)) (BLOCK HOGE (+ X 1))>

> (funcall foo 1)
2

> (foo 1)

*** - EVAL: the function FOO is undefined
当然ながらfooは関数ではなく変数として認識されているので,関数のようには扱えない点に注意.関数のように扱うには以下のようにする.
> (setf (symbol-function 'var) #'hoge)
#<CLOSURE HOGE (X) (DECLARE (SYSTEM::IN-DEFUN HOGE)) (BLOCK HOGE (+ X 1))>
> (var 1)
2
> (var 2)
3
関数名がvarである関数の実体に関数名hogeの関数の実体を代入している.defunは内部でsymbol-functionとsetfの組み合わせに変換して評価しているらしい.例えば先述の関数hogeのdefunは以下のように実行されるらしい(関数名はhoge2と変更した).
> (setf (symbol-function 'hoge2)
           #'(lambda (x) (+ x 1))) 
#<CLOSURE :LAMBDA (X) (+ X 1)>
> (hoge2 1)
2
> (hoge2 2)
3

(setf 関数名)という関数名

自分で定義した関数の返り値に対して値をsetfするにはそれなりの手順が必要であるので,本節ではそれについて記述する.リストのcar部に値を設定する際には,以下のような方法がある.
> (setf x '(1 2 3))
(1 2 3)
> (setf (car x) 100)
100
> x
(100 2 3)
私はこれをxのcar部に値100をsetfするものだと思っていたが,実はそれは勘違いらしい.この勘違いはsetf式の第一引数に自分で定義した関数による式を指定すると気づく.
> (defun hoge (x) (car x))
HOGE
> (setf (hoge '(1 2 3)) 100)

*** - EVAL/APPLY: too few arguments given to (SETF HOGE)
エラーメッセージによると,setf式の第一引数に関数呼び出し式を指定すると,(setf 関数名)という関数をcallするらしいことがわかる.すなわち,先のcarの例では(setf car)という関数が存在していることになる.
(defun (setf 関数名) (引数))
引数の一番目がsetfの設定値で,二番目以降がsetf式の第一引数の式の関数への引数のように見える部分を示す.わかりにくいので具体例を示す.
> (defun (setf fuga) (val arg1 arg2)               
(format t "val[~A]:arg1[~A]:arg2[~A]~%" val arg1 arg2))
(SETF FUGA)
> (setf (fuga 1 2) 3)               
val[3]:arg1[1]:arg2[2]
NIL
二番目の式において,どの部分がval,arg1,arg2になるのかに注意.

大域関数と局所関数

defunがsetfとsuymbol-functionで表現できることからもなんとなく想像できるが,defunで定義した関数は大域関数で,大域変数と同様にどこからでも参照できる.
> (defun hoge (x) (+ x 1))
HOGE
> (progn (hoge 1))
2
> (progn (defun fuga (x) (+ x 2)))
FUGA
> (fuga 1)
3
局所変数はletで定義できるが,局所関数,すなわち特定のスコープ内でのみ参照できる関数はlabelsで定義できる.
(labels (リスト) 本体)
上のリストには関数定義式を複数記述できる.ここで定義された関数はlabels式内でのみ参照できる局所関数である.関数定義式もリストであり,以下の並びである.
(関数名 パラメータリスト 本体)
例によって本体も複数の式を記述できる.以下は二つの局所関数foo,varを作成する例である.
> (labels ((foo (x) (+ x 10))    
              (var (x) (* x 10)))
       (format t "(foo 1) = ~A~%" (foo 1))
       (format t "(var 2) = ~A~%" (var 2)))
(foo 1) = 11
(var 2) = 20
NIL
> (foo 1)

*** - EVAL: the function FOO is undefined
先に「局所変数とlet」に対して「局所関数とlabels」というような書き方をしたが,正確には「局所変数とlet*」に対して「局所関数とlabels」である.すなわち複数の関数定義式が並ぶリストでは,同じリストで定義する局所関数を使用することができる.局所関数fooを呼び出す局所関数varを定義してみる.
> (labels ((foo (x) (+ x 10))   
     	      (var (x) (foo (* x 10))))
       (format t "(foo 1) = ~A~%" (foo 1))
       (format t "(var 2) = ~A~%" (var 2)))
(foo 1) = 11
(var 2) = 30
NIL
> (labels ((var (x) (foo (* x 10))) 
              (foo (x) (+ x 10)))
       (format t "(foo 1) = ~A~%" (foo 1))
       (format t "(var 2) = ~A~%" (var 2)))
(foo 1) = 11
(var 2) = 30
NIL
二番目の例からわかるように,嬉しいことに関数定義式の順番には規制がないようだ.また,再帰する局所関数も定義できる.以下は再帰する局所関数の例である.
> (labels ((recursive (x)
  		        (if (= x 0)
			    1
		          (* x (recursive (- x 1))))))
      (format t "(recursive 5) = ~A~%" (recursive 5)))
> (recursive 5) = 120
NIL

関数のパラメータ

Cだと...とva_???を使用して変態的な(?)関数を作成することができるが,Common Lispではさらに強力な関数の引数指定ができる.
レストパラメータ
&restで指定した変数に残余パラメータの全てからなるリストが設定される.
オプショナルパラメータ
&optionalで指定する,省略でき,デフォルト値設定も可能なパラメータ.
キーワードパラメータ
&keyで指定する,パラメータ.呼び出し時にはタグとともに指定する.デフォルト値設定も可.
例えば+は任意の数の引数をとることができる.
> (+ 1 2)
3
> (+ 1 2 3)
6
> (+ 1 2 3 4)
10
これと同様のことができる関数を作成するには&restパラメータを使用する.以下は任意の数の引数をとり,コロン(:)区切りで出力する関数である.
> (defun lst-format (&rest arg)
       (dolist (disp arg)
         (format t "~A:" disp)))
LST-FORMAT
> (lst-format 1)
1:
NIL
> (lst-format 1 2)
1:2:
NIL
> (lst-format 1 2 3)
1:2:3:
NIL
関数定義の引数リストに&optionalを記述すると,それ以降,&keyが現れる前までの引数は全てデフォルトとなる.以下は,第一引数を[]で囲って出力する関数である.オプション引数を指定すると,それを第一引数の前に出力する.
> (defun disp-val (target &optional prefix)
       (if (null prefix)
           (format t "[~A]~%" target)
         (format t "~A=[~A]~%" prefix target)))
DISP-VAL
> (disp-val 1)    
[1]
NIL
> (disp-val 1 'val)
VAL=[1]
NIL
上の例からわかるように,オプション引数はデフォルト値がnilとなっている.デフォルト値を指定したければ,関数定義でオプション引数とデフォルト値からなるリストで指定する.先の例を変更して,オプション引数のデフォルト値を'valとしてみる.
> (defun disp-val2 (target &optional (prefix 'val))
       (format t "~A=[~A]~%" prefix target))
DISP-VAL2
> (disp-val2 1)
VAL=[1]
NIL
> (disp-val2 1 'a)
A=[1]
NIL
> (disp-val2 1 'val)
VAL=[1]
NIL
引数がどのオプション引数に対応するかは関数呼び出し時の位置によって決まる.複数のオプション引数を持つ関数を作成してこれを確認してみる.先の例を変更して出力行の末尾に出力する値を指定できるようにする.
> (defun disp-val3 (target &optional (prefix 'val) (suffix "."))
(format t "~A=[~A]~A~%" prefix target suffix))
DISP-VAL3
> (disp-val3 1)
VAL=[1].
NIL
> (disp-val3 1 'a)
A=[1].
NIL
> (disp-val3 1 'a '!)
A=[1]!
NIL
> (disp-val3 1 '!)
!=[1].
NIL
このように複数のオプション引数が定義されているとき,指定されたオプション引数がどのオプション引数なのかは位置によって決まる.結果として,n番目のオプション引数を指定するためには,n-1番目のオプション引数が指定されていなければならない.これに対してキーワード引数はタグとともに指定する.タグは関数定義におけるキーワード引数の名前と同じである.先の例をさらに変更してオプション引数をキーワード引数にしてみる.
> (defun disp-val4 (target &key prefix suffix) 
       (format t "~A=[~A]~A~%" prefix target suffix))
DISP-VAL4
> (disp-val4 1)
NIL=[1]NIL
NIL
> (disp-val4 1 :prefix 'val)
VAL=[1]NIL
NIL
> (disp-val4 1 :suffix ".")
NIL=[1].
NIL
> (disp-val4 1 :prefix 'val :suffix ".")
VAL=[1].
NIL
> (disp-val4 1 :suffix "." :prefix 'val)
VAL=[1].
NIL
このようにキーワード引数はタグとともに指定できるので,好きな順番で指定できる.また,上の例から,キーワード引数のデフォルト値は,指定しなければnilであることがわかる.キーワード引数のデフォルト値はオプション引数のそれと同様の方法で指定できる.
> (defun disp-val5 (target &key (prefix 'val) (suffix "."))
       (format t "~A=[~A]~A~%" prefix target suffix))
DISP-VAL5
> (disp-val5 1)
VAL=[1].
NIL
> (disp-val5 1 :prefix 'a)
A=[1].
NIL
> (disp-val5 1 :suffix '!)
VAL=[1]!
NIL
> (disp-val5 1 :prefix 'a :suffix '!)
A=[1]!
NIL
> (disp-val5 1 :suffix '! :prefix 'a) 
A=[1]!
NIL

レキシカル変数と関数とクロージャ

例えばletで作成した変数は,そのlet式内でのみその変数名による使用が可能であった.この変数はレキシカル変数といい,レキシカルコンテキスト(ここではlet式内)が使用されている限り参照可能である.この表現は,「レキシカル変数はそのレキシカルコンテキスト外でも参照可能である」ことを匂わせている.だが,レキシカル変数はレキシカルコンテキスト外ではその変数名による使用ができない.では,レキシカルコンテキスト外でレキシカル変数を参照するのはどういった場合か.例えばlet式がレキシカル変数を使用する関数を返す場合である.以下は,一つのレキシカル変数を共有して1増,1減する二つの関数を作成し,レキシカルコンテキスト外から呼び出している.
> (multiple-value-bind (one-up one-down)
         (let ((x 0))
           (values #'(lambda () (setf x (+ x 1)))
                   #'(lambda () (setf x (- x 1)))))
       (setf (symbol-function 'one-up) one-up)
       (setf (symbol-function 'one-down) one-down))
#<CLOSURE :LAMBDA NIL (SETF X (- X 1))>
> (one-up)
1
> (one-up)
2
> (one-down)
1
> (one-down)
0
> x 

*** - EVAL: variable X has no value
xはlet式内のレキシカル変数なのでトップレベルではxという変数名で参照することはできないが,そのレキシカルコンテキストで定義された関数one-upとone-downを通して参照可能である.上の結果から,レキシカル変数xはlet式外に処理が移っても存在していることがわかる.ところで,上の例のlet式内のlambda式で作成した関数は,そのlambda式外の変数xを参照している.二つのlambda式で作成した関数は,外部で定義した変数を参照しているのである.このように関数定義外部にありながらその関数から参照される変数をフリー変数と呼ぶ.xはlet式内をレキシカルコンテキストにもつレキシカル変数でもあるので,フリーレキシカル変数である.そして,フリーレキシカル変数を参照する関数はクロージャと呼ばれる.クロージャは関数とコンテキストを一つにした概念である.上の場合,one-upとone-downはクロージャである.(+ x 1)や(- x 1)といった処理のみならず,同一のフリーレキシカル変数xを参照していて,その変数のレキシカルコンテキストはlet式内である,といったコンテキストまでを情報にもつ.

動的スコープ

レキシカル変数やクロージャという概念が出てくると,動的スコープについても触れなければならない.Lispにおける変数は,より正確には大域変数か局所変数ではなく,レキシカル変数かスペシャル変数に区別される.レキシカル変数はレキシカルスコープをもち,スペシャル変数は動的スコープを持つ.
レキシカル変数
xというシンボルをもつレキシカル変数があるとき,その変数はそのスコープでxというシンボルによって参照される.
スペシャル変数
xというシンボルをもつスペシャル変数があるとき,その変数はspecial宣言されたシンボルxによって参照される.
局所変数はデフォルトではレキシカルスコープを持ち,大域変数は常に動的スコープを持ち,トップレベルでsetfした変数は暗黙的にスペシャル変数である.以下はレキシカル変数を関数定義で参照する例である.
> (let ((x 1))
       (let ((x 2))             
         (defun hoge () x)
         (format t "~A~%" (hoge)))
       (format t "~A~%" (hoge))
       (let ((x 3))
         (format t "~A~%" (hoge))))
2
2
2
NIL
> (hoge)
2
関数hogeのdefunで使用したxはレキシカル変数で,そのスコープは値を2に初期化するlet式内である.このletの外で関数hogeを呼んでも返り値は2,次のlet式(xを3で初期化しているlet式)で関数hogeを呼んでも返り値は2である.シンボルは同じxでも,hoge内のxはレキシカル変数であり,参照先はあくまでdefunした時点でのレキシカルコンテキスト内のxである.つまり関数を定義した時点でのコンテキスト内の変数を参照し,その関数を呼び出すコンテキストの変数は参照しない.このへんのことはクロージャの説明でも記述した.関数hogeでスペシャル変数を参照すると,逆に関数を呼び出すコンテキストでの変数を参照する.スペシャル変数を指定するには,宣言を行なう.
(declare (special 変数名))
declareはコンパイラへの局所宣言で(※),上の場合は変数名で参照される変数がspecialであると宣言している.declare式はdefun,lambda,let,doなど,変数を作成する式の本体部分の先頭に置くことができる.これを使用して関数hoge内のxをスペシャル変数にしてみる.
> (defparameter x 10)
X
> (let ((x 1))
       (let ((x 2))
         (defun hoge () (declare (special x)) x)
         (format t "~A~%" (hoge)))
       (format t "~A~%" (hoge))
       (let ((x 3))
         (format t "~A~%" (hoge))))
2
1
3
NIL
> (hoge)
10
> (defparameter x 20)         
X
> (hoge)
20
スペシャル変数は動的スコープをもつ.すなわち関数hogeを呼び出すコンテキスト内の変数xを参照するので,コンテキストによって関数hogeの返り値が変化する.スペシャル変数を参照する関数は,その関数を呼び出すコンテキストでの変数を参照するので,関数定義の時点では,そのシンボルをもつ変数が存在しなくてもよい.ただし,当然ながら呼び出すコンテキストでは存在しなければならない.
> (defun hoge ()
(declare (special x))
x)
HOGE
> (hoge)

*** - EVAL: variable X has no value
また,関数定義した時点で,関数定義内にある変数がコンテキスト内にない場合,その変数はスペシャル変数として扱われるようだ.
> (defun hoge () x)
HOGE
> (let ()
(defun fuga () x))
FUGA
> (hoge)

*** - EVAL: variable X has no value
> (fuga)

*** - EVAL: variable X has no value
> (let ((x 1))
(hoge))

*** - EVAL: variable X has no value
> (defparameter x 10)
X
> (hoge)
10
> (fuga)
10
> (let ((x 1))
(hoge))
1
上の例ではlet内でのhoge呼び出しを2回行なっているが,1回目では失敗している.その後トップレベルでdefparameterによってスペシャル変数xを作成している.そして2回目では局所変数を参照している.1回目のようにスペシャル変数xがない場合,たとえlet内で局所変数xを作成していてもエラーになるようだ.
※ 大域宣言にはdeclaimを使用する.

You may also like...