[ 目次, 前節, 次節, 索引 ]

5.4.4 評価の実行



積極制御評価器の実装をもって, 1章で始めた展開の最後に来た. その展開で, 評価プロセスの次第に精密なモデルを研究した. 比較的略式の置換えモデルで始め, 次にこれを3章で状態と変更の扱える環境モデルに拡張した. 4章の超循環評価器では, 式の評価中に構成された環境構造を更に明確にする言語としてScheme自身を使った. ここではレジスタ計算機を使い, 評価器の記憶管理, 引数渡しと制御の機構を詳しく見た. 記述の新しいレベルのそれぞれで, それ以前の詳しさを欠く評価の扱いでは不鮮明であったものに論点を立て, 曖昧さを解決して来た. 積極制御評価器の振舞いを理解するには, それをシミュレートし, その性能を監視することが出来る.

   われわれの評価器計算機に, 駆動ループを組み込もう. これは4.1.4節のdriver-loop手続きの役を果す. 評価器は繰り返し促進記号を印字し, 式を読み込み, eval-dispatchへ行って式を評価し, 結果を印字する. 次の命令は積極制御評価器の制御列の始めの部分を形成する:29



read-eval-print-loop
  (perform (op initialize-stack))
  (perform
   (op prompt-for-input) (const ";;; EC-Eval input:"))
  (assign exp (op read))
  (assign env (op get-global-environment))
  (assign continue (label print-result))
  (goto (label eval-dispatch))

print-result
  (perform
   (op announce-output) (const ";;; EC-Eval value:"))
  (perform (op user-print) (reg val))
  (goto (label read-eval-print-loop))

   手続きの中で(apply-dispatchの示す「unknown procedure type error」のような)エラーを見つけたら, エラーメッセージを印字し駆動ループへ戻る.30


unknown-expression-type
  (assign val (const unknown-expression-type-error))
  (goto (label signal-error))


unknown-procedure-type
  (restore continue)    ; スタックを清掃する (apply-dispatchから)
  (assign val (const unknown-procedure-type-error))
  (goto (label signal-error))


signal-error
  (perform (op user-print) (reg val))
  (goto (label read-eval-print-loop))

(未定義変数のような)エラーが評価を中断した後, スタックは空ではないかも知れず, シミュレーションのためには駆動ループで毎回スタックを初期化する.31

   5.4.1--5.4.4節で述べたプログラムの断片をすべて組み合せると, 5.2節のレジスタ計算機シミュレータを使って走らせることの出来る評価器計算機モデルが作れる.

(define eceval
  (make-machine
   '(exp env val proc argl continue unev)
   eceval-operations
  '(
    read-eval-print-loop
      ⟨前述した計算機の全評価器⟩
   )))
評価器が基本手続きとして使う演算をシミュレートするScheme手続きを定義しなければならない. これらは4.1節の超循環評価器で使ったのと同じ手続きに, 5.4節の脚注で定義した多少の手続きを追加したものである.
(define eceval-operations
  (list (list 'self-evaluating? self-evaluating?)
        ⟨積極制御評価器 [eceval] 計算機の演算の全リスト⟩))

   最後に大域環境を初期化し, 評価器を走らせる.

(define the-global-environment (setup-environment))

(start eceval)

;;; EC-Eval input:
(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)

   もちろん式のこのような評価は, 多層レベルのシミュレーションをやっているので, Schemeに直接入力したのと比べると, 非常に時間がかかる. われわれの式は積極制御評価器計算機で評価され, それはSchemeプログラムで評価され, それ自身はまたScheme解釈系で評価される.

評価器の性能監視
シミュレーションは評価器の実装を手引きする強力な道具である. シミュレーションは, レジスタ計算機の各種の設計の検討だけではなく, シミュレートした評価器の性能の監視をも容易にする. 例えば性能の重要な要因の一つは, 評価器がスタックをいかに効率よく使うかにある. スタック使用の統計量(5.2.4節)を集めるシミュレータを持つレジスタ計算機評価器の版を定義し, 評価器のprint-resultの入り口にその統計量を印字する命令を追加して, いろいろな式を評価するのに必要なスタック演算の回数を見ることが出来る:

print-result
  (perform (op print-stack-statistics)); 命令を追加
  (perform
   (op announce-output) (const ";;; EC-Eval value:"))
   ... ; 前と同様
評価器との対話はこのようになろう:
;;; 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
印字する統計量が直前の式の評価に使われたスタック演算だけを示すよう, 評価器の駆動ループは, 各対話の開始時にスタックを初期化することに注意しよう.

問題 5.26


監視つきスタックを使い, 評価器の末尾再帰的特性(5.4.2節)を検討せよ. 評価器を実行開始させ, 1.2.1節の反復的factorial手続きを定義せよ:
(define (factorial n)
  (define (iter product counter)
    (if (> counter n)
        product
        (iter (* counter product)
              (+ counter 1))))
  (iter 1 1))
いくつかの小さいnの値につき, 手続きを走らせよ. 各値のn!の計算に必要なスタックの最大深さとプッシュの回数を記録せよ.

a. n!の評価に必要な最大深さはnに無関係であることが分るであろう. その深さは何か.

b. データからn ≥ 1なるn!の評価に使ったプッシュ演算の総数のnに関する式を決めよ. 使った演算の数はnの線形関数で, 二つの定数で決ることに注意せよ.

問題 5.27


問題5.26と比較するため, 階乗を再帰的に計算する次の手続きの振舞いを検討せよ:
(define (factorial n)
  (if (= n 1)
      1
      (* (factorial (- n 1)) n)))
この手続きを監視つきスタックで走らせ, n ≥ 1なるn!の評価に必要なスタックの最大深さとプッシュの総数をnの関数として決めよ. (この関数も線形である.) 次の表をnを使った適当な式で埋めて, 実験を総括せよ:

最大深さは評価器が計算を実行するのに使うスペースの量の大きさで, プッシュの回数は必要な時間とよい相関がある.

問題 5.28


5.4.2節に述べたようにev-sequenceを変更して評価器の定義を修正し, 評価器が最早末尾再帰的でないようにせよ. 問題5.26と5.27を再び走らせ, 両方の版のfactorial手続きが今回は入力に線形に成長するスペースを必要とすることを示せ.

問題 5.29


木構造再帰のFibonacci計算:

(define (fib n)
  (if (< n 2)
      n
      (+ (fib (- n 1)) (fib (- n 2)))))
のスタック演算を監視せよ.

a. n ≥ 2なるFib(n)を計算するのに必要なスタックの最大深さの, n を使った式を与えよ. ヒント: 1.2.2節ではこのプロセスの使うスペースはn に線形に成長することを論じた.

b. n ≥ 2なるFib(n)を計算するのに使うプッシュの総数の式を与えよ. プッシュの数(これは使う時間とよく相関する.)はnに指数的に成長することを見つけなければならない. ヒント: S(n)をFib(n)を計算するのに使うプッシュの数とする. S(n-1), S(n-2)とnに無関係の一定の「オーバーヘッド」定数k を使ったS(n)を表す式があることを論じなければならない. その式を書き, kが何かを述べよ. 次にS(n)はa Fib(n+1) + bで表せることを示し, abの値を与えよ.

問題 5.30


われわれの評価器は, 今は二種類のエラー---式の型の不明と手続きの型の不明---だけを捕えて知らせる. 他のエラーはわれわれを評価器の読込み-評価-印字のループの外へ連れ出す. レジスタ計算機シミュレータを使って評価器を走らせると, それらのエラーは基盤のSchemeシステムで捕えられる. これは利用者のプログラムがエラーを犯した時の計算機のクラッシュに似ている.32 実際のエラーシステムを働くようにするのは大プロジェクトであるが, 何が関っているか理解するのは努力のし甲斐がある.

a. 未束縛変数へのアクセスの試みのような評価プロセスで起きるエラーは, 探索演算を変更し利用者の変数の値になり得ない特別な条件コードを返すようにすれば捕えることが出来る. 評価器はこの条件コードをテストし, signal-errorに行くのに必要な作業を行うことが出来る. 評価器でこういう変更が必要な場所をすべて見つけ, それを修正せよ. 相当な仕事である.

b. 更に具合が悪いのは, 零で割ろうとしたとか, 記号のcarをとろうとしたのような, 基本手続きの作用で生じるエラーを扱う問題である. 職人気質に書いた高品質システムでは, 各基本的作用は, 基本演算の一部として安全性を調べる. 例えばcarの呼出しでは, いつもまず引数が対であることを調べる. 引数が対でなければ, 作用は特別な条件コードを評価器に返し, 評価器は失敗を報告する. われわれのレジスタ計算機シミュレータを, 各基本手続きが作用可能性を調べ, 失敗なら特別な条件コードを適切に返すようにし, このような準備が出来る. そうすれば評価器のprimitive-applyは, 条件コードを調べ, 必要ならsignal-error へ行くことが出来る. この構造を構築し, 働くようにせよ. 大プロジェクトである.



29 ここではread や各種の印字演算は基本的な機械演算として使用可能と仮定する. この仮定はシミュレーションには有用だが, 実際的には現実的ではない. これらは非常に複雑な演算である. 実際には, 一文字を外部装置とやりとりする低レベル入出力演算を使って実装する.

get-global-environment演算を実現するため

(define the-global-environment (setup-environment))

(define (get-global-environment)
  the-global-environment)
を定義する.

30 解釈系に扱って欲しい他のエラーもあるが, それらはさほど単純ではない. 問題5.30参照.

31 スタックの初期化をエラーの後でだけ実行することは出来るが, 次に述べるように, 評価器の性能監視には駆動ループで行う方が便利である.

32 残念なことにこれは Cのような普通の翻訳系中心の言語システムでは通常の出来事である. UNIXTMではシステムは「コアをダンプし」, DOS/WindowsTMではそれは精神分裂になる. マッキントッシュTMでは破裂する爆弾の絵が現れ---幸運に恵まれれば---計算機を再び立ち上げる機会が与えられる.

[ 目次, 前節, 次節, 索引 ]