2010年8月26日木曜日

符号的プログラミングのすすめ on Common Lisp

初めに

Lispの括弧ネタ(注1)に触発されて思い浮かんだネタを書いていきます。

注1: @nitro_idiotさん http://e-arrows.sakura.ne.jp/2010/08/is-lisp-really-has-too-many-parenthesis.html

符号的プログラミングについて

Perlは非常にリッチな言語です。世界はPerlでかかれているらしいです。Perl最強ですね。

このPerl言語のエキスパートたちが、Perlの持つリッチな機能をフル活用するプログラミングスタイルを符号的プログラミングと呼びます。

Perlには及ばないかもしれませんが、私の大好きなCommon Lispも非常にリッチな言語なため、符号的プログラミングを行うことができます。

これから、一般的なスタイルのCommon Lispプログラムをいかにして符号的なスタイルに変換していくかを見ていきましょう。

一般的なスタイルのCommon Lispプログラム

元ネタに合わせて、階乗の値を順番に表示していくプログラムを書いてみます。

(defun fact-1 (to)
(labels ((inner (n acc)
(when (<= n to)
(format t "~A! = ~A~%" n acc)
(inner (1+ n) (* (1+ n) acc)))))
(inner 1 1)))

(fact-1 20)

関数fact-1の内部で、関数innerを定義し、再帰呼び出しを行っています。

この関数を徐々に符号的に改良していきます。

名前は付けない、使わない

fact-1を眺めて、まず気づく問題点は内部関数に名前を付けている点です。 Common Lispには無名関数を作るlambdaマクロ(注2)があるので、これを利用しましょう。

注2: 関数呼び出しの位置にlambdaフォームがくると特別扱いされるので、他のシンボルより特殊な存在だと思います。

(defun fact-2 (to)
((lambda (fn n acc)
(when (<= n to)
(format t "~A! = ~A~%" n acc)
(funcall fn fn (1+ n) (* (1+ n) acc))))
(lambda (fn n acc)
(when (<= n to)
(format t "~A! = ~A~%" n acc)
(funcall fn fn (1+ n) (* (1+ n) acc))))
1 1))

関数に名前を付けることは避けられましたが、今度はまったく同じ式を2回も記述しなければならないという問題が発生しました。関数(サブルーチン)にも言えますが、同じものはまとめてしまうのが普通でしょう。

しかし、このlambdaフォームをまとめるために名前を付けてしまっては本末転倒です。そこで、共有構造を利用することにしましょう。

(defun fact-3 (to)
(#1=(lambda (fn n acc)
(when (<= n to)
(format t "~A! = ~A~%" n acc)
(funcall fn fn (1+ n) (* (1+ n) acc))))
#1#
1 1))

これで見やすくなりました。

せっかくなので、defunもlambdaに直してみましょう。

(set 'fact-4
(lambda (to)
(#1=(lambda (fn n acc)
(when (<= n to)
(format t "~A! = ~A~%" n acc)
(funcall fn fn (1+ n) (* (1+ n) acc))))
#1#
1 1)))

setはマクロではなく関数です。第1引数のシンボルに、第2引数引数の値をセットします。 setは関数スロットではなく普通の値を格納するスロットに第2引数をセットするため、関数呼び出しにはfuncallが必要となります。

> (funcall fact-4 20)

さて、本題に戻ります。内部関数の名前は消え去りましたが、この関数にはまだまだ名前がたくさん残っています。

まずは関数名から消していきましょう。関数は単純に他のシンボルにセットすればいいだけなので簡単です。

(setf (symbol-function '@) #'funcall)
(setf (symbol-function '~) #'format)
(set 'fact-5
(lambda (to)
(#1=(lambda (fn n acc)
(when (<= n to)
(~ t "~A! = ~A~%" n acc)
(@ fn fn (1+ n) (* (1+ n) acc))))
#1#
1 1)))

(@ fact-5 20)

次は変数名を消していきましょう。先ほどから何度か話題に出ていましたが、 Common Lispのシンボルには値をセットするスロットが複数あるので、関数と通常の変数で同じシンボルを別々の意味で利用できます。

(defparameter & t)
(set 'fact-6
(lambda (>)
(#1=(lambda (@ - *)
(when (<= - >)
(~ & "~A! = ~A~%" - *)
(@ @ @ (1+ -) (* (1+ -) *))))
#1#
1 1)))

whenはマクロなので、setfで設定できません(多分)

なので、他のマクロでラップしてしまいましょう。

(defmacro  ? (&rest args)
`(when ,@args))
(set 'fact-7
(lambda (>)
(#1=(lambda (@ - *)
(? (<= - >)
(~ & "~A! = ~A~%" - *)
(@ @ @ (1+ -) (* (1+ -) *))))
#1#
1 1)))

lambdaと括弧を符号的にする

lambdaも同じように対処できるかと思いきや、「注2」に書いたように、関数呼び出し位置に現れるlambdaフォームは特別扱いされるため、うまくいきません。

;; 例
((lambda (x y) (list x y)) 2 3)
-> (2 3)
(defmacro my-lambda (&rest args)
`(lambda ,@args))
(macroexpand-1 '(my-lambda (x y) (list x y)))
->(LAMBDA (X Y) (LIST X Y))
((my-lambda (x y) (list x y)) 2 3)





この問題を解決するためには、自分の定義したシンボルが読み込みんむタイミングでlambdaに変化してくれれば良さそうです。

Common Lispでは、実行時、コンパイル時(≒マクロ)の他に、読み込み時の動作を定義するリーダマクロが存在します。

リーダマクロを利用すれば、問題を解決できるに違いありません。

(set-macro-character
#\^
#'(lambda (stream char)
(declare (ignore char))
`(lambda ,@(read-delimited-list #\) stream t))))
(set 'fact-8
^ (>)
(#1=^ (@ - *)
(? (<= - >)
(~ & "~A! = ~A~%" - *)
(@ @ @ (1+ -) (* (1+ -) *))))
#1#
1 1)))

ここまで書くとふと思います。この括弧の群れは符号的ではないのではないか、と。

消し去ってやりましょう。

(set-macro-character
#\!
#'(lambda (stream char)
(declare (ignore char))
(read-delimited-list #\$ stream t)))
(set-macro-character
#\^
#'(lambda (stream char)
(declare (ignore char))
`(lambda ,@(read-delimited-list #\$ stream t))))

! set 'fact-9
^ ! > $
! #1= ^ ! @ - * $
! ? ! <= - > $
! ~ & "~A! = ~A~%" - * $
! @ @ @ ! 1+ - $ ! * ! 1+ - $ * $ $ $ $
#1#
1 1 $ $ $

ここまでくれば後一歩です。最後に残った関数名、fact-nを取り去り、直接引数を与えて呼び出してみましょう。

! @ ^ ! > $
! #1= ^ ! @ - * $
! ? ! <= - > $
! ~ & "~A! = ~A~%" - * $
! @ @ @ ! 1+ - $ ! * ! 1+ - $ * $ $ $ $
#1#
1 1 $ $
20 $

終わりに

最初と最後のプログラムを比べると、もはや別のプログラミング言語ではないかと思えてしまいます。しかし、これはどちらも同じCommon Lispプログラムなのです。

符号的プログラミングは、一般的なプログラミングスタイルとはかけ離れているように見え、一部のエキスパートにしか駆使することの出来ない黒魔術かのような錯覚を覚えますが、一つ一つの要素を抜き出して考えれば、私たちが日頃書いているごく普通のプログラムとかわりはありません。

Perlは非常に高機能ですが敷居が高いのが難点です。その点、Common Lispは一般的なスタイルのプログラムも非常に書きやすいので、符号的プログラミング入門者にもおすすめです。

Perlを極めた人も、これからプログラミングを始める人も、ぜひ一度Common Lispで遊んでみてください。

0 件のコメント:

コメントを投稿