Scheme手習い(24) eval、またはvalue、またはmeaning(5)
普通のソースをdefine
を使わないソースに変換する手法は
様々なシーンで役立つノウハウですのでまとめておきましょう(嘘
lambdaによる名前付け
define
による定義は、lambda
による名前付けに書き換えることができます
修正前
(define A (lambda (a) (aaaaa)))
(A aaa)
修正後
((lambda (A)
(A aaa))
;Aの本体
(lambda (a) (aaaaa)))
一律名前付けして使うより、適宜インライン展開して使った方がかえって見やすい場合も
あるかもしれませんが今回は全て元のまま名前付けしました
複数の関数
複数の関数を同時に名前付けすることが可能です
適宜グループにして同一の階層で定義し、階層が深くなり過ぎないようにします
ただし、互いに呼び出し関係にない場合に限ります
同時に定義すると、相手の関数がスコープにはいらないためです
修正前
(define A (lambda (a) (aaaaa)))
(define B (lambda (b) (bbbbb)))
(A (B bbb))
修正後
(lambda (A B)
(A (B bbb))
;Aの本体
(lambda (a) (aaaaa))
;Bの本体
(lambda (b) (bbbbb)))
呼び出しの入れ子
呼び出し関係のある関数は、呼び出される関数が呼び出す関数のスコープ内に入るよう
呼び出される関数の名前付けを外側にし、呼び出す側の関数を内側で名前付けします
修正前
(define A (lambda (a) (B aaaaa)))
(define B (lambda (b) (bbbbb)))
(A aaa)
修正後
((lambda (B)
((lambda (A)
(A aaa))
;Aの本体
(lambda (a) (B aaaaa))))
;Bの本体
(lambda (b) (bbbbb)))
原理上は呼び出し関係がない場合でも全て1関数ごとに入れ子にして問題ないはずですが
モニタの横幅と頭がついていけないものと思われます
関数の隠蔽
ある関数からのみ呼び出される関数は
呼び出す関数だけを囲むように名前付けしてやることにより
他の関数から隠ぺいすることができます。
修正前
(define A (lambda (a) (B aaaaa)))
(define B (lambda (b) (C bbbbb)))
(define C (lambda (c) (ccccc)))
(A (B bbb))
修正後
((lambda (B)
((lambda (A)
(A aaa))
;Aの本体
(lambda (a) (B aaaaa))))
;Bの本体
((lambda (C)
(lambda (b) (C bbbbb)))
;Cの本体 B以外からは見えない
(lambda (c) (ccccc))))
B
の本体は、C
を引数にとってBという関数を返す関数になっています
この技を使うと、全体の入れ子の深さを浅くできます。呼び出し関係を思い浮かべつつ階層を考えます
一般的なモジュール化やカプセル化と違って外に見せられる関数はひとつだけです
(何かいい工夫はないかな)
再帰する関数
再帰する関数はY
を使って定義します
修正前
(define A (lambda (a) (A aaaaa)))
(A aaa)
修正後
((lambda (A)
(A aaa))
;Aの本体
(Y (lambda (A)
(lambda (a) (A aaaaa)))))
複数の引数を持つ再帰関数
複数の引数を持つ再帰関数は1引数+残りにカリー化します
呼び出し側もカリー化した呼び出し方にする必要があるので注意が必要です
修正前
(define A (lambda (a1 a2) (A aaaaa)))
(A aaa1 aaa2)
修正後
((lambda (A)
((A aaa1) aaa2))
;Aの本体
(Y (lambda (A)
(lambda (aaa1)
(lambda (aaa2) (A aaaaa))))))
関数のラッピング
いちいちカリー化した形で呼び出すのが面倒な場合は
Y
による定義を直接使うのではなく、呼び出しやすい形式でラッピングすることも可能です
修正前
(define A (lambda (a1 a2) (A aaaaa)))
(A aaa1 aaa2)
修正後
((lambda (A)
(A aaa1 aaa2))
;Aの本体
((lambda (A)
(lambda (aaa1 aaa2) ((A aaa1) aaa2)))
(Y (lambda (A)
(lambda (aaa1)
(lambda (aaa2) (A aaaaa)))))))
関数を返す関数を書くという点で、関数の隠蔽でやったことと似ています
value
の書き直しではlookup-in-table
でやってみました
修正前と同じ形で呼び出すことができるようになりますが
その分本体の定義部分が面倒になります
効果は正直微妙です
他の関数を経由した再帰
いくつかの関数を経由して再起する場合は、Y
の本体の内側に、経由する関数を定義します
Y
の外側で定義した関数からはY
によって定義される関数を呼び出すことはできませんので
修正前
(define A (lambda (a) (B aaaaa)))
(define B (lambda (b) (C bbbbb)))
(define C (lambda (c) (A ccccc)))
(A aaa)
修正後
((lambda (A)
(A aaa))
;Aの本体
(Y (lambda (A)
((lambda (C)
((lambda (B)
;Aの本当の本体
(lambda (a) (B aaaaa)))
;Bの本体
(lambda (b) (C bbbbb))))
;Cの本体
(lambda (c) (A ccccc))))))
value
の書き換え
value
の書き換えでは、呼び出し関係の図を見ながら末端の関数から定義していきました
始める前はどれだけ入れ子が深くなるかビビリ入ってましたが
やってみると関数のグループ化や隠蔽により心配するほどの入れ子にはなりませんでした
以下の4階層にすることにしました
- 汎用ユーティリティ
first
、second
、Y
など、value
に限らず利用される関数 value
内で利用するユーティリティ
text-of
、lookup-in-table
など、主にvalue
で使うデータ構造にアクセスするための関数- 本体
meaning
の再帰に含まれる関数 value
meaning
を呼び出す関数を返すだけの関数
最大の難関はカッコの対応を合わせることだったりしました
最後の方では少し慣れましたが
終わり
これでscheme手習いが終わりました
次は何にするか少々迷うところではありましたが
自然なところで続編の「scheme修行」に進もうと思います