プログラミング再入門

プログラミングをもう一度ちゃんと勉強する読書ノート

SICP 5.4.4 Running the Evaluator

ノート

ここに書いてあるドライバーループは既に実装として使っている。しかもExcersese 5.25では少し変更もしている。
Exercise 5.25の結果でもちゃんと例は動作する。

(define (append x y)
  (if (null? x)
      y
      (cons (car x)
            (append (cdr x) y))))

;;; EC-Eval value:
ok

;;; EC-Eval input:
(append '(a b c) '(d e f))

;;; EC-Eval value:
(a b c d e f)

;;; EC-Eval input:

ここでは、Racketで実装されたSchemeインタープリータでSchemeプログラムを実行しているので、Racketに直接プログラムを実行させるのよりはパーフォーマンスが劣る。

Monitoring the performance of the evaluator

テキストの通りスタックの状況をプリントしてみる。

;;; EC-Eval input:
(define (factorial n)
  (if (= n 1)
      1
      (* (factorial (- n 1)) n)))

(total-pushes = 3 maximum-depth = 3)
;;; EC-Eval value:
ok

;;; EC-Eval input:
(factorial 5)

(total-pushes = 144 maximum-depth = 28)
;;; EC-Eval value:
120

;;; EC-Eval input:
Exercise 5.26
n n! total-pushes maximum-depth
3 6 134 10
4 24 169 10
5 120 204 10
6 720 239 10

a)
と言う訳で、maximum-depthは常に10。

b)
3!と4!の差が35、4!と5!の差も35、以下同文。また、35×3=105なので134との差は29。と言う訳で
total-pushes=35n+29

Exercise 5.27
n n! total-pushes maximum-depth
3 6 80 18
4 24 112 23
5 120 144 28
6 720 176 33

プッシュ回数は32ずつ、深さは5ずつ増えていく。

実装方法 maximum-depth toal-pushes
再帰 5n+3 32n+16
反復 10 35n+29
Exercise 5.28

再帰的実装から:

n n! total-pushes maximum-depth
3 6 86 27
4 24 120 35
5 120 154 43
6 720 188 51

次に反復的実装。

n n! total-pushes maximum-depth
3 6 144 23
4 24 181 26
5 120 218 29
6 720 255 32
実装方法 maximum-depth toal-pushes
再帰 8n+3 34n-16
反復 3n+14 37n+33
Exercise 5.29
n fib(n) total-pushes maximum-depth
2 1 72 13
3 2 128 18
4 3 240 23
5 5 408 28
6 8 688 33

a)
5ずつ増えて、n=0を仮定すると3になるので
maximum-depth=5n+3

b)
S(4)=240、S(3)=128、S(2)=72から考えるとS(4)=S(3)+S(2)+40。
この式からS(5)を予想すると、S(5)=240+128+40=408。合ってる。
S(6)を予想すると、S(6)=408+240+40=688。合っていそう。
と言う訳で、
 k=40
S(n)=S(n-1)+S(n-2)+40

S(2)をa Fib(3)+bで表す。72=2a+b。S(3)から128=3a+b。
連立方程式を解くとa=56、b=-40。つまりS(n)=56 Fib(n+1)-40
S(4)を予測すると、56 \times Fib(5)-40=56 \times 5 - 40=240
S(5)を予測すると、56 \times Fib(6)-40=56 \times 8 - 40=408

Exercise 5.30

a)
インタープリタのコードがエラーで止まる箇所は以下の通り。

make-register/dispatch レジスタに対する未定義のメッセージを送った
make-stack/pop 既にからのスタックからポップしようとした
make-stack/dispatch スタックに対する未定義のメッセージを送った
make-new-machine/allocate-register 既に定義されたレジスタ名を再定義しようとした
make-new-machine/dispatch マシンに対する未定義のメッセージを送った
lookup-label 未定義のラベルを参照した
make-execution-procedure 未定義の命令を実行しようとした
make-test test命令の文法が間違っている
make-branch branch命令の文法が間違っている
make-goto goto命令の文法が間違っている
make-perform perform命令の文法が間違っている
make-primitive-exp プリミティブ命令の文法が間違っている
lookup-prim 未定義のプリミティブを実行しようとした
extend-evaluation 環境に登録する名前と値の数が一致しない
lookup-variable-value 未定義の名前を参照しようとした
set-variable-value 未定義の名前の値を変更しようとした

lookup-variable-valueでのエラーだけはユーザーの入力によって起こり得るが、それ以外は全てインタープリタのコードのバグでしか起こり得ない。インタープリタのバグの場合はREPLから抜けるのは適切な対応と言えるので、対応すべきはlookup-variable-valueでのエラーへの対処のみと言って良い。

まずはlookup-variable-valueがerrorで停止している部分を改造。
Racketのdictionaryを使った現在の実装。

(define (lookup-variable-value var env)
  (define (env-loop env)
    (if (eq? env the-empty-environment)
        (error "Unbound variable" var)
        (dict-ref (first-frame env) var (lambda () (env-loop (enclosing-environment env))))))
  (env-loop env))

変数の値としては何でも取り得るので、Maybe型の様な(タグ+値)と言うタプルを返したい所。そうすると変数が見つかった時に直ぐに戻ってしまう今の形では無理。これを以下の様に変更して、更に補助関数も用意。

(define (lookup-variable-value var env)
  (define (env-loop env)
    (cond ((eq? env the-empty-environment) (cons 'unbound '()))
          ((dict-has-key? (first-frame env) var) (cons 'bound (dict-ref (first-frame env) var)))
          (else (env-loop (enclosing-environment env)))))
  (env-loop env))
(define (bound? result)
  (eq? (car result) 'bound))
(define (bound-value result)
  (cdr result))

動作確認

> (lookup-variable-value 'x (list #hash((x . 1) (y . 2)) #hash((a . "a") (b . "b")) '()))
'(bound . 1)
> (lookup-variable-value 'z (list #hash((x . 1) (y . 2)) #hash((a . "a") (b . "b")) '()))
'(unbound)
> (lookup-variable-value 'b (list #hash((x . 1) (y . 2)) #hash((a . "a") (b . "b")) '()))
'(bound . "b")
> (bound? (lookup-variable-value 'x (list #hash((x . 1) (y . 2)) #hash((a . "a") (b . "b")) '())))
#t
> (bound? (lookup-variable-value 'z (list #hash((x . 1) (y . 2)) #hash((a . "a") (b . "b")) '())))
#f
> (bound-value (lookup-variable-value 'x (list #hash((x . 1) (y . 2)) #hash((a . "a") (b . "b")) '())))
1
> (bound-value (lookup-variable-value 'b (list #hash((x . 1) (y . 2)) #hash((a . "a") (b . "b")) '())))
"b"
> 

補助関数をオペレータとして追加

(define eceval-operations
  (list (list 'self-evaluating? self-evaluating?)
        
        (list 'bound? bound?)
        (list 'bound-value bound-value)
        ))

ev-variableの部分は以下のように変更

     ev-variable
     (assign val (op lookup-variable-value) (reg exp) (reg env))
     (test (op bound?) (reg val))
     (branch (label bound-variable))
     (goto (label unbound-variable))
     bound-variable
     (assign val (op bound-value) (reg val))
     (goto (reg continue))

エラー処理部分は

     unbound-variable
     (assign val (const unbound-variable-error))
     (goto (label signal-error))

動作させてみる。

;;; EC-Eval input:
(define x 1)

;;; EC-Eval value:
ok

;;; EC-Eval input:
x

;;; EC-Eval value:
1

;;; EC-Eval input:
y
unbound-variable-error

;;; EC-Eval input:

REPLに戻っている。

b)

;;; EC-Eval input:
(/ 1 0)
. . /: division by zero
> 

現状ではプリミティブのエラーはインタープリタの実行が止まってしまう。

ベースのSchemeとして使用しているRacketではwith-handlersと言う関数を使って例外処理ができるので、プリミティブのエラーは例外を補足する事で取り敢えず実現できる。

> (with-handlers ([exn:fail? (lambda (exn) exn)]) (/ 1 0))
(exn:fail:contract:divide-by-zero "/: division by zero" #<continuation-mark-set>)
> 

これを使ってapply-primitive-procedureからwith-handlersを呼び出す様に変更する。結果はタプルで成否を示す#t/#fと、成功した時には戻り値を、失敗のときには飛ばされた例外のペアにする。またこのタプルにアクセスする補助関数を用意する。

(define (apply-primitive-procedure proc args)
  (with-handlers ([exn:fail? (lambda (exn) (cons #f exn))])
    (cons #t (apply-in-underlying-scheme (primitive-implementation proc) args))))
(define (primitive-apply-succeeded? result)
  (car result))
(define (primitive-apply-result result)
  (cdr result))



(define eceval-operations
  (list (list 'self-evaluating? self-evaluating?)
        
        (list 'primitive-apply-succeeded? primitive-apply-succeeded?)
        (list 'primitive-apply-result primitive-apply-result)
        ))

primitive-applyはapply-primitive-procedureを呼び出した後に成否をチェックする。

     primitive-apply
     (assign val (op apply-primitive-procedure)
             (reg proc)
             (reg argl))
     (test (op primitive-apply-succeeded?) (reg val))
     (branch (label primitive-apply-succeeded))
     (goto (label primitive-apply-failed))
     primitive-apply-succeeded
     (assign val (op primitive-apply-result) (reg val))
     (restore continue)
     (goto (reg continue))

エラーハンドリングでも投げられた例外を表示するようにする。

     primitive-apply-failed
     (assign val (op primitive-apply-result) (reg val))
     (goto (label signal-error))

実行してみる。

;;; EC-Eval input:
(/ 10 5)

;;; EC-Eval value:
2

;;; EC-Eval input:
(/ 1 0)
#(struct:exn:fail:contract:divide-by-zero /: division by zero #<continuation-mark-set>)

;;; EC-Eval input:
(car 'a)
#(struct:exn:fail:contract car: contract violation
  expected: pair?
  given: 'a #<continuation-mark-set>)

;;; EC-Eval input:
(cons 'a)
#(struct:exn:fail:contract:arity cons: arity mismatch;
 the expected number of arguments does not match the given number
  expected: 2
  given: 1
  arguments...:
   'a #<continuation-mark-set>)

;;; EC-Eval input:

もう少し気の利いた出力が出来ると良いが、取り敢えずこれで目的は達成している。