kb84tkhrのブログ

何を書こうか考え中です

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階層にすることにしました

  • 汎用ユーティリティ
    firstsecondYなど、valueに限らず利用される関数
  • value内で利用するユーティリティ
    text-oflookup-in-tableなど、主にvalueで使うデータ構造にアクセスするための関数
  • 本体
    meaning再帰に含まれる関数
  • value
    meaningを呼び出す関数を返すだけの関数

最大の難関はカッコの対応を合わせることだったりしました
最後の方では少し慣れましたが

終わり

これでscheme手習いが終わりました
次は何にするか少々迷うところではありましたが
自然なところで続編の「scheme修行」に進もうと思います