2010年4月14日水曜日

dlambda

R5RSの衛生的マクロって使い辛い、使い辛い、使い辛い……。

って文句ばっか言っててもしゃーないんで、逆の例をお見せしましょう。これは衛生的マクロだったら書きやすいんですけど、CLの伝統的マクロなら書きづらい、と言う好例だと思います。そしてあんまSchemeマクロの例示でも見ないんですよね。多分本邦初公開(笑)。

LET OVER LAMBDA Edition 1.0にdlambdaと言うマクロが紹介されています。dはdestructuring(分配)のdだそうです。コードは以下のようなものです。

(defmacro! dlambda (&body ds)
`(lambda (&rest ,g!args)
(case (car ,g!args)
,@(mapcar
(lambda (d)
`(,(if (eq t (car d))
t
(list (car d)))
(apply (lambda ,@(cdr d))
,(if (eq t (car d))
g!args
`(cdr ,g!args)))))
ds))))

筒井康隆風に言うと

俺は「ひゃあ」と叫んで椅子から5センチ程飛び上がった。

ってカンジでしょう(笑)。

まず注釈しておきますが、defmacro!自体がLET OVER LAMBDA Edition 1.0依存のマクロなんです。そしてdefmacro!はonce-only問題を解決するレイヤーだけでなく、その他の関数やマクロの上に成り立っています。まさしくOn Lispなんですが……。
レイヤー組み立てる側にはいいんですが、逆方向にレイヤー降っていこうとすればシャレになりませんね(笑)。defmacro!を組み立てる為の全部の手順なんてここで紹介しねえぞ(笑)。んな事やってられっか、っての(笑)。
ぶっちゃけ、一度ライブラリ化狙ってasdfにしちまおうか、とか思ったんですが、asdf作るのもメンド臭くってやってられっか、とか思いました。平たく言うと失敗したんです(爆)。わけわかんねーぞ、asdf(怒)。こんちくしょーめ。

上のワケワカメなマクロには目立った特徴が二つあります。それらは

  • defmacro!を用いている以上、once-only問題を解決したいのが第一義である。

  • 引数の分配のメカニズムがメンド臭い。


の二点です。
once-only問題を説明するのはシンドイんですが……。要するに、dlambdaに与えた引数によっては二重に評価されて計算結果がおかしくなる可能性がある、と言う事です。これを避ける為にdefmacro!を使わざるを得ないんですけど……。
ここは取りあえずピンと来なくて構いません。しかし、要するに言い換えると、once-only問題が浮上してくるのは、CLのdefmacroが衛生的ではないから、です。つまり、何だかんだ言って一つ目の問題はSchemeの衛生的マクロなら気にせんで構わない、と言う事です。
二番目に関しては、引数分配に高階関数であるmapcarを使ってて、これはこれで強力なテクニックなんですが、一見何をしてるのか分かりません。分からないでしょ(笑)?単純に言うと、case式にはめ込むキーと、そこに列挙する節を上手い具合に分断する為にmapcarで操作してるんです。
ちょっと慣れたら読めるんですが、実際問題、個人で意図してmapcarによる大技繰り出すのは構わないんですが、他人が書いたコードだと一発では分かんないっすね(苦笑)。意図してるところが見え辛い。
加えると、mapcarが出る時って大体計算結果のリストが欲しいわけです。と言う事はmapcar以降は部分評価になる。と言う事はバッククオート解除にコンマやコンマアットが十中八九出てくる。って事はネストしたバッククオートが出てくる……。まあ、今は意味が分からなくて結構なんですけど、要するに読みづらくなるのは確定だって事です。泣きたくなるだろ?泣きたいんだよ、こっちはよ。

とっころがね~~。上のdlambdaと同等のコードはSchemeの衛生的マクロだったらアッサリ書けるんですよ。直球勝負ですね。いや、不思議。何でこれをSchemeの代表的マクロとして紹介せんのだ、って程アッサリ仕上がります。しかも意味はCLに比べると明確です。

(define-syntax dlambda
(syntax-rules (else)
((_ (key d0 body0 ...) ... (else d1 body1 ...)) ;基本的な変換式
(lambda arg
(case (or (null? arg) (car arg))
((key) (apply (lambda d0
body0 ...) (cdr arg)))
...
(else (apply (lambda d1
body1 ...) arg)))))
((_ (key d body ...) ...) ;再帰的定義で else 節の扱いを変える
(dlambda (key d body ...) ... (else () #f)))))

CL13行に対してScheme12行です。もっとも、行数比較には意味が無いんですけど、先ほど指摘した通り、defmacro!の背後には恐ろしい程のコード量がある。圧縮率から言うとSchemeの衛生的マクロの圧勝でしょう。こちらはR5RS仕様書の範囲内で書きあがるのです。

まあ、百聞は一見に如かず。一回LET OVER LAMBDA Edition 1.0の例示に従って動かしてみましょうか。その後、衛生的マクロでどうしてこんなに簡単にdlambdaを書けるのか、考えてみます。

> (define count-test
(let ((count 0))
(dlambda
(inc () (set! count (+ count 1)) count)
(dec () (set! count (- count 1)) count))))
> (count-test 'inc)
1
> (count-test 'dec)
0
>

dlambdaは引数にキーワードとそこに渡したい引数、そして手続き定義を取ります。上の例で言うとキーワードが例えばinc、incは無引数で、そして手続き定義が(set! count (+ count 1))とcountです。その形式の引数を複数取ってます。
そして、count-testに引数としてキーワードシンボルを与えると、それに準じた手続きが実行されるわけです。局所変数countの初期値は0だったので、'incを引数で与えるとcountは1に書き換えられ、もう一度引数を'decで呼び出すと0が返される。
もっと複雑な事も出来ますね。再びLET OVER LAMBDA Edition 1.0の例示に従ってみます。

> (define count-test
(let ((count 0))
(dlambda
(reset () (set! count 0) count)
(inc (n) (set! count (+ count n)) count)
(dec (n) (set! count (- count n)) count)
(bound (lo hi)
(set! count
(min hi
(max lo
count)))
count))))
> (count-test 'reset)
0
> (count-test 'inc 100)
100
> (count-test 'bound -10 10)
10
> (define dlambda-test
(dlambda
(something-special ()
(display "SPECIAL") (newline))
(else args
(for-each display (list "DEFAULT: " args)) (newline))))
> (dlambda-test 1 2 3)
DEFAULT: (1 2 3)
> (dlambda-test)
DEFAULT: ()
> (dlambda-test 'something-special)
SPECIAL
>

さて、Schemeの衛生的マクロだとdlambdaが何故書きやすいのか?乱暴に言うと、原始的なパターンマッチ構文であるcaseがパターンを指定する書き方である衛生的マクロと相性が良いと言う事に他ならない、と言う事だと思います。
Schemeではcase式は嫌われているのか、あまり目にする事が無いんですが、R5RSにキチンと定義されている組み込み構文です。

(case <キー> <節1> <節2> ... ) ライブラリ構文
構文: <キー> はどんな式でもよい。各<節> は次の形式をとること。

    ((<データ1> ... ) <式1> <式2> ... ),

ここで各<データ> は,なんらかのオブジェクトの外部表現である。<データ> はすべて異なっていなければならない。最後の<節> は“else 節” でもよい。これは次の形式をとる。

    (else <式1> <式2> ... ).

意味: case 式は次のように評価される。<キー> が評価され,その結果が各<データ> と比較される。もし<キー> を評価した結果が,ある<データ> と(eqv? の意味で) 等価ならば,対応する<節> の各式が左から右へと評価され,そしてその<節> の最後の式の(1個または複数個の) 結果がcase 式の(1個または複数個の) 結果として返される。もし<キー> を評価した結果がどの<データ> とも異なるとき,else 節があればその各式が評価されてその最後の(1個または複数個の) 結果がcase 式の(1個または複数個の) 結果になるが,なければcase 式の結果は未規定である。

(case (* 2 3)
((2 3 5 7) 'prime)
((1 4 6 8 9) 'composite)) => composite
(case (car '(c d))
((a) 'a)
((b) 'b)) => 未規定
(case (car '(c d))
((a e i o u) 'vowel)
((w y) 'semivowel)
(else 'consonant)) => consonant


つまり、caseが要求するパターンにパターンとして記述した要素を当てはめれば一丁上がり、と言う事です。
加えて、通常Common Lispでは&bodyでレストパラメータとして式本体をリストにしちゃうせいで分配がメンド臭かったりするわけですが、基本Schemeの衛生的マクロでは本体を「要素のパターン」として記述します。つまり、分配自体が必要がなく、そのパターン自体さえ適切に記述出来れば置換自体は衛生的マクロがすべて面倒を見てくれるわけです。
衛生的マクロ版dlambdaの変換の基本的アイディアは次の通りです。

(dlambda (key d body ...) ...) ;; このパターンを
||
変換
||
\ /
\/
(lambda arg ;; こう変換する
(case (car arg)
((key) (apply (lambda d
body ...) (cdr arg)))
...))

マクロdlambdaはクロージャを返せば良いので、(lambda arg ..)で書き始めます。引数argは可変長引数なんで括弧は要りません。そして、dlambdaの記述パターンにargはありませんが、これはリストの第一要素にdlambdaを使った式が来れば後続する要素が実引数として処理されるんでこれで良いのです。λ式の性質ですよね。
そして、argで外部から与えられる実引数の第一要素は<キー>になります。以降の要素は処理されるべきものとしてリストにまとめられています。従って、case内の

(apply (lambda d body ...) (cdr arg))

が生きてくる。

基本的にはこれだけ、なんです。考え方としてはCL版のdlambdaより簡単です。CLのLegacy Macroだと&bodyの分配に頭を悩ますハメになるんで、記述コストは高く付くんじゃないか、と思います。

あとは、<キー>に対応する<節>が無かった場合はどうするか?要するにデフォルト挙動をどうするのか、だけ考えれば良い。もうちょっと具体的に言うと、caseはelse節を取れるんで、そこをどうするか、だけ考えれば良いのです。
そこで、dlambdaのパターンをelseと言うキーワードを用いて次のように変更します。

(dlambda (key d0 body0 ...) ... (else d1 body1 ...))

つまり、dlambdaは必ずelse節を持たなきゃならないと仮定する。
elseが記述された場合、もはやargの第一要素は<節>の<データ>を意味しません。従って、

(else (apply (lambda d body ...) arg))

がelse節になります。
次にデフォルトの挙動が無引数の場合にどうなるか、です。つまりargが空リストだった場合。argが空リストの場合、CLと違ってSchemeは(car arg)だとエラーを返します。これを何とかしないとならないんですが、ここはcaseの次の性質により回避は簡単です。

<キー> はどんな式でもよい。

従って、

(case (or (null? arg) (car arg)) ...)

で書いて構わないのです。返り値が#tだろうと(car arg)だろうと結局お構いなし、ですね。と言うか、#tが返された途端にelse節が実行されます。
つまり、ここまででelseと必須として、次のような変換パターンで95%は完成するわけです。

(dlambda (key d0 body0 ...) ... (else d1 body1 ...))
||
変換
||
\ /
\/
(lambda arg
(case (or (null? arg) (car arg))
((key) (apply (lambda d
body ...) (cdr arg)))
...
(else (apply (lambda d
body ...)))))

あとは衛生的マクロでの再帰的定義を用いてelseを記述しないパターンを定義すれば良いわけです。else節が#fを返すようにして潰しちゃいます。

(dlambda (key d body ...) ...)
||
変換
||
\ /
\/
(dlambda (key d body ...) ... (else () #f))

これで完成ですね。

2010年4月12日月曜日

alet

LET OVER LAMBDA Edition 1.0ではaletと言うアナフォリックマクロも紹介されています。定義は以下の通り。

CL-USER> (defmacro alet (letargs &body body)
`(let ((this) ,@letargs)
(setq this ,@(last body))
,@(butlast body)
(lambda (&rest params)
(apply this params))))
ALET
CL-USER>

aletの動作例はalambdaと組み合わされたなかなか複雑なものが紹介されています。

CL-USER> (alet ((acc 0))
(alambda (n)
(if (eq n 'invert)
(setq this
(lambda (n)
(if (eq n 'invert)
(setq this #'self)
(decf acc n))))
(incf acc n))))
#<CLOSURE (LAMBDA (&REST PARAMS)) {B40D5D5}>
CL-USER>

これはなかなか凄い例です(笑)。aletで指示する代名詞thisの中身をsetqで置き換えてる。
実行例は次の通りです。

CL-USER> (setf (symbol-function 'alet-test) *)
#<CLOSURE (LAMBDA (&REST PARAMS)) {BC6900D}>
CL-USER> (alet-test 10)
10
CL-USER>

シンボルinvertを渡すとalet-testの挙動が変わります。

CL-USER> (alet-test 'invert)
#<CLOSURE (LAMBDA (N)) {B02F49D}>
CL-USER> (alet-test 3)
7
CL-USER>

つまり、alet内のthisの中身が置き換えられています。

CL-USER> (alet-test 'invert)
#<CLOSURE (LABELS SELF) {BC6D605}>
CL-USER> (alet-test 5)
12
CL-USER>

面白いですね。代名詞を使って丸ごと関数の挙動を変えるとは……。

では、R5RS Schemeで同様のマクロを記述するにはどうするか?
前回見た通り、衛生的マクロではアナフォリックマクロは直接書けません。従って、ここでも外部から束縛すべきシンボルを与えるPseudoなアナフォリックマクロに改造してみます。

(define-syntax alet
(syntax-rules ()
((_ this ((letarg letvar) ...) body ... last)
(let ((this #f) (letarg letvar) ...)
(set! this last)
body ...
(lambda params
(apply this params))))))

割に個人的な感想では、LET OVER LAMBDA Edition 1.0ではCLのletらしい乱暴な使い方をしています。
CLの場合、letは束縛されるべきデータ側を省略しても問題ないようにデザインされています。この場合thisがそうなんですけど、Schemeではそれは許されません。従って、仮に与えるデータとして#fを束縛しておきます。
もう一つはSchemeヴァージョンのlambda式の引数が括弧無しの場合、与えられた引数はリストになります。
ではSchemeインタプリタで動作を確認してみます。

> (define alet-test (alet this ((acc 0))
(alambda self (n)
(cond ((eq? n 'invert)
(set! this
(lambda (n)
(cond ((eq? n 'invert)
(set! this self))
(else
(set! acc (- acc n))
acc)))))
(else
(set! acc (+ acc n))
acc)))))
> (alet-test 10)
10
> (alet-test 'invert)
> (alet-test 3)
7
> (alet-test 'invert)
> (alet-test 5)
12
>

上手く動いていますね。

alambda

R5RS Schemeの衛生的マクロでアナフォリックマクロは書けません。
色んな意見があるとは思いますが、アナフォリックマクロが書けない時点でSchemeの衛生的マクロは設計としては失敗してる、と思います。
実際、R6RSの衛生的マクロ、syntax-caseでは書けるようになっている、と言う話なんですが、相当ややこしくなってる、との事です。またsyntax-caseに関してのまとまった文献なり入門、ってのも見たことないです(ビューティフルコードには載ってるっぽいんですが、どっちかと言うと論文っぽいんで、実用的にどうする、って話を読み取るのは難しいでしょう)。従って今のところ、syntax-caseは使いようがない。

ポール・グレアムのOn Lispにalambdaと呼ばれるアナフォリックマクロが紹介されています。これはLET OVER LAMBDA Edition 1.0でも使われまくってるアナフォリックマクロの代表格です。定義は以下のようなものです。

CL-USER> (defmacro alambda (parms &body body)
`(labels ((self ,parms ,@body))
#'self))
ALAMBDA
CL-USER>

同書には次のような例が掲載されています。

CL-USER> (alambda (x) (if (zerop x) 1 (* x (self (1- x)))))
#<FUNCTION (LABELS SELF) {BB55A75}>
CL-USER>

ご覧の通り、alambdaは代名詞selfで束縛されたクロージャを返す。上のワンライナーは階乗の定義なんで、階乗を計算するクロージャですね。
続けてインタプリタに次のように入力します。

CL-USER> (funcall * 10)
3628800
CL-USER>

10の階乗がキチンと返ってきます。ちなみに、上の*は「掛け算」の意味ではなくって、CLのインタプリタ上では、その前の計算結果を参照する場合、*で参照出来ます。つまり、関数としての役割ではなくって、大域変数なんです。詳しくはこちらをご覧下さい。*を掛け算と解釈すると、上の入力は意味不明になります。
話を元に戻すと、最初の定義、

CL-USER> (defmacro alambda (parms &body body)
`(labels ((self ,parms ,@body))
#'self))
ALAMBDA
CL-USER>

に於いて、一見不用意にselfと言う変数名が挿入されているように見えます。これはマクロが展開される場所の外側からselfが指示されていたらそれを参照してもおかしくないんです。と言うか、前の例示の通り、明らかに参照している。
そのお陰で代名詞として役割を果たしてるんですね。上手い手です。そしてこれはSchemeで実現するのが難しい。と言うよりR5RSでは出来ません。

一見、R5RSマクロで次のような定義方法に翻訳出来て、それで良さそうに見えます。

;; この定義方法じゃ実はダメ。
(define-syntax alambda
(syntax-rules ()
((_ params body ...)
(letrec ((self
(lambda params
body ...)))
self))))

Schemeインタプリタで次のように走らせてみても、一見上手く動きそうに見えるのです。

> (alambda (n)
(if (zero? n)
'()
(cons
n
(self (- n 1)))))
#<procedure:self>
>

確かに手続きのクロージャselfが返ってるように見える。
ところが、これに実値を与えてみると、破綻している事が分かる。

> ((alambda (n)
(if (zero? n)
'()
(cons
n
(self (- n 1))))) 10)
reference to undefined identifier: self

=== context ===
/usr/lib/plt/collects/scheme/private/misc.ss:74:7

>

つまり、alambdaを用いたクロージャの定義で現れるselfは、alambda自体の定義で使われているselfとは別物だ、と認識されるのです。CLでのalambdaでは同一と見なされるものがR5RSでは同一と見做されない。変数衝突が起きない。これが衛生的と言われる所以です。

R5RSマクロは「letが変数束縛をする」ということを知っており、マクロが挿入する束縛変数とマクロの「外から来た」変数とを区別するのです。

-----<中略>-----

Schemeは、レキシカルクロージャを導入したのと同じ発想を、一段メタな領域に適用しようとしています。そこではもはやプログラムはS式そのものではありません。一度読み込まれて構文解析されたプログラムは「S式+環境」となり、マクロは「S式+環境」に対して作用します。


つまり、Schemeで翻訳を試みたalambdaの定義で使われているselfはクロージャによって守られていて、外側から中を参照する事が出来ません。コンテクストが外部定義からは切り離されているのです。故にアナフォリックマクロが定義出来ない。


Schemeではむしろ、束縛する変数を呼び出し環境から与えてやる書き方が推奨されます。


プログラミングGaucheの流儀に従うと、Schemeでのalambdaは次のように定義した方が無難だと言うことです。


(define-syntax alambda
(syntax-rules ()
((_ self params body ...)
(letrec ((self
(lambda params
body ...)))
self))))

つまり、外部からselfを与えてやるようにする。そうすると、

> (alambda self (n)
(if (zero? n)
'()
(cons
n
(self (- n 1)))))
#<procedure:self>
>

とクロージャselfが返るのは同じですが、引数を与えてやると、

> ((alambda self (n)
(if (zero? n)
'()
(cons
n
(self (- n 1))))) 10)
(10 9 8 7 6 5 4 3 2 1)
>

となります。
しかし、こうなると実際、アナフォリックマクロと言うよりクロージャを返すnamed-letの変種ですね。

こんな感じで、CLのマクロをSchemeに移植しようとすると、色んな障害が立ちはだかります。また、外部から変数を与えてPseudoなアナフォリックを狙っても、CLと違い、手続き定義に於いては味方の筈のクロージャが敵にまわり、同じ効果を出すには大きな障害として立ちふさがるのです。

2010年4月11日日曜日

コーディング・スタイル

ちょっとしたツマラン話を。

On Lispに次のようなコードが紹介されてるんですよね。

CL-USER> (defun group (source n)
(if (zerop n) (error "zero length")) ;ここで暗黙のprognを利用している
(labels ((rec (source acc)
(let ((rest (nthcdr n source)))
(if (consp rest)
(rec rest (cons
(subseq source 0 n)
acc))
(nreverse
(cons source acc))))))
(if source (rec source nil) nil)))
GROUP
CL-USER> (group '(a b c d e f g) 2)
((A B) (C D) (E F) (G))

うん、まあ問題がない。当然動きます。
しかし、Scheme勉強している人は二行目が気にくわないのでは、と思います。

マクロdefunは暗黙のprognが含まれていて、defunの内部に記述されたS式は逐次実行され、最後のS式の評価値が返り値として返されます。それが故に2行目に

(if (zerop n) (error "zero length"))

なんて書き方が可能なんですよね。ここで(zerop n) => NILだとしても、この返り値はここでは無視されて、次のlabelsからの部分が実行されて関数groupは問題なく動くのです。

この暗黙のprognを利用した関数定義って良く見かけるんですよ。CLのコードやあるいはEmacs Lispのコードでは良くあります。Scheme弄ってる時間が長いと「何々なに?」とか思っちゃうんですが(笑)。と言うのも、論理的な話すると、

CL-USER> (defun group (source n)
(if (zerop n)
(error "zero length") ;論理的にはこれで良い
(labels ((rec (source acc)
(let ((rest (nthcdr n source)))
(if (consp rest)
(rec rest (cons
(subseq source 0 n)
acc))
(nreverse
(cons source acc))))))
(if source (rec source nil) nil))))
STYLE-WARNING: redefining GROUP in DEFUN
GROUP
CL-USER> (group '(a b c d e f g) 2)
((A B) (C D) (E F) (G))

上のように書いてもいっこうに構わないのです。ここではlabels以降は(zerop n) => Tの場合処理されないので、結局局所関数は作成されません。そして(zerop n) => NILの場合はじめて再帰関数が生成されるので、ifの第1引数、第2引数はほっぽったままです。多分こう言う書き方の方がSchemerの好みであって、また、Schemeではifの第3引数が省略された場合の返り値が未定義な以上、こっちの書き方の方が望ましいでしょうね。

第1の書き方はCLやEmacs Lispのコードだと本当に良く見かけます。恐らく、C言語でああ言う書き方で脱出試みる場合があるんで、その影響もあるんでしょうね。が、Schemerはそう言うスタイルは好まないとは思います。
あるいは、ifに全てを包めば、インデントが凄く深くなるのが嫌われている原因かもしれません。Schemerの方がCLerやEmacs Lisperほどインデントが深くなるのをさほど問題視していないのかもしれません。

ちなみに、あくまで論理的な話をすると、こう言う書き方の方がなおスッキリするやもしれません。

CL-USER> (defun group (source n)
(labels ((rec (source acc)
(let ((rest (nthcdr n source)))
(if (consp rest)
(rec rest (cons
(subseq source 0 n)
acc))
(nreverse
(cons source acc))))))
(cond ((zerop n) (error "zero length"))
(source (rec source nil))
(t nil))))
STYLE-WARNING: redefining GROUP in DEFUN
GROUP
CL-USER> (group '(a b c d e f g) 2)
((A B) (C D) (E F) (G))

このように、実行形態を最後にまとめちゃっても、結果は同じですよね。
ただ、このスタイルの場合、最初に局所関数を作っちゃうでしょうから、その辺をCLerの人たちは「メモリの無駄」ってんで嫌ってるのかもしれません。

2010年4月7日水曜日

竹内関数と遅延評価

竹内関数ってご存知でしょうか?
ベンチマークテストに使われる関数なんだそうですが、シンプルな外見に似合わず、計算に無茶苦茶時間がかかる関数だそうです。
Schemeでの定義は以下の通りです。

(define (tak x y z)
(if (<= x y)
y
(tak (tak (- x 1) y z)
(tak (- y 1) z x)
(tak (- z 1) x y))))

環境にもよるんですが、あまり大きな実引数を与えると計算がなかなか終わりません。PLT Scheme依存のtime手続きで時間を計ってみます。なお、CPUはCore2DuoのT7500の2.2GHz、メモリは2GBです。

> (time (tak 10 5 0))
cpu time: 20 real time: 21 gc time: 0
10
> (time (tak 12 6 0))
cpu time: 388 real time: 388 gc time: 0
12
> (time (tak 18 12 6))
cpu time: 376 real time: 379 gc time: 0
18
>

まあ、実際各自マシンで試してみてください。「意外と計算に時間がかかる」と思うでしょう。

ところで、風の噂で聞くには、この竹内関数、Haskellと言うプログラミング言語ではちょっぱやで計算終了しちゃうそうです。へえへえへえ。どうやら、Haskellの伝家の宝刀、遅延評価は竹内関数の天敵らしい。

遅延評価、と言えばSchemeも遅延評価を備えています。しかし今まで使い勝手が良く分からなかったんでマトモに触った事がありませんでした。
曰く、

評価しなければならない値が存在するとき、実際の計算を値が必要になるまで行わないことをいう。

………。
な~んか、日常生活的な感覚から言うとあまりに当たり前のような気もしますがねえ……。あまりにも当たり前の事が出来ないのが前提なら、コンピュータってメンド臭えな。マジな話、実感としてはそんな感じですよ。プログラミング言語の世界って日常生活的には当たり前の事を大げさに言ってる、ってたまに感じます。

脱線しますが、例えば、

彼女になりそうな女性が存在するとき、彼女が必要になるまで口説かない。

とか言われると当たり前でしょ(笑)?彼女がいるのに別の女性を口説いたりしたらそれは人非人と後ろ指さされる世の中です(笑)。僕がプライベートでどう言う行動を取ろうが関係なく、まあ、そうですよね(笑)。プログラミング言語で「遅延評価」とか言ってますが、人間の行動なんて多かれ少なかれ遅延評価、です。別に特別な話じゃない。
実際問題、コンピュータの世界では人間の「常識」がいまだ通用しない世界なんですね。前にも書きましたが、プログラミング言語に於ける「抽象性」と言うのは、人間の常識に近づいてる事だ、と言うのはこう言う事なんです。竹内関数がコンピュータでクソみたいに計算に時間がかかるのは、女と見れば見境なしに手を出してるロクデナシみたいな事をプログラミング言語が行っているから、です。そう言う評価形式を先行評価と呼ぶらしい。そして竹内関数はそう言う女ったらしに鉄槌を下す。
いずれにせよ、我々の世界では、遅延評価が当たり前で先行評価が「異常」です。しかし、プログラミング言語の世界では、まだまだ「我々の行動原理で言う異常」が当たり前ならしい。そしてプログラマはその世界にどっぷりと浸かってなれざるを得ない。

そんな中でHaskellとSchemeは「ちょっとだけ」僕等の常識に近いトコにいるんですよね。Haskellは遅延評価がデフォですが、Schemeではちょっと工夫しないとならない。いずれにせよ、早いトコ、もっと人間側の常識に従っているプログラミング言語が普及して欲しいものです。

さて、Schemeでのその「工夫」ですけど。マクロを使って次のようなifの亜種を作ります。名づけてlif(lazy-ifの意)です。

(define-syntax lif
(syntax-rules ()
((_ _cond _then _else)
(force (if (delay _cond)
(delay _then)
(delay _else))))))

ここでdelayはS式の評価を遅延させるための特殊形式です(出来たものをプロミスと呼びます)。そしてforceはプロミスを強制評価する為の手続きです。
このforceとdelayを同じマクロ内に書いても意味が無い、と思われるかもしれませんが、さにあらず。特殊形式ifは述語判定が行われないと第二引数ないしは第三引数が評価されません。しかしながら条件節自体がプロミスなので、評価ははじまりません。故にifが形作るS式は全体として評価が遅延されている。
また、マクロ自体もマクロ展開が行われないと評価が開始されません。そしてdelayのせいでどう言う展開形に落ち着くのか、はこのlifはまだ知らないのです。従って、forceも発動しようがないのです。
このマクロlifを使えば、ltak(lazy-takeuchi)手続きは次のようにオリジナルのtak手続きと殆ど同様に定義可能です。

(define (ltak x y z)
(lif (<= x y)
y
(tak (tak (- x 1) y z)
(tak (- y 1) z x)
(tak (- z 1) x y))))

実際、インタプリタで計測してみましょう。恐ろしい程の竹内関数のベンチマークを叩き出します。

> (time (ltak 10 5 0))
cpu time: 0 real time: 0 gc time: 0
5
> (time (ltak 12 6 0))
cpu time: 0 real time: 0 gc time: 0
6
> (time (ltak 18 12 6))
cpu time: 0 real time: 0 gc time: 0
12
>

全く計算時間がかかってない事が分かるでしょう。すげえ、ブラボー!!!Viva!遅延評価(笑)!!!
それどころか、マトモな竹内関数だったらいつ計算が終わるか分からない次のような大きな引数を与えても難なくltakは結果を返してくれます。

> (time (ltak 100 50 0))
cpu time: 0 real time: 0 gc time: 0
50
>

すっごいですね~~。一瞬で答えを返してくれます。これも「値が必要になるまで評価しない」遅延評価版の竹内関数ならでは、の仕事です。オリジナルはよっぽど無駄な計算をしてるんでしょうね。こちらは不必要な計算は全く行わないので、迅速に計算結果が返ってくるのです。

面白いですね、遅延評価。また何かあったら使ってみたいな、と思います。

2010年4月3日土曜日

なぜマクロの使いどころは分かり辛いのか

9LISPでもマクロに入るらしいんで、ちょこっとしたメモを書いておこうと思います。何故マクロの学習が難しいのか、と言う一つの答えですね。

LISP入門と言う超古い本に紹介されてるんですが、大昔のLISP(それこそMacLispやInterlispより歴史が古いLisp1.5の流れ)だと関数定義ってもっと種類があったんです。下にそれを抜書してみます。







関数の種類




実引数を評価する
(eval)

実引数はそのまま
(quote)

実引数は仮引数にそのまま渡す
(spread)

EXPR型
(ARRAY)

NEXPR型

実引数はひとまとめ
(non-spread)

LEXPR型

FEXPR型

関数と実引数をまとめる

---

MACRO型

実は超古典的なLispの文脈に於いては、関数定義の種類と言うのは4種類くらいあったんです。
このうち、可変長引数を用いる関数に関しては、現代のLispでは関数定義で方法を変える代わりにレストパラメータで解決するようになりました。問題は、古典的Lispでは引数を評価しない関数が存在した、と言う辺りです。
同書からちょっと引用してみましょうか。

EXPR型の関数は、定義した引数(仮引数)の個数と実際の引数(実引数)の個数が一致して、しかも実引数の値を求めるために評価(evaluation)が行われます。本書で扱ったプログラムはほとんど全部EXPR型で定義されています。最も常識的な関数型です。
しかし、EXPR型ではうまく定義されない関数もあります。これらを定義するにはEXPR型以外の型が必要となります。


そして、同書では関数QUOTEの定義を紹介しています(ここに注目!QUOTEは関数なんです!)。

(DN QUOTE (X) X)

これビックリなんですよね。少なくとも現代的なLispではこんなQUOTEはマクロを使っても定義出来ません。まさしく引数を評価しないため「だけの」目的の為に存在してる関数定義方法があったわけです。

実はSchemeが登場する前後辺りで、ミニマリスティックな観点でLispの大刷新が起こりました。「引数を評価しない」のはマクロも同じなんで、「引数を評価しない関数」群がマクロに統合されたんです。システム的には当然の考え方ですよね。
一方、マクロは必ず展開が行われます。つまり、一種マクロは二段階評価になっていて、まずは展開(引数をそのままコードのテンプレートに挿入)して、最終的には「評価」されます。つまり、使いどころが異なるモノ同士がシステム的観点で統合されちゃった。これがマクロ学習の困難な面を露にしたのです。
加えると、「Schemeにはマクロが無い」と言うCLerの言い分も歴史的なちょっとしたアヤが原因だ、と言うのも分かると思います。マクロは展開を伴うので「いつ展開されるのか?」と言うのが重要なポイントで、CLでは「コンパイル時」と規程されている模様です。一方、Schemeはマクロ展開時がいつになるかR5RSに正確な規程が存在しません。Scheme登場時には多くのLisp方言が今よりもバラバラに存在してたんで、「展開時」をどこにするのかまだ考える猶予があったのでしょう。かつ、その言い方を借りるとLispには「引数を評価しない」関数があって良いのです。Schemeが持ってるのはもっと緩やかな、むしろ古典的なNEXPR型やFEXPR型の関数定義だ、と言う言い方も出来るのです。


多変数のSETQ


変数に値を代入するSETQという関数があります。プログラム中では、変数値の変更は1個だけでなく複数の変数について行いたい場合がよくあります。このようなときに(SETQ … ) (SETQ … ) … (SETQ … )と書くのは面倒なので、(MSETQ x1 v1 x2 v2 … xn vn)としたいと思います。ここでxi、viはi番目の変数名とその代入値を指します。
このMSETQの定義を与えてください。



同書にはFEXPR型関数定義の例として上のような問題を掲載しています。「関数定義」の問題として、ですよ。マクロじゃないんです。現代的なLispを扱ってると信じられない状況です(笑)。つまり、大昔のLispだと、この程度だとわざわざマクロを持ち込まなくても良かった、と言う事でしょう。
回答例は以下のようになっています。

(DF MSETQ (L)
(PROG (X V)
(COND [(GREATERP 2 (LENGTH L))
(ERROR "ARGUMENTS LESS THAN 2!")])
(LOOP () (SETQ X (CAR L)) ;MSETQが使えれば、
(SETQ V (CADR L)) ;(MSETQ X (CAR L) V (CADR L) L (CDDR L))
(SETQ L (CDDR L)) ;と書けたはず。
(SET X (EVAL V))
(COND [(NULL L) (RETURN (EVAL X))]
[(NULL (CDR L))
(ERROR "ODD NUMBER OF ARGS!")]
))))

Schemeの衛生的マクロで書くとこうですかね。

(define-syntax mset!
(syntax-rules ()
((_ x)
(error "arguments less than 2!"))
((_ x v)
(set! x v))
((_ x v y)
(error "odd number of args!"))
((_ x v y ...)
(begin (set! x v)
(mset! y ...)))))

他にもこんな例題が掲載されています。

構造化構文


いわゆる構造化プログラミング(Structured Programming)では、goto文(PROG内のGOなど)を廃して、構造化構文を推奨しています。LOOPもその一例ですが、ほかにWHILEとREPEATと言う構造化構文がよく用いられます。
WHILEは(WHILE <条件> DO <実行文1> … <実行文n>)と言う形式で、<条件>が成立している間は実行文を繰り返し実行します。
一方REPEATは(REPEAT <実行文1> … <実行文n> UNTIL <条件>)という形式で、<条件>が成立するまで、実行を繰返します。
このWHILEとREPEATという二つの構造的繰返し実行関数を定義してください。


これもマクロ例題としては良くある問題なんですが、やっぱり注目してください。当時はこれらは関数として定義出来た、んです。
当時の解法だと次のような感じですね。

(DF WHILE (L)
(PROG (PRED EXEC)
(SETQ PRED (CAR L))
(SETQ EXEC (CDDR L)) ;DO のチェックは省きました。
(LOOP ()
(COND [(NULL? (EVAL PRED))
(RETURN 'END-OF-WHILE)])
(MAPC 'EVAL EXEC) ;MAPC を使って実行します。
)))

(DF REPEAT (L)
(PROG (PRED EXEC)
(SETQ L (REVERSE L)) ;逆転して処理しました。
(SETQ PRED (CAR L))
(SETQ EXEC (REVERSE (CDDR L)))
(LOOP ()
(MAPC 'EVAL EXEC)
(COND [(EVAL PRED) (RETURN 'END-OF-REPEAT)])
)))

これも今風にSchemeの衛生的マクロで書けば以下のようになりますかね。

(define-syntax while
(syntax-rules ()
((_ (pred exec ...)) ;do の省略
(do ()
((not pred) 'end-of-while)
(begin exec ...)))))

(define-syntax repeat
(syntax-rules ()
((_ (pred exec ...))
(do ()
(pred 'end-of-repeat)
(begin exec ...)))))

いずれにせよ、当時はこの範疇の「構文」でさえ、マクロで書くような事はなかったんです。もっとも、そのせいでコード中にevalを挿入して醜くなったりしてますが。
そして、マクロに「引数を評価しない」関数の役割が統合された為、マクロの「役割」が膨大になっちゃった、という副作用が出てきたのです。

2010年3月24日水曜日

TSS scramble は末尾再帰可能

valvallowさんのブログ見てたんですけど、The Seasoned Schemerのscrambleって全然意味不明ですな~~。困ったもんだ。
valvallowさんが抜粋したscrambleのコードですが、ちょっと見てみますか。

(define sub1
(lambda (n)
(- n 1)))

(define pick
(lambda (n lat)
(list-ref lat (sub1 n))))

(define scramble
(lambda (tup)
(letrec ((iter (lambda (t rev-pre)
(if (null? t)
'()
(let* ((n (car t))
(rev (cons n rev-pre)))
(cons (pick n rev)
(iter (cdr t) rev)))))))
(iter tup '()))))

ふ~む。何やってんだかサッパリです(苦笑)。
基本的にletrecがいけねえんだな(苦笑)。見づらい。
と言うわけでnamed-letに書き換えてみます。そして個人的にはあまりlet*が好きじゃないんで(何故、って訊かないで・笑)、そこも直して、pickも敢えて解体して書き直してみます。

(define (scramble tup)
(let iter ((t tup) (rev-pre '()))
(if (null? t)
'()
(let ((n (car t)))
(let ((rev (cons n rev-pre)))
(cons (list-ref rev (- n 1))
(iter (cdr t) rev)))))))

ふ~~~む。まだ何やってるか分かり辛いですよね。しかも、このスタイルは末尾再帰じゃない。何か末尾再帰出来そうな気がするんですよね~~。
コードの意味が分からん、かつ末尾再帰になりそう、と思った時どうすっか。一番簡単なのは継続渡し方式に取りあえず書き直してみる事です。ひょっとしたら、これで何か見えてくるかもしれません。やってみます。

(define (scramble tup)
(let iter ((t tup) (rev-pre '()) (k values))
(if (null? t)
(k '())
(let ((n (car t)))
(let ((rev (cons n rev-pre)))
(iter (cdr t) rev
(lambda (u)
(k (cons (list-ref rev (- n 1)) u)))))))))

んで、詳しい論理の流れはこのページを参考にして欲しいんですけど、上のコードは次のように書き換えられて、

(define (scramble tup)
(let iter ((t tup) (rev-pre '()) (acc '()) (k values))
(if (null? t)
(k (reverse acc))
(let ((n (car t)))
(let ((rev (cons n rev-pre)))
(iter (cdr t) rev (cons (list-ref rev (- n 1)) acc)
(lambda (u) (k u))))))))

ついでに次のように書き換えられます。

(define (scramble tup)
(let iter ((t tup) (rev-pre '()) (acc '()) (k values))
(if (null? t)
(k (reverse acc))
(let ((n (car t)))
(let ((rev (cons n rev-pre)))
(iter (cdr t) rev (cons (list-ref rev (- n 1)) acc) k))))))

ここまで来ると、もはやローカル手続きのkが要らなくなります。事実上、全く仕事をしてないから、です。
従って、題意のコードは、

(define (scramble tup)
(let iter ((t tup) (rev-pre '()) (acc '()))
(if (null? t)
(reverse acc)
(let ((n (car t)))
(let ((rev (cons n rev-pre)))
(iter (cdr t) rev (cons (list-ref rev (- n 1)) acc)))))))

となりますね。末尾再帰になりました。
末尾再帰の方が若干何やってるか分かりやすいかもしれません。

  1. n番目の指定はリストtの先頭の数字によって決定される。

  2. リストrevは(cons n rev)で生成される。また、このリストは常にリストtに対しては部分的に逆順となっている。

  3. リストaccはリストrevのn番目の要素をconsしていって生成される。

  4. tは(cdr t)となり(null? t) => #tとなるまで1番から作業を繰り返す。

  5. 返り値はaccを逆順にしたものである。


まだ、ちょっと分かり辛いかもしれませんね。例えば、

  1. tは(1 1 1 3 4 2 1 1 9 2)、revは空リスト、accも空リストとする。

  2. nは1となり、revは(1)となる。

  3. revのn = 1番目の要素は1なので、accは(1)となる。

  4. tは(1 1 3 4 2 1 1 9 2)となり、以下繰り返し。


つまり、キーポイントとしては「常にtに対して部分的には逆順のリストであるrevのn番目の要素を問題にしている」と言う事なんです。
まだ分かり辛いかもしれないんで、valvallowさんの真似をして出力部分を組み合わせてみます。

(define (scramble tup)
(let iter ((t tup) (rev-pre '()) (acc '()))
(if (null? t)
(reverse acc)
(let ((n (car t)))
(let ((rev (cons n rev-pre)))
(for-each (lambda (x) ; ここは出力
(display x))
(list "t = " t ", n = " n ", rev = " rev ", acc = " acc))
(newline) ; 改行
(iter (cdr t) rev (cons (list-ref rev (- n 1)) acc)))))))

出力結果は以下の通りです。

> (scramble '(1 1 1 3 4 2 1 1 9 2))
t = (1 1 1 3 4 2 1 1 9 2), n = 1, rev = (1), acc = ()
t = (1 1 3 4 2 1 1 9 2), n = 1, rev = (1 1), acc = (1)
t = (1 3 4 2 1 1 9 2), n = 1, rev = (1 1 1), acc = (1 1)
t = (3 4 2 1 1 9 2), n = 3, rev = (3 1 1 1), acc = (1 1 1)
t = (4 2 1 1 9 2), n = 4, rev = (4 3 1 1 1), acc = (1 1 1 1)
t = (2 1 1 9 2), n = 2, rev = (2 4 3 1 1 1), acc = (1 1 1 1 1)
t = (1 1 9 2), n = 1, rev = (1 2 4 3 1 1 1), acc = (4 1 1 1 1 1)
t = (1 9 2), n = 1, rev = (1 1 2 4 3 1 1 1), acc = (1 4 1 1 1 1 1)
t = (9 2), n = 9, rev = (9 1 1 2 4 3 1 1 1), acc = (1 1 4 1 1 1 1 1)
t = (2), n = 2, rev = (2 9 1 1 2 4 3 1 1 1), acc = (1 1 1 4 1 1 1 1 1)
(1 1 1 1 1 4 1 1 1 9)
> (scramble '(1 2 3 4 5 6 7 8 9))
t = (1 2 3 4 5 6 7 8 9), n = 1, rev = (1), acc = ()
t = (2 3 4 5 6 7 8 9), n = 2, rev = (2 1), acc = (1)
t = (3 4 5 6 7 8 9), n = 3, rev = (3 2 1), acc = (1 1)
t = (4 5 6 7 8 9), n = 4, rev = (4 3 2 1), acc = (1 1 1)
t = (5 6 7 8 9), n = 5, rev = (5 4 3 2 1), acc = (1 1 1 1)
t = (6 7 8 9), n = 6, rev = (6 5 4 3 2 1), acc = (1 1 1 1 1)
t = (7 8 9), n = 7, rev = (7 6 5 4 3 2 1), acc = (1 1 1 1 1 1)
t = (8 9), n = 8, rev = (8 7 6 5 4 3 2 1), acc = (1 1 1 1 1 1 1)
t = (9), n = 9, rev = (9 8 7 6 5 4 3 2 1), acc = (1 1 1 1 1 1 1 1)
(1 1 1 1 1 1 1 1 1)
> (scramble '(1 2 3 1 2 3 4 1 8 2 10))
t = (1 2 3 1 2 3 4 1 8 2 10), n = 1, rev = (1), acc = ()
t = (2 3 1 2 3 4 1 8 2 10), n = 2, rev = (2 1), acc = (1)
t = (3 1 2 3 4 1 8 2 10), n = 3, rev = (3 2 1), acc = (1 1)
t = (1 2 3 4 1 8 2 10), n = 1, rev = (1 3 2 1), acc = (1 1 1)
t = (2 3 4 1 8 2 10), n = 2, rev = (2 1 3 2 1), acc = (1 1 1 1)
t = (3 4 1 8 2 10), n = 3, rev = (3 2 1 3 2 1), acc = (1 1 1 1 1)
t = (4 1 8 2 10), n = 4, rev = (4 3 2 1 3 2 1), acc = (1 1 1 1 1 1)
t = (1 8 2 10), n = 1, rev = (1 4 3 2 1 3 2 1), acc = (1 1 1 1 1 1 1)
t = (8 2 10), n = 8, rev = (8 1 4 3 2 1 3 2 1), acc = (1 1 1 1 1 1 1 1)
t = (2 10), n = 2, rev = (2 8 1 4 3 2 1 3 2 1), acc = (2 1 1 1 1 1 1 1 1)
t = (10), n = 10, rev = (10 2 8 1 4 3 2 1 3 2 1), acc = (8 2 1 1 1 1 1 1 1 1)
(1 1 1 1 1 1 1 1 2 8 2)
>