星田さんの記事に対するコメント。
初期値が「どんな型でも取れる」ってのがすごいですね・・ドットリストを使ってACCの役割させてたのは驚いたなぁ・・つまり初期値と同じ型が返せるLambda式を書けば良いってこと・・になるのか・・な?
いや、初期値と同じ型じゃなくても実は結構です。
(define (last xs)
(foldl (lambda (y x)
y) #f xs))
が成り立つのはそれが理由ですね。初期値はブーリアンだったのが、数値に変わってますね。
同じ型にする、んじゃなくって「形式をどうするか」であって、上の計算の場合は「初期値を計算に利用しない」と言う前提があるからこれでO.K.なわけですね。
初期値に形式を持たせ、それに従うかどうか、と言うのは設計した計算プロセスに依ります。
そこもラムダ式の設計次第、なんです。
あ、マクロでよく聞く「展開」ってこういうルールで変換されることを言ってるのかな?まずはSyntax-rulesからだけどいずれはマクロも・・
うーんとね、「展開」ってのは「マクロで書かれたルールに従って」実質的にソースコードの指定場所を「書き換えちゃう」事です。
どうしようかな・・・。うん、ちとズレるけど。
一般的に「マクロ」ってのはショートカットの事なんですよ。例えばExcelのマクロもショートカットでしょ?「複雑なExcel上の工程」を一発で再現する。
で、プログラミング言語で「マクロ」ってのもショートカットの意で、先にも書いた通り、「ソースコードに書かれた字句を指定に従ってすげ替える」機能の事を言うんです。
Lisperの連中は「Lispのマクロは他のマクロとは違う。Lispマクロは強力だ。」って言いたがるんだけど、原理的な話をすると、実は上に書かれてる通り、マクロは「指定に従ってソースコードの一部を書き換える」機能なんです。
例えばね。C言語は知らんだろう、って前提で話をしますが。
これがC言語のマクロです。
#include <stdio.h>
#define X68000 printf("I love X68000!\n") /* ここがマクロ */
int main(void) {
X68000; /* コンパイラはX68000と言う字面を見るとそこを書き換える */
return 0;
}
例えばC言語だと
#define マクロ名 何らかの表現
ってのがマクロ定義です。
で、上のソースコードだと、コンパイル時に「X68000」って字句を見かけると全部 printf("I love X68000!") に置き換えます。これが「マクロの展開」と呼ばれるものです。
んで、実はANSI Common LispもSchemeもやってるこたぁC言語のマクロと実は全く同じです。コンパイル時に(とは言ってもSchemeには仕様上コンパイラがないが)、ソースコードの中に「マクロ名」を見かけると、そこを指定した通りに「何らかの表現」に置き換えていく。全く仕組みは同じなんです。
ただ、それでどうしてLispのマクロが強力なのか、と言うのは、マクロの仕組みが強力なんじゃなくってS式って表現形式が強力なんですよ。マクロの仕組みはフツーの言語と違わないのに、データにも関数にもなるS式がLispのマクロを他の言語じゃ考えられない程強力にしてる。
例えば、何度か書いてるけど、letはマクロで、Schemeでは理論的にはこう定義されている。
(define-syntax let
(syntax-rules ()
((_ ((x e) ...) body ...) ;; このパターンを見かければ
((lambda (x ...) ;; こう書き換える
body ...) e ...))))
で、例えば
(let ((x 1))(+ x 1))
と書いたとする。
ここでSchemeがやることは
- Schemeがマクロ名「let」を発見する。
- let以降が定義したパターンに一致するか調べる。
- パターンに一致すれば、それを定義した通り、ソースコードを(事実上)書き換え、変数なんかも並べ替える。
です。
結果、上のletを使ったコードは、実行される前に
((lambda (x) (+ x 1)) 1)
に書き換えられるんです。これが「マクロ展開」です。
実際はマクロがマクロを使ってる事もあるんで、1つのマクロの中で別のマクロを見つけるとそこでもマクロ展開が起き、結局、プログラミング言語のコアが提供する原始的な関数(そのままPrimitiveとか呼ぶ)を使って「ベタ書き」されるトコまでマクロ展開が行われます(と言う事は、1行で済んでたプログラムがこの時点で数十行になる場合がある)。
そのあと「やっと実行される」わけです。
ANSI Common Lispが仕様上コンパイラがなければならない、ってのはこのプロセスが厳密に定義されてるから、です。そして一回ソースコードがコンパイルされれば、その時点で、「書くには非効率で汚いコード」だけど、「実行するには最適な展開されたソースコード」を元にしたオブジェクトコード(機械語の事もあるし、バイトコードの場合もある)が生成されます。
一方、Schemeの場合、実装依存でコンパイラを持ってる場合があるけど、仕様には含まれていない。よって単純に考えると、インタプリタ上では、マクロを「実行」するにはフツーに関数を実行するより時間がかかる、と言う事は「あり得ます」ね。なんせ実行する前にソースコードを毎回マクロ展開せなアカンので、その分実行効率が落ちるのが「原則」となる。
ただ、いずれにせよ、「指定に従ってソースコードの指定場所の字句を定義に従って置き換えていく」ってのはC言語のそれと変わりません。繰り返しますが、Lispの場合、マクロが強力なのはS式、って表現形式が強力なせいで、とんでもないパワーがあるんだ、って言った方が正しいです。
という訳で写経。単にリストにする部分の文法が変わってるってくらい?
appendを利用した版に対してはその通りです。それくらいしか意味がない。
ただし、繰り返しますが、consを使用した版と違って、最期の返り値にreverseする必要がなくなってる。
うん、初期値が決まればLambda部の書き方が決まるという事だろうから、まさしく出力するデータ形式を決めてプロシージャを作るってことなんすね!
その通りです。
データが決まれば全てが決まる。
ニクラウス・ヴィルトが言う通りで、それがあらゆるレベルで起きてきます。
初期値部分に演算(って言うのか・・分からないんですけど手順的な?)のような物が入ってても良いと。
いいです。
と言うより、foldlを適用する前にその引数が全部評価されます。
フツーのプログラミング言語は、関数を適用する前に関数に与えられた引数が全部評価されます。これを先行評価と呼びます(あるいは正格評価、と呼ぶ)。
あとList-refの時にドットペアでCAR部にカウンタ(y)、CADR部にxnのCARを入れれば!と思ったんですけど初期値が更新されるって分からなかったので ((0 . a)(1 .b)(2 .c)...とリストがつながって行くイメージしか出来なくて「じゃあ結局特定するのにList-refがいるじゃねぇかwと。
そう、「破壊的変更」で想像するとそう思っちゃうんですよ。
そこが「落とし穴」です(笑)。