関数の拡張
ここでは、関数の合成で見た compose 関数や compose-Apply 関数の使い勝手を広げる拡張について考えます。
これらの関数を合成する関数は、どちらも二つの関数名を引数として受け取る形で定義されていました。いわゆる二項演算子の形式をとる関数です。そのため二つ以上の関数の合成は、合成を入れ子状に書きます。合成する関数が増えていくと、入れ子が深くなって読みにくく関連がつかみづらくなります。
(setq sysValue-off (compose-Apply (compose-Apply 'undo-group 'osmode-off ) 'blipmode-off ) )
ここでは、一度に二つの関数しか合成できなかったものを、リストの形式で合成される関数の一覧を一度に渡すことができないかを考えます。しかも、合成を行う関数を書き換えるのではなく再帰関数の形でラップすることで、以前のものには手を加えることなく拡張します。
compose 関数を使ってリストの引数をとる再帰関数を書くと次のようになります。
(defun compose-recurse (alist) (if (< 1 (length alist)) (compose (car alist) (compose-recurse (cdr alist))) (car alist) ) )
compose-Apply 関数を使ってリストの引数をとる再帰関数を書くと次のようになります。
(defun compose-Apply-recurse (alist) (if (< 1 (length alist)) (compose-Apply (car alist) (compose-Apply-recurse (cdr alist))) (car alist) ) )
上の二つを見比べると一目瞭然で、関数名以外は同じ構造の関数であることが見て取れるでしょう。共通の構造を感じたら、抽象化してまとめてしまいたくなるのが関数型言語の考え方です。
匿名の関数で自分自身を呼び出すには?
必要な関数名を渡すことで上の二つの関数をそれぞれ生成する関数が作れそうです。生成された関数は匿名の関数の形で返し、必要に応じて適当なシンボルに代入して使用できるようにします。ところがここで、再帰関数とするために匿名の関数内部で自分自身を呼び出す方法が無いことに気づきます。何しろ名前が無い関数です。
(if (< 1 (length alist))
(<関数 compose または compose-Apply> (car alist) (<自分自身を再帰呼び出し!> (cdr alist)))
(car alist)
)
)
しかし、ダイナミックスコープの仕組みと lambda 式を組み合わせる事で、実は匿名の関数で再帰関数を定義することが可能です。その関数は次のようになります。引数の関数名 afunc を評価して匿名の関数に埋め込むために lambda 式を表すリストを作成し、一番外側の eval 関数で具体的に返値となる匿名の関数を生成しています。
(defun recurse-binaryOperator (afunc) (eval (list 'lambda '(alist) (list '(lambda ($_recursiveFunction /) ($_recursiveFunction alist)) (list 'lambda '(listArg) (list 'if '(< 1 (length listArg)) (list (eval afunc) '(car listArg) '($_recursiveFunction (cdr listArg)) ) '(car listArg) ) ) ) ) ) )
リストの入れ子となっていて読みにくいので、普通の実行コードの形で書き下すと次のようなものになります。lambda 式が三つあります。
( (lambda ($_recursiveFunction /) ($_recursiveFunction alist))
(lambda (listArg /)
(if (< 1 (length listArg))
(<関数 compose または composeApply> (car listArg) ($_recursiveFunction (cdr listArg)))
(car listArg)
)
)
)
)
最も外側の lambda 式はリストの引数を受け付けるインターフェースです。引数の名前が alist となっているのを確認してください。この関数の内側で alist と出てきたら、ダイナミックスコープの仕組みからローカル変数で隠されていない限り引数の alist のことです。
具体的にその関数の中身を見ていきます。のこり二つの lambda 式の関係に注目します。
(lambda (listArg /)
(if (< 1 (length listArg))
(<関数 compose または composeApply> (car listArg) ($_recursiveFunction (cdr listArg)))
(car listArg)
)
)
)
二番目の lambda 式は一番目の lambda 式に対する引数となっているのが見て取れます。つまり二番目の lambda 式で生成される関数は一番目の lambda 式の引数 $_recursiveFunction という名前を与えられて alist を引数に一番目の lambda 式内で実行されることになります。さて、二番目の lambda 式が再帰関数を表していますが、自分自身を呼び出す問題はどうなっているかというと $_recursiveFunction という関数名で呼び出しています。二番目の lambda 式が一番目の lambda 式内で実行される限り、自分自身は $_recursiveFunction という名前で呼び出すことができるわけです。ここでもダイナミックスコープの仕組みが働いています。
使用例は次の通りです。再帰的に apply 関数を合成する composeApply-r 関数を作成し、それを使ってリストに連ねた関数を合成しています。
_$ (setq composeApply-r (recurse-binaryOperator 'compose-Apply))⏎
#<USUBR @000000003d4172f0 -lambda->
_$ (setq sysValue-off (composeApply-r '(undo-group osmode-off blipmode-off)))⏎
#<USUBR @00000000434075c0 -lambda->
パズルのようなトリックでした。通常のコードを書いているときにこのようなパズルを解きたくはありませんが、ひとたびこのような関数を作っておきますと、さまざまなケースで使用できます。今回の例の合成される関数は二項演算子であると始めに言いました。そして、他の二項演算子の関数でも recurse-binaryOperator 関数を通すことによってリストに対応した関数に拡張できるのです。
例えば次のようなベクトルの加算の関数を作ったとします。二つのベクトルを受け取る二項演算子の形式を取っています。
(defun Vector:Add (v1 v2) (mapcar '+ v1 v2))
通常の使い方は以下のようになるでしょう。
_$ (Vector:Add (Vector:Add '(1 1 1) '(2 2 2)) '(3 3 3))⏎
(6 6 6)
ここで先ほどの recurse-binaryOperator 関数を使って関数を拡張すると、リストにまとめられたベクトルを一度に処理する関数を生成することができます。
_$ (setq Vector:Add-r (recurse-binaryOperator 'Vector:Add))⏎
#<USUBR @00000000434077a0 -lambda->
_$ (Vector:Add-r '((1 1 1) (2 2 2) (3 3 3)))⏎
(6 6 6)
いちいち最初からそれ専用に再帰関数を書き下す必要は無くなりました。関数型言語は面白いものです。
reduce
再帰化した関数に名前をつけるほどではない場合もあるでしょう。その際は、再帰化した関数をそのままその場で実行する書き方もできます。
((recurse-binaryOperator 'Vector:Add) '((1 1 1) (2 2 2) (3 3 3)))
Common LISP には同様のことを行う関数 reduce があります。次のような関数定義で AutoLISP でも実現できます。
(defun reduce (func alist) ((recurse-binaryOperator func) alist) )
reduce 関数を使った場合は、次のように書けます。
(reduce 'Vector-Add '((1 1 1) (2 2 2) (3 3 3)))
再帰関数を生成する関数のもう一つの例
手続き型言語になれていて関数型言語のスタイルである再帰関数をいちいち書くのも抵抗があるという方でも、見てきたように再帰関数を自動的にプログラムで生成できるなら使ってもよいと感じられるのではないでしょうか。むしろ、もっと他のパターンに対応する再帰関数ジェネレータのようなものが作れるかもと思ったかもしれません。ここでは前の例をすこし改造して、副作用をもたらすのが目的の関数を再帰関数に変換する関数を紹介します。副作用を目的とする関数とは、画面表示や AutoCAD 図形の操作等が目的で、関数の戻り値を目的としていないケースです。先の例が理解できれば、特に新しいことはやっていません。
(defun recurse-effect (afunc) (eval (list 'lambda '(alist) (list '(lambda ($_recursiveFunction /) ($_recursiveFunction alist)) (list 'lambda '(listArg) (list 'if 'listArg (list 'progn (list (eval afunc) '(car listArg)) '($_recursiveFunction (cdr listArg)) ) ) ) ) ) ) )
次の関数は、文字列の引数をひとつとって【コマンドライン】に改行してから表示します。
(defun printline (string) (terpri)(prompt string))
これを文字列のリストを受け付けるように再帰関数へと拡張して実行します。
_$ (setq printline-r (recurse-effect 'printline))⏎
#<USUBR @000000003855f840 -lambda->
コマンドラインで実行してみます。
コマンド: (printline-r '("Pen" "Pinaple" "Apple" "Pen"))⏎
Pen
Pinaple
Apple
Pennil
経験的に AutoCAD プログラミングでは、最初に紹介した二項演算子を再帰化する関数とこの副作用関数を再帰化する関数でほとんどの再帰処理は書け、これ以外では mapcar 関数を使ったりして改めて再帰関数を書く機会はかなり減ります。