星田さんの記事に対するコメント。
と言うかちと補足的な内容だ。
もうホント、Scheme系実装のマニュアルって知ってる限り、Gauche以外はどれもサイテーだ。動作例示をしないマニュアルとか一体何の役に立つんだ、って程酷い(しかも書いてる通りに動作もせん、なんつーのもあるあるだ)。
オープンソース界隈では、とにかくMicrosoftが批判されるんだけど、一方、Microsoftが作った言語処理系のマニュアルは常に評価が高いんだよ。Microsoftの統合開発環境ではAPIや関数群は物凄く丁寧に例示+説明がされてる、との事だ。Windowsがこの世を支配してる、恐ろしい、とか言うけど、Microsoftは自社のOSやその開発環境をみんなに使ってもらう為に常に心を砕いてるのは事実なんだ。
それに比べると、Schemeが地べた這いつくばってるのも理由はハッキリしてる。とにかくリファレンスマニュアルの質の悪さだ。調べてみようと思うとイミフな動作説明なしの記述しかしてねぇような言語処理系を一体誰が使いたがるんだ、って話だよな。
とまぁそれはそれとして、だ。
Racketのmatch-lambdaとmatch-lambda*の説明。
そしてGaucheのそれ。
星田さんが書いてる通りだ。
問題はmatch-lambda*の方で・・解説アッサリしすぎだろw 例文も無いし。
うん。フツーに考えれば暴挙なんだよな。
Gaucheのマニュアルも、match-lambdaに付いては例示してるけど、match-lambda*に対してはあんまRacketと変わらん。
両者ともある種の「甘え」があって、結果、全くのプログラミング言語未経験者が「はじめて学ぶプログラミング言語をScheme系に出来ない」くらいのミスになってるたぁ思う。
いや、「はじめて学ぶプログラミング言語にLisp系言語を選ぶやつぁいない」って言われればその通りなんだけど(笑)、「そこを想定しておかない」から「敷居が高い言語だ」と勘違いされ、そして殆どの人は決してLispに近づかなくなるんだ。
じゃあその「甘え」ってのは何だろう。そう、これらは「マクロ」だ。つまり、「展開型を表示しておいたら"Lisp慣れしてる層は"意味が分かるだろう」と言う意味だ。「甘え」だろ(笑)?
マクロが無い言語からマクロてんこ盛りの言語へやってきて、マクロ展開型表示で説明を終えてる、ってのがどんだけ敷居が高いのか。Lisperってその辺の読者モデルの想定がいっつも甘いんだよなぁ。だから流行らない。
逆に言うと、フツーのプログラミング言語のリファレンスと違って、Lisp系言語の場合はリファレンスを読む際に、必然的にマクロの知識が必要になる、って事になる。
なんてめんどくせぇ言語なんだろう(苦笑)。
ここでも何度目にかなるマクロの説明を書いておこう。
Lispに於けるマクロとは。平たく言うと、ソースコードを書き換える機能の事だ。字面通りに受け取って良いのか否か、ってのはあるが、ザックリ言うと、大まかにはそういう機能の事を指す。
例えばletはマクロだ(※1)。
ここで貴方が、例えば次のようなletを含んだコードを書いた、とする。
(define (foo)
(let ((x 2) (y 3))
(* x y)))
この関数fooは、実行時には次のようなソースコードに書き換えられてる、ってことだ(※2)。
(define (foo)
((lambda (x y)
(* x y)) 2 3))
Lisp処理系では上のような「ソースコードの書き換え」が頻繁に起こる。この「ソースコードの書き換え」をマクロ展開(macro-expansion)と呼ぶんだ。
平たく言うと、この「ソースコードの書き換え方」を指定する方法をマクロと読んでるんだ。
何度か紹介してるが、例えば上のマクロlet。これはSchemeでは次のようにして「ソースコードを書き換えろ」と指定する。
(define-syntax let
(syntax-rules ()
((_ ((var init) ...) body ...)
((lambda (var ...) body ...) init ...))))
これにより、Schemeは、ソースコード中にletを見つけて
(let ((var init) ...) body ...)
と言うパターンがあったら、
((lambda (var ...) body ...) init ...)
と言うパターンへとガンガンと書き換えてから実行していく。
上の例で言うと
(let ((x 2) (y 3))
(* x y))
と言うletが引き連れるパターンを見つけたら
((lambda (x y)
(* x y)) 2 3)
に書き換える、ってこった。
もう一度言うが、この作業をマクロ展開と呼ぶ。
と言うわけで、何故にLisperが、ユーザーから言わせれば望ましくないにも関わらず、上記のようなお座なりなmatch-lambdaやmatch-lambda*の説明で済む、って考えてるのか、ってぇのもこれが理由なんだ。
例えばGaucheのmatch-lambdaの実行例をもう一回見てみよう。
そして、match-lambdaに対しての「マクロ展開型」に対して、GaucheのリファレンスもRacketのリファレンスも、clauseの中身の解釈はさておき、同じような事を書いている。
つまり、match-lambdaがマクロ展開した時点で、上記のソースコードは次のように書き換えられてる、って事だ。
そして、リファレンスマニュアル通りに解釈すると、match-lambdaと言うマクロは次のように定義されている、と予想する事が出来る。
(define-syntax match-lambda
(syntax-rules ()
((_ clause ...)
(lambda (expr)
(match expr
clause ...)))))
こう考えてみると、Gauche及びRacketのマニュアルは、マクロの知識が無いとイミフだが、実は実装方法をぶっちゃけてる、と言うこれ以上ない説明を一応してんだよ。
親切なんだか不親切なんだかサッパリ分からんけどな(苦笑)。
いずれにせよ、フツーのプログラミング言語だと関数の知識がないとリファレンスを読むのは苦労するが、Lisp系言語の場合はその知識に加えてマクロ(あるいはScheme用語ではsyntax)の知識が無いと読めない、っちゅーこっちゃな。
同様に、match-lambda*の説明は、Gaucheは
Racketは
と書いてる。
と言う事は、想定されてるマクロは
(define-syntax match-lambda*
(syntax-rules ()
((_ clause ...)
(lambda expr
(match expr
clause ...)))))
と言う事だ。そしてラムダ式の引数にカッコが付いてない以外はmatch-lambdaのマクロと全く同じだ(※3)。
そして結果、ここで起こるマクロ展開は次のようなモノになる。
マクロ展開型を直に挿入したコードとマクロを使ったコードは全く同じ結果を返す、と言う事を実感しよう(※4)。
なお、ちと不思議に思うかもしれない。マクロを作っても1行か2行程度短くなっただけじゃん?本当に必要?と。
これに対して、ポール・グレアムは次のように言っている。
マクロのやっていることと言ったら,打ち込む文字の節約が全てだ. 似たような考え方をしたいなら, コンパイラの仕事はマシン語を打ち込む作業を省くことだと言える. ユーティリティの効果は累積していくので,その価値は見過ごせない. 単純なマクロを複数の層に重ねることで, エレガントなプログラムと理解しがたいプログラムとの違いが分かれる.
※1: 厳密に言うと、SchemeではそうだがANSI Common Lispでは違う。ANSI Common Lispは最適化の関係のせいか、letはマクロとしては定義されていない。
しかしながら、あくまでここでは、理論的にはそうだ、と言う事にしている。
※2: ここの説明は曖昧だ。ANSI Common Lispの場合、コンパイル時に、と規定されている。Schemeはその辺は、仕様上コンパイラが無い為ハッキリとはしない。
※3: ラムダ式の引数にカッコが無い場合は、そのラムダ式の引数は可変長引数、と言う意味になる。R7RSを参照の事。
※4: なお、本当の事を言うと、matchそのものもマクロなんで、「真のマクロ展開型」は展開に展開を重ねて、結果、もっとメチャクチャに長く、ややこしくなっている。
ただ、そこまでここで展開するのは議論上、必要ない。
なお、RacketのIDE、Dr.Racketに付いてるMacro Stepperと言うのはこのマクロの展開型を知る為の機能だ。
Macro StepperはStepボタンをクリックしていけば一段階づつコードで展開されていくマクロを眺められるが、Endボタンをクリックすると、Racketで用意されてるprimitive(Racketで用意されてる処理系の真の基本機能)の段階までマクロを完全に展開する。
もうこうなると人が書けないコードになってる(笑)。と言うより、人が書いたコードがRacketと言う処理系の中ではこのようにマクロ展開されまくって実行されてる、と言う事だ。
なお、ANSI Common Lispにもマクロ展開を眺められるmacroexpandと言う機能が仕様上定義されている。