タグ「SICP」が付けられているもの

SICP 4.3.1 Ex. 4.35 Ex. 4.36 Ex. 4.37

  • 投稿日:
  • カテゴリ:

amb と require の動作の確認

amb1
...
amb2
  ...
  require
  ...
  1. require が失敗すると、require の前に実行された amb2 に処理が戻る
  2. amb2 はバックトラックして、その位置からプログラムが再実行する
  3. amb2 のリストが終了してて バックトラックできない場合、その前のamb1 に戻る(バックアップ)
  4. amb1 もバックトラックしてその位置からプログラムが再実行する
  5. amb1 のリストが終了していてバックトラックできない場合、前の amb に戻りたいが、ないので先頭まで戻って終了する。
  6. require が成功した場合は、そのまま下の式を実行してプログラムの最後まで行って終了する。
  7. プログラムの最後まで行って終了した場合、評価器の駆動ループが (require false) を発行して処理を前の amb に戻す。

図にするとこんな感じ

なお、require 失敗時の戻り先の amb をどれにするかは、関数の親子関係には全く関係がない。また、実行フレームの親子関係にも全く関係ない。関係あるのは、実行の順番だけである。require より以前に実行された amb に戻ることになっている。例えば

(define (f) 
  ...
  (amb 1 2 3)
  ...)

(define (g) (require false))

(begin ...
       f
       ...
       g)

を実行した場合、g の中の require の失敗時の戻り先は、f の中の amb である。なぜなら、f の中の amb は、require より以前に実行されているからである。

まとめると、基本的な動作は、(amb ...) を実行して、リストが終わっていた場合、その前に実行した amb に戻る(バックアップ)ということである。

require の失敗というのは、(amb) を実行することである。これはambのバックアップ動作を利用して前の amb に戻っているのである。

逆に言えば、無条件に以前の amb に戻りたいときは、(require false) とか (amb) を実行すればよいということである。

Ex. 4.35

p.246 の an-element-of 、an-integer-starting-from と同じようにつくる

(define (an-integer-between s e)
  (require (<= s e))
  (amb s (an-integer-between (+ s 1) e)))

Ex. 4.36

i, j, k が無限ストリームになった場合の探索の様子を見てみる

こんな感じで、探索が k の無限ストリームにつかまって返ってこなくなる。

探索ツリーの途中や末端が無限ストリームになると、探索が返ってこなくなる。なので、 修正方法は、最初のストリームだけ無限にして、途中や末端のストリームは有限にすることである。

(define (a-pythagorean-triple-between low high)
  (let ((k (an-integer-starting-from low)))
    (let ((i (an-integer-between low k)))
      (let ((j (an-integer-between i k)))
        (require (= (+ (* i i) (* j j)) (* k k)))
        (list i j k)))))

i, j は三角形の短辺、k は三角形の長辺を指している。短辺は長辺より長くなることはないので。

Ex. 4.37

2重の amb に対して、2重の require がある場合。

amb1
amb2
  require1
  require2

こんな感じ。この場合、この本の評価器の行う探索は、

  1. require1 によるツリーの枝刈りは行わない。ツリーは全て探索される。
  2. require1、require2 が2つあるからといって、探索を2回行うことはない。探索は1回だけである。

である。まとめると、評価器は amb1、amb2 の作るツリーの全体を1度だけ探索する。ということである。

枝刈りをしないので、効率の良い悪いは、単純にツリーの大小で比べられる。

で、low=1, high=5 のとき、Benさんのプログラムの探索ツリーは、こんな感じになる。

調査の必要な枝は 15本。一方、前問(Ex. 4.35)は amb が3重になっており、探索ツリーはこんな感じになる。

調査の必要な枝は 35本である。Benさんのほうが効率が良い。

SICP 4.2.3 Ex. 4.32 Ex. 4.33 Ex. 4.34

  • 投稿日:
  • カテゴリ:

Ex. 4.32

遅延度が高いというのは、cons の第1引数が遅延することを指す。3章のストリームでは、これは遅延されない。3章ストリームで遅延されるのはcons の第2引数のみである。

例1. 未知数のストリームが作れる。

(x x x x x ...)

こういうの。3章ストリームだと

(define xs (cons-stream x xs))

x の未定義エラーになる。cons-stream は第1引数の x を評価するからだ。 遅延ストリームだと

(define xs (cons x xs))

エラーにならない。遅延ストリームの cons はどちらの引数も遅延するからだ。

例2. ネストが無限のリストが作れる。

((((...(1)...1) 1) 1) 1)

こういうの。3章ストリームだと

(define ones2 (cons-stream ones2 '(1)))

ones2 の未定義エラーになる。cons-stream は第1引数の ones2 を評価するからだ。 遅延ストリームだと

(define ones2 (cons ones2 '(1)))

エラーにならない。遅延ストリームの cons はどちらの引数も遅延するからだ。

Ex. 4.33

方針: ペアのクォートは cons 式で置き換えてから評価して返す。

'(a b) → (eval  (cons 'a (cons 'b '())) the-global-environment)

という感じ。

アトムのクォートはそのまま返す。

'a → a
'1 → 1
'() → ()

という感じ。実装は

;; convert a quoted pair into cons expression and then eval it,
;; on the other hand a quoted atom return itself.
(define (text-of-quotation exp env) 
  (let ((e (cadr exp)))
    (if (pair? e)
        (eval (quote->cons e) env)  ;;; quoted pair is converted to cons and evaled
        e)))    ;;; quoted atom returns itsef

;; convert a pair into cons expression
;; like this
;; (a b) -> (cons 'a (cons 'b '()))
;; convert an atom as quoted
;; a -> 'a
;; 1 -> '1
;; () -> '()
(define (quote->cons exp)
  (if (pair? exp)
      (list 'cons (quote->cons (car exp)) (quote->cons (cdr exp)))
      (list 'quote exp)))

(define (eval exp env)
        ...
        ((variable? exp)(lookup-variable-value exp env))
        ((quoted? exp) (text-of-quotation exp env)) ;; changed
        ((assignment? exp)(eval-assignment exp env))
        ...

アトムのクォートの処理がどんくさくて微妙。

Ex. 4.34

まず (cons 1 2) の評価結果は、

(compound-procedure (m) ((m x y)) <procedure-env>) ← ①

となる。procedure として返ってくる。cons を lambda に変換しているのだから当然である。 で、これを印字するときだけ (1 2) としたいわけであるが、、、

まずこの procedure を他の procedure と区別する方法がない。ので何か区別をつける工夫をしてやらねばならない。

面倒なので、あまり正しくない方法であるが、仮引数 m を $cons とかにして仮引数の名前で区別することにする。

次に表示のためにリストの car と cdr を取得せねばならないが、これは ①に car, cdr を apply して取り出すこととする。

あと、無限リストの対応は、面倒なので、リストを10個表示したら、そこで中止するくらい。

以上の方針で、実装しようとしましたが失敗しました。

修正途中のソースは以下のとおり。

;; This doesn't work at all.
(define (user-print object)
  (if (compound-procedure? object)
      (if (cons-procedure? object)    ;;; changed 
          (display (actual-value (apply (actual-value 'car the-global-environment) object the-global-environment)) the-global-environment) ;;; changed
          (display (list 'compound-procedure
                         (procedure-parameters object)
                         (procedure-body object)
                         '<procedure-env>)))
      (display object)))

(define (cons-procedure? obj)
  (eq? (caadr obj) '$cons))

あと、子Scheme のcons の定義も変える

;;; L-Eval value:
(define (cons x y)
        (lambda ($cons) ($cons x y))) the-global-environment)

仮引数を m から $cons に変えた。

以上、とりあえずリストの car だけ表示させるつもりで作ったのですが、全然動きません、、、orz、疲れたので 退却~。(^^ゞ

SICP 4.2.2 Ex. 4.27 Ex. 4.28 Ex. 4.29 Ex. 4.30 Ex. 4.31

  • 投稿日:
  • カテゴリ:

Ex. 4.27

(define w (id (id 10)))
;;; L-Eval input:
count
;;; L-Eval value:
<応答> 1

define は関数でなく、構文である。従って引数を遅延したりしない。(id (id 10))は即座に評価され、結果がwに割り付けられる。

で、(id (id 10)) の評価だが、今度は id が関数なので引数は遅延される。つまり内側の (id 10) はthunk化されるだけで実行されない、外側の id は、この thunk を引数に貰って実行される。

外側のid の実行時に count が +1 される。また実行結果は (thunk (id 10)) である。これが w に割り付けられる。

;;; L-Eval input:
w
;;; L-Eval value:
<応答> 10
;;; L-Eval input:
count
;;; L-Eval value:
<応答> 2

w に割り付けられた値 (thunk (id 10)) は、画面表示の際に force されるので、(id 10)の実行結果 10 が表示される。このとき、count は +1する。

Ex. 4.28

引数が関数の場合を考慮している。あまりいいサンプルではないが、こんなメタ関数を考える。

(define (f g) (g 1))

f を使ってみる。

(f  (lambda (x) (+ x 1)) )

これを評価すると、引数を遅延して(つまり thunk化して)、f を評価し、 結果こうなる。

((thunk (lambda (x) (+ x 1))) 1)

このように、引数が関数の場合、演算子に thunk がつく。これを評価すると、apply でこける。こけないように、演算子の thunk をとりたいのだが、eval では thunk をとることはできない。それで、actual-value を使って thunk を取っている。

Ex. 4.29

ありきたりだが、n! を計算する関数を考えてみる

(define (factorial n)
  (if (= n 0)
      1
      (* n (factorial (- n 1))))))

引数 n が関数の本体の3箇所で使われている。 引数 n は3回計算されるのである。もし n がメモ化されているなら、 3回のうち2回はメモで済むことになり、実行が早くなる。

実際 (factorial 3) を実行した場合の展開の様子を追ってみると

(factorial 3)
   ↓
...
   ↓
(* 3 (factorial (- 3 1)))
   ↓
(* 3 
   (if (= (thunk (- 3 1)) 0)
       1
       (* (thunk (- 3 1)) (factorial (- (thunk (- 3 1)) 1))))
   ↓
...

となる。太字はもともと1つの引数であり、同一のオブジェクトである。 これが、3箇所に現れて計算されている。メモ化されているなら計算の手間を2回省くことができる。(正確には引数3もthunk化されるが、説明のため(- 3 1)だけthunk化した)

注意すべきは、関数の返り値 (factorial 2) とかがメモ化されるということではないことである。メモ化機能は、thunk に関する機能であり、thunk 化されるのは関数の引数だけである。結局、メモによる省力化は、関数の引数に効くのである。

次の問題

(define (square x)
  (* x x))
;;; L-Eval input:
(square (id 10))
;;; L-Eval value:
<応答> 100  ← メモ化しても、しなくても同じ

;;; L-Eval input:
count
;;; L-Eval value:
<応答> 1 ← メモ化
<応答> 2 ← メモ化しないとき

最初のsquare の評価結果は、

(* (thunk (id 10))  (thunk (id 10)))

となる。太字は、2箇所に現れているが、元々1つの引数だったので、同一のオブジェクトである。* は基本手続きなので、引数は直ちに force され、結果が掛け算される。

メモ化してる場合、
左の(thunk (id 10)) は実行されて10が返る。このときcount + 1 を実行する。
右の(thunk (id 10)) はメモの10が返る。このとき count は変わらない 。

メモ化しない場合、
左の(thunk (id 10)) は実行されて10が返る。このときcount + 1 を実行する。
右の(thunk (id 10)) は実行されて10が返る。このときcount + 1 を実行する。

Ex. 4.30

まず Cy さんの心配は何かをハッキリさせる。

(define (f x)
  (g x)
  (h x))

という関数を考えてみる。で、(f 1)を実行すると、

(f 1)
  ↓
( (g (thunk 1))  ← ①
  (h (thunk 1)))  ← ②

となる。返り値は ② である。 返り値②は呼び出し側でforce される可能性があるが、 途中式の①は thunk化されたまま、force される機会はない。

つまり、途中式①が表示や代入の副作用を持っていたとしても、発動する機会がないことになる。 これが Cy さんの心配である。

また Cy さんは、これを改良するために、途中式①を eval ではなく、 actual-value で評価しようと主張している。

a. Benさんのfor-eachを見てみる

(define (for-each proc item)
  (if (null? items)
      'done
      (begin (proc (car items)) ← ①
             (for-each proc (cdr items)))))

begin の中の①式が途中式となっている。この for-each を使った例は、

(for-each (lambda (x) (newline)(display x))
           (list 57 321 88))

となる。これを評価すると、途中式①は

( (lambda (x) (newline)(display x)) (car items) )

となる。評価は再帰的に行われるので、これがさらに eval で評価される。結局

( (newline) (display (thunk (car items))) )

となる。newline は引数がないので、そのまま実行されて改行表示する。 また、display は基本手続きなので、引数は直ちに force されて、結果が表示される。

ここで注意は、遅延評価は引数を遅延(thunk化)するが、関数の本体はすぐに実行するのである。あくまで引数を遅延するだけで、本体の実行を遅延する訳ではない。

付け加えて、関数が基本手続きの場合は、引数の遅延も行わない。むしろ直ちに 引数を force して関数本体を実行する。

ということで、この例では、①は途中式だったのに、 基本関数ということで、うまく force されたのである。

b. 今度は途中式が基本関数でない場合である。
関数 p1 の中の(set! ・・・) が途中式になっている。
関数 p2 の中の e が途中式になっている。

(p1 1)を評価すると

(p1 1)
  ↓
( (set! x (cons x:(thunk 1) '(2))) 
  x )

となる。太字は変な書き方だが、変数 x には (thunk 1) が割り当てられていることを示しているつもりである。

(set! ・・・) は途中式となるが、set! は関数ではなく構文なので、引数を遅延しないし、 さらに、consは基本関数なので、これは引数を force する。結局この途中式は完全に実行されて、実行結果 (1 2) が x に代入される。なので最終的に返される値は

(1 2)

となる。Cyさんの改良を入れた場合も途中式を実行すだけなので同じ結果である。 次に (p2 1) を評価すると、

(p2 1)
  ↓
(p (set! x (cons x:(thunk 1) '(2)))) ← p は引数を遅延する
  ↓
( (thunk (set! x (cons x:(thunk 1) '(2)))) ← 途中式。実行されない。
  x:(thunk 1) )

今回は、Cy さんの心配どおり、途中式が実行されない。x には(thunk 1) が割り当てられたままである。なので、最終的に返される値は

1

となる。Cyさんの改良をいれた場合、途中式が実行されるので、最終的に返される値は

(1 2)

となる。

c. a.の例は、たまたま基本関数 display を使っているので、eval で評価したときも遅延しない。actual-value で評価した場合はもちろん遅延しない、どちらにしても遅延しないので結果は同じになる。

d. Cyさんの動作のほうがまし。 上の例 (p2 1) で (1 2) が返るのが自然。1が返るのは俄かには理解しがたい。

しかし、Cyさんの方法にも難点はあり、結局、どちらの方式も実はあまりうまくない。

本文の方式の難点は、途中式は評価されない、ということをプログラム中に考慮しなければならず難しいということである。

一方Cyさんの方式の難点は、途中式を force するので引数の遅延が破れる。つまり途中式で使われる引数は遅延しない、使われない引数はそのまま遅延される。ということをプログラム中に考慮しなければならず、やっぱりこっちも難しいのである。

ということで、残念ながら、どちらの方式もややこしい。

ここでの例では、(thunk (set! ...)) を引数に渡すことによってこの難点が表面化しているので、引数に (set! xxx) を渡さないようにして、回避するのがいいかもしれない。

Ex. 4.31

関数定義の仮引数に修飾子がついた場合に評価器を対応させる問題である
(なお、この問題は、前問 Ex. 4.30 とは何の関係もない)

考慮すべきは以下の2点。

  1. 仮引数は、環境に登録する際の変数名に使われるので(b lazy)みたいなリストのままでは不味い。
  2. なので、lazy とかは取って、(f a b c d) としてしまうわけであるが、あとで評価するときに lazy などの条件を使いたいので、取った lazy をどこかに保存しておかないといけない

上の2つ目の保存場所であるが、一番修正が少なくなるよう考慮して、procedure 文の中に入れるのがましなように思われる。

元々の procedure 文 はこんな形をしている。

(procedure (x y) ((+ x y)) the-global-environment) 

これを、こんな風に変える。

(procedure (x y) (() lazy) ((+ x y)) the-global-environment) 

なお、この procedure 文を生成する元の関数は

(define (f x (y lazy))
  (+ x y))

である。実装は、まず239頁辺りの遅延のための評価器の修正をした上で、以下のコードを追加する。少し長い、、

;; make procedure
(define (make-procedure parameters body env)
  ;; unmodify parameter ex. (x lazy) -> x
  (define (unmodify a)
    (if (symbol? a) a (car a)))
  ;; take modifier ex. (x lazy) -> lazy
  (define (modifier a)
    (if (symbol? a) '() (cadr a)))
  (let ((p (map unmodify parameters))
        (m (map modifier parameters)))
        ;; procedure format: (procedure (x y) (() lazy) ((* x y)) the-global-environment)
        (list 'procedure p m body env)))

(define (procedure-body p) (cadddr p))

(define (procedure-parameters p) (cadr p))

(define (procedure-environment p) (car (cddddr p)))

(define (procedure-modifier p) (caddr p))

(define (apply procedure arguments env)
  (cond ((primitive-procedure? procedure)
         (apply-primitive-procedures 
          procedure 
          (list-of-arg-values arguments env)))
        ((compound-procedure? procedure)
         (eval-sequence
          (procedure-body procedure)
          (extend-environment
           (procedure-parameters procedure)
           (list-of-delayed-args arguments 
                                 (procedure-modifier procedure) ;; changed
                                 env)
           (procedure-environment procedure))))
        (else
         (error
          "Unknown procedure type -- APPLY" procedure))))

(define (delay-memo-it exp env)
  (list 'thunk-memo exp env))

(define (list-of-delayed-args exps modifier env)
  (if (no-operands? exps)
      '()
      (let ((m (first-operand modifier))
            (o (first-operand exps)))
        (cons (cond ((eq? m 'lazy)
                     (delay-it o env))
                    ((eq? m 'lazy-memo)
                     (delay-memo-it o env))
                    ((null? m)
                     (actual-value o env))
                    (else (error "Unkonwn modifier -- LIST-OF-DELAYED-ARGS" m)))
            (list-of-delayed-args (rest-operands exps)
                                  (rest-operands modifier)
                                  env)))))

(define (thunk-memo? obj)
  (tagged-list? obj 'thunk-memo))

(define (force-it obj)
  (cond ((thunk-memo? obj)
         (let ((result (actual-value
                        (thunk-exp obj)
                        (thunk-env obj))))
           (set-car! obj 'evaluated-thunk)
           (set-car! (cdr obj) result)
           (set-cdr! (cdr obj) '())
           result))
        ((evaluated-thunk? obj)
         (thunk-value obj))
        ((thunk? obj)
         (actual-value (thunk-exp obj)(thunk-env obj)))
        (else obj)))

こんな感じ。

SICP 4.2.1 Ex. 4.25 Ex. 4.26

  • 投稿日:
  • カテゴリ:

Ex. 4.25

作用順序の場合、unless の引数が先に評価される。

(unless false (* 5 (factorial 4)) 1)

なので、5! が返るかと思っていたら、なぜか無限ループしてしまった。

何が悪かったのか見てみると、引数1でfactorial が止まらない。考えてみると

(factorial 1)

は、

(unless true (* 1 (factorial 0)) 1)

となるが、作用順序なので、unless を展開する前に、先に (factorial 0) を実行しようとする。(factorial 0)を実行すると、(factorial -1)が出てきて、それをさらに実行して、、、と全然止まらないのである。

一方、正規順序(遅延評価)の場合、

(factorial 1)

は、

(unless true (* 1 (factorial 0)) 1)

となるが、正規順序なので、(factorial 0)を実行する前に、先に unless が展開される。

(if true
    1
    (* 1 (factorial 0)))

これを実行すると、 if式は、false節を無視して1を返す。(factorial 0)は何もされず捨てられ る。こうして無事ループは停止する。

Ex. 4.26

unless を導出式にする。eval に unless の節を追加して、そこで

(unless c a b)
(if c b a)

にするような置き換え行う。...実装は省略。

unless が関数のままで嬉しい場合。→ メタ関数の引数に使える。map の引数に使えてうれしい。

SICP 4.1.7 Ex. 4.22 Ex. 4.23 Ex. 4.24

  • 投稿日:
  • カテゴリ:

構文解析

関数の構文解析を1度だけ実行し、内部形式にして保存する。PascalのP形式みたいなもの。関数の呼び出しは、内部形式のほうを呼ぶ。解析の手間が減って速くなる。
では、Schemeの関数の内部形式はどんな形をしているのか?どこに保存しているのか?残念ながら、この章ではこれらの具体的な説明はない。ここでは、子Schemeの内部形式は、親Schemeの内部形式をそのまま流用しているだけである。

Ex. 4.22

let の導入。(内部define の対応、名前つき let などの変更する前のシンプルな let)

まず analyze の変更

(define (analyze exp)
  (cond ((self-evaluating? exp) 
        ...
        ((if? exp)(analyze-if exp))
        ((let? exp)(analyze-let exp))  ;; let 節追加
        ...

追加した関数

(define (analyze-let exp)
  (let ((bindings (cadr exp))
        (body (cddr exp)))
    (analyze (cons (cons 'lambda 
                         (cons (map car bindings) body))
                   (map cadr bindings)))))

Ex. 4.23

並びが一つの式 exp1 の場合、Alyssa の analyze-sequence の返す値は以下のとおり。

(lambda (env) (execute-sequence exp1 env))

このうち、先頭の lambda と exp1 は構文解析済みになる。しかし、execute-sequence 関数の部分は構文解析されない。そのまま、lambda のなかに保存される。lambda の本体は解析されないからである。ということで未解析の部分式が残ってしまう。(解析と評価をごっちゃにしないこと。)

同じく、本文中の analyze-sequence の返す値は以下のとおり。

exp1

exp1 は構文解析済みである。

並びが2つの式 exp1, exp2 の場合、Alyssa の analyze-sequence の返す値は以下のとおり。

(lambda (env) (execute-sequence (exp1 exp2) env))

このうち、先頭の lambda と exp1 と exp2 は構文解析済みになる。が、execute-sequence 関数の部分が構文解析されないまま残り、lambda のなかに保存される。

同じく、本文中の analyze-sequence の返す値は以下のとおり。

(lambda (env) (exp1 env) (exp2 env))

exp1, exp2 と先頭の lambda は構文解析済みになる。式は、部分式も含めて、全て解析済みになる。

Ex. 4.24

1~n の和を求める関数を使って速度を比べる

(define (f n)
  (if (= n 1)
      1
      (+ (f (- n 1)) n)))

(f 1000000) の実行時間

内部形式使わない版 15秒

内部形式使う版 10秒

すこしだけ、内部形式を使うと速くなる

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 はそれを利用してるだけと言ったほうがいいかもしれない)

SICP 4.1.5 Ex. 4.15

  • 投稿日:
  • カテゴリ:

Ex. 4.15

証明
(1) (halts? try try) が true ならば、(try try) は無限ループする。これは、(halts? try try)がtrueということと矛盾する。
(2) (halts? try try) が false ならば、(try try) は停止する。これは、(halts? try try)がfalseということと矛盾する。
なので、このような halts? を書くことは不可能である。

halts?が書けないということの意味は、プログラムが無限ループするか、しないかは各々のプログラムごとに地道に判定するしかないということ。

あと、もう一つ教訓がある。それは、halts? のように仕様がはっきりしていても、プログラム不可能な場合があるということ。

...しかし、"任意のpについて...正しく決める" なんていう仕様が、 はっきりした仕様といえるかどうかは疑問ではある。

あと実際の場では、メモリ制限や速度制限でプログラムできないのというは、なんとなく気付くが、論理的にプログラム不可能というのはちょっと気付かないかもしれない。

SICP 4.1.4 Ex. 4.14

  • 投稿日:
  • カテゴリ:

Ex. 4.14

car や cdr と同じように、基本手続きに map を追加する

(define primitive-procedures
  (list (list 'car car)
        (list 'cdr cdr)
        (list 'map map)
        ...

で、子Schemeで、map を使ってみる。

(map square '(2 3))

map は基本手続きなので、apply-primitive-procedure で処理されて、、、結局

(apply-in-underlying-scheme map '(square (2 3)))

という形で親Schemeに渡される。 map の2項以下が、square もろとも1つの引数扱いになっている。 もちろん親Schemeは、こんな map の構文は許していないのでエラーになる。

なお、親Schemeで許されている map の構文は

(apply-in-underlying-scheme map square '(2 3))

である。apply-primitive-procedure を改造して map のときだけうまく調整すれば、修正可能かもしれない。

SICP 4.1.3 Ex. 4.11 Ex. 4.12 Ex. 4.13

  • 投稿日:
  • カテゴリ:

Ex. 4.11

環境変数のフォーマット(p.225)は、例えばこんな感じ

(((x y z) (1 2 3)))

これを、こんな感じに変える

(((x 1)(y 2)(z 3)))

実装、、、無理。省略(^^ゞ

Ex. 4.12

親環境、子環境がある場合の環境変数のフォーマットは、こんな感じ

(  ((a b)(10 20))  ((x y z)(1 2 3))  )

向かって右側が親環境であり、左側が子環境である。
従って、環境変数の検索は、まず子環境、親環境と各環境をスキャンする必要がある。
さらに、子環境で、変数 a,b を、親環境で変数 x, y, z と各変数をスキャンする必要がある。
2重スキャンである。この2重スキャンの部分を関数化しろというのが、ここの設問である。

(define (search-env var env)
  (define (env-loop env)   ;;環境のループ
    (define (scan vars vals)  ;; 変数のループ
      (cond ((null? vars)
             (env-loop (enclosing-environment env)))
            ((eq? var (car vars))
             vals)
            (else (scan (cdr vars)(cdr vals)))))
    (if (eq? env the-empty-environment)
        '()
        (let (frame (first-frame env))
          (scan (frame-variables frame)
                (frame-values frame)))))
  (env-loop env))

これを使って、lookup-variable-value を定義する

(define (lookup-variable-value var env)
  (let ((vals (search-env var)))
    (if (null? vals)
        (error "Unbound variable" var)
        (car vals))))

Ex. 4.13

省略。
ところで、unbind! は、特殊形式でないとダメなんだろうか。

(unbind! 'x env)

という感じで、関数でもいいような気が、、、

Ex. 4.2

Louiseの主張は、evalを、このようにすること。

(define (eval exp env)
  (cond ((application? exp)  ← application の節を先頭にする
           (apply (actual-value (operator exp) env)
                  (operands exp)
                  env))
        ((self-evaluating? exp) exp)
        ...

よく使う節を先頭にして効率を上げるというのは正しい。が、それ以前にこれには不具合がある。

a. (define x 3) を評価すると、application? の結果が true になり、関数として評価されてしまう。

b. application? がちゃんと関数呼び出しを区別できるように、関数呼び出しには目印をつけることにする。

(call + 2 3)

とか。なので、application? をこう変える。

(define (application? exp) (tagged-list? exp 'call))

Ex. 4.3

データ主導流 → 関数表を作って処理の振り分けする(p.108)
省略...

Ex. 4.4

and と or を構文形式として導入する。
まず、eval の変更

(define (eval exp env)
  (cond ((self-evaluating? exp) exp)
        ...
        ((cond? exp)(eval (cond->if exp) env))
        ((and? exp)(eval-and exp env)) ← この辺に and の処理追加
        ((or? exp)(eval-or exp env)) ← この辺に or の処理追加
        ...

追加した関数はこんな感じ。

(define (and? exp) (tagged-list? exp 'and))

(define (or? exp) (tagged-list? exp 'or))

(define (eval-and exps env)
  (cond ((null? exps) 'true)
        ((last-exp? exps) (eval (first-exp exps) env))
        (else (if (not (true? (eval (first-exp exps) env)))
                  'false
                  (eval-and (rest-exps exps) env)))))

(define (eval-or exps env)
  (cond ((null? exps) 'false)
        (else (let ((a (eval (first-exp exps) env)))
                (if (true? a)
                    a
                    (eval-or (rest-exps exps) env))))))

次に、and と or を導出式として処理する方法。
if形式で置き換える。
and の置き換えは、こんな感じ。

(and a b)
(if (a)
    (if (b)
        'true
        'false)
    'false)

or の置き換え

(or a b)
(if (a)
    'true
    (if (b)
        'true
        'false))

実装は省略 (^^;

Ex. 4.5

cond の変な節。どこで使うのか?
expand-clauses を変更

(define (expand-clauses clauses)
  (if (null? clauses)
      'false
      (let ((first (car clauses))
            (rest (cdr clauses)))
        (if (cond-else-clause? first)
            (if (null? rest)
                (sequence->exp (cond-actions first))
                (error "ELSE clause isn't last -- COND->IF"
                       clauses))
            (let ((actions (cond-actions first)))  ;; make-if の前に=>の処理を追加した
              (if (and (= 2 (length actions))
                       (eq? '=> (car actions)))
                  (eval (cadr actions) env)
                  (make-if (cond-predicate first)
                           (sequence->exp (cond-actions first))
                           (expand-clause rest))))))))

Ex. 4.6

let形式の導入。方法は、letをlambdaに変換するということで、、、
lambdaへの変換の方法は、let の変数を lambda の仮引数にし、各値は lambdaを呼び出すときの引数にするという感じ。
まず、evalの変更点。

(define (eval exp env)
  (cond ((self-evaluating? exp) exp)
        ...
        ((cond? exp)(eval (cond->if exp) env))
        ((and? exp)(eval-and exp env))
        ((or? exp)(eval-or exp env))
        ((let? exp)(eval (let->combination exp) env)) ← この辺に let 節追加
        ...

他の追加関数は、以下の通り。

(define (let? exp) (tagged-list? exp 'let))

(define (let->combination exp)
  (let ((bindings (cadr exp))
        (body (cddr exp)))
    (list (cons 'lambda 
                (cons (map car bindings)
                      body))
          (map cadr bindings))))

Ex. 4.7

let*形式の導入。方針は、let* を let の入れ子に変換するということで、、、
入れ子にするのは、束縛の順番を指定するため。
変換は、こんな感じ

(let* ((x 1)(y 2)) (+ x y))
(let ((x 1))
  (let ((y 2))
    (+ x y)))

eval の変更点

(define (eval exp env)
  (cond ((self-evaluating? exp) exp)
        ...
        ((let? exp)(eval (let->combination exp) env)) 
        ((let*? exp)(eval (let*->nested-lets exp) env)) ← この辺に let* 節追加
        ...

他に追加した関数

(define (let*? exp) (tagged-list? exp 'let*))

(define (let*->nested-lets exp)
  (let ((bindings (cadr exp))
        (body (caddr exp)))
    (define (make-nest b)
      (if (null? b)
          body
          (list 'let (list (car b)) (make-nest (cdr b)))))
    (make-nest bindings)))

問題の最後の "...節を追加するだけで十分か..."云々という質問について。
質問の意図は、let*節は、let* を let に変換したまま放っておいていいのか? let をさらに lambda にする責任があるのではないのか?ということ
evalは再帰的に適用されるので、この質問にたいしては、"OK"という解答である。

Ex. 4.8

名前つき let

(define (let->combination exp)
  (if (list? (cadr exp)) ;; case that there is no name 
      (let ((bindings (cadr exp))
            (body (cddr exp)))
        (cons (cons 'lambda 
                    (cons (map car bindings)
                          body))
              (map cadr bindings)))
      (let ((var (cadr exp))  ;; ここ以下を追加
            (bindings (caddr exp))
            (body (cdddr exp)))
        (list 'let '() (list 'define (cons var (map car bindings)) body) ;; bug?
              (cons var (map cadr bindings))))))

バグってるっぽいが...

Ex. 4.9

for形式の設計。こんな感じ

(for 1 10 f)

1~10までの繰り返し。f は1変数関数。
これを、こんな風に置き換える

(let ()
  (define (iter n)
    (if (not (= n 10))
        (begin (f n) (iter (+ n 1)))))
  (iter 1))

実装は、、、省略。(^^ゞ

Ex. 4.10

データ抽象とは、データを作ったり、操作するのに、直接cons, car, cdrを使わない。 構成子、選択子という関数を介してアクセスすること。(p.46)
ここのプログラムに関して言えば、例えば、if式の判定を

(define (eval exp env)
  (cond ((self-evaluating? exp) exp)
        ...
        ((if? exp)(eval-if exp env))
        ...

としていること。もしこれが、

(define (eval exp env)
  (cond ((self-evaluating? exp) exp)
        ...
        ((eq? 'if (car exp))(eval-if exp env))
        ...

となっていたら、データ抽象とは言えない。
if式の構文を変更して、FORTRAN風の

"if x then a else b"

という文字列にしたとしても、データ抽象を使っていれば、eval と apply は変更不要である。
変更するのは、選択子だけ。こんな感じ。

(define (if? exp)
  (equal? (substring exp 1 2) "if"))

(define (if-predicate exp)
  (substring 3 (string-scan exp "then")))

(define (if-consequent exp)
  (substring (string-scan exp "then") (string-scan exp "else")))

(define (if-alternative exp)
  (substring (string-scan exp "else")))

余計なことを言うなら、この章では、Scheme で Scheme を作っているだけなので、データ抽象の恩恵はない。オーバースペックである。むしろデータ抽象なしのほうが、コードの量が減ってうれしいかも。

SICP 4.1.1 Ex. 4.1

  • 投稿日:
  • カテゴリ:

Ex. 4.1

実行順序を指定できるのは、begin しかないので、これを使う。

(define (list-of-values exps env)
  (if (no-operands? exps)
      '()
      (begin (set! a (eval (first-operand exps) env))
             (set! b (list-of-values (rest-operands exps) env))
             (cons a b))))