SICP 4.1.6 Ex. 4.16 Ex. 4.17 Ex. 4.18 Ex. 4.19 Ex. 4.20 Ex. 4.21

  • 投稿日:
  • カテゴリ:

Ex. 4.16

内部defineの実装

a. lookup-variable-value(p.225) の変更

(define (lookup-variable-value var env)
  (define (env-loop env)
    (define (scan vars vals)
      (cond ((null? vars)
             (env-loop (enclosing-environment env)))
            ((eq? var (car vars))
             (if (eq? (car vals) '*unassigned*)  ;; *unassigned* の処理追加。このif以外は元と同じ
                 (error "Unassigned variable" var) 
                 (car vals)))
            (else (scan (cdr vars)(cdr vals)))))
    (if (eq? env the-empty-environment)
        (error "Unbound variable" var)
        (let ((frame (first-frame env)))
          (scan (frame-variables frame)
                (frame-values frame)))))
  (env-loop env))

b. scan-out-defines の追加
方針:2パスで作る。1パス目で変数をかき集めて、2パス目で変換式を作る。

(define (scan-out-defines exps)
  (let ((lll '())  ;; (u *unassigned*) (v *unassigned*) ... のリストを集める
        (sss '()) ;; (set! u <e1>) (set! v <e2>) ... のリストを集める
        (bbb '())) ;; 本文を集める
    (define (loop exps)
      (if (last-exp? exps) 
          (set! bbb exps)
          (let ((var (definition-variable (first-exp exps)))
                (val (definition-value (first-exp exps))))
            (set! lll (cons (list var ''*unassigned*) lll))
            (set! sss (cons (list 'set! var val) sss))
            (loop (rest-exps exps)))))
    (loop exps) ;; 1パス目で変数を集める
    (cons 'let (cons lll (append sss bbb))))) ;; 2パス目で変換式作る

Ex. 4.17

内部define を逐次的に解釈したときの環境

内部define を let に変換したときの環境

let に変換したときに、フレームが増える理由:
let は、lambda に変換されるので、lambdaが2重になる。 lambda 1つにつき、実行時に1つフレームを作るので、結局フレームも2重になる。

どちらも同じ結果になるのは、<e3> の実行フレームの内容が同じだからである。

フレーム増やさずに、内部define を同時にする方法:
let を使うとどうしてもフレームが増えるので、define と set! を使ってみる

(lambda <vars>
  (define u <e1>)
  (define v <e2>)
  <e3>)

を、このように変換する。

(lambda <vars>
  (define u '*unassigned*)
  (define v '*unassigned*)
  (set! u <e1>)
  (set! v <e2>)
  <e3>)

Ex. 4.18

内部define を2重の let で変換した場合

solve を2重の let で変換すると

(define (solve f y0 dt)
  (let ((y '*unassigned*)
        (dy '*unassigned*))
    (let ((a (integral (delay dy) y0 dt))
          (b (stream-map f y)))
      (set! y a)
      (set! dy b)
      y)))

となる。dy の評価は、let式 で a に代入されるときに1回、set!式で y に代入されるときに1回の合計2回評価される。delay が効くのは、最初の1回だけで、2回目は dy がそのまま評価される。 2回目 dy が評価されるとき dy は *unassigned* なのでエラーになる。

Ex. 4.17のように、solve を1重の let で変換すると

(define (solve f y0 dt)
  (let ((y '*unassigned*)
        (dy '*unassigned*))
      (set! y (integral (delay dy) y0 dt))
      (set! dy (stream-map f y))
      y)))

となる。この場合、dy の評価は、set!式 で y に代入されるとき1回だけで、このときdelay が効いているので、エラーにならない。

ところで、この問題の2重の let の意味がわからない。こうしたほうがよい場合があるのだろうか?

Ex. 4.19

徹底的に遅延評価するという見方なら、Eva の解釈が正しい。
場合によって遅延したり、しなかったりするのが一番困る。

Eva の解釈に沿った置き換え

(let ((a 1))
  (define (f x)
    (define b (delay (+ a x))) ;; a が見えないよう delay する
    (define a 5)
    (+ a b))
  (f 10))

この例だといけそうな気がするが、define とか、 let とかが入り組んでくるとうまくいかなくなる。
きちんとした解決は、eval を遅延評価にしてしまうことであろう。

Ex. 4.20

letrec 構文の追加

a. letrec を let 式に変換する。
変換方法は、こんな感じ

(letrec ((u 2)(v 3))
  (+ u v))
(let ((u '*unassigned*)
      (v '*unassigned*))
  (set! u 2)
  (set! v 3)
  (+ u v))

実装は、省略。

b. 問題の意図は letrec の使用上の制限について注意することである。
通常のSchemeでも、letrec については、このような使用上の制限がある。

(letrec ((var1 val1)(var2 val2)...)...)

において、各val は、他の var を直接参照してはならない。ということ。

問題の letrec 実行時の環境図は、こんな感じ。

letrec を let で変換したときの環境図

これらの環境図から分かるのは、 どこもおかしなところはなく、Louis の言うとおり、この f は正常に動作するであろうということである。

しかし、よく見ると even?, odd? は、 lambda 式で、lambda 式の中から互いを参照している。lambda 式の中からの参照でなく、直接参照した場合はどうなるのであろうか? Louisの考えはこの点が抜けている。

直接参照というのは、こんな場合である。

(define (f x)
  (letrec ((a b)
           (b 2))
    (+ x a b))

で、これは、もちろんエラーになる。(a b) を評価する時点で、b の未定義エラーになる。

Ex. 4.21

Ex. 4.20で言っていたのは、 letrec は変数を互いに直接参照できないことであった。
Louiseは、この点を見落としていたが、逆に言えば、Louiseのように参照を lambda 式のなかに埋め込んでしまえば、直接参照を避けて変数の相互参照が可能であるということである。結局 Louiseは letrec の制限を正しく回避していたことになる。

で、変数の定義を lambda 式にするのならば、わざわざ letrec を使わなくても、最初から lambda 式だけで書けるのではないか。というのがこの問題の主旨である。

a. ループ用の内部定義関数を、引数にすればよい。

(lambda (n)
  ((lambda (fib)
    (fib fib 0 1 n))
  (lambda (fb a b k)
    (if (= k 0)
       b
       (fb b (+ a b) (- k 1))))))

こんな感じ?動かしてないけど、、、

b.相互参照ありの再帰

(define (f x)
  ((lambda (even? odd?)
    (even? even? odd? x))
   (lambda (ev? od? n)
     (if (= n 0) true (od? ev? od? (- n 1))) ;; こっちがeven?
   (lambda (ev? od? n) 
     (if (= n 0) false (ev? ev? od? (- n 1))))) ;; こっちがodd?

こんな感じかな。試してないけど。

ポイントは、define とか、let で変数を保存していたが、その代替品として、関数(メタ関数)の引数を変数の記憶場所に使えるということである。(若しくは、もともと関数の引数にしか記憶作用がなくて、define、let はそれを利用してるだけと言ったほうがいいかもしれない)