基本的考えは, 手続きを作用させる時, どの引数は評価し, どれは遅延するかを解釈系が決めなければならないということである. 遅延引数は評価しない; その代りそれは サンク(thunks)というオブジェクトに変換する.34 サンクは式が必要とする時, 作用の時に評価されていたかのように引数の値を生じるのに必要な情報を持っていなければならない. 従ってサンクは引数の式と手続き作用が評価された環境を含んでいなければならない.
サンクの中の式を評価するプロセスを 強制(forcing)という.35 一般にサンクは値が必要になった時にだけ強制される: つまりサンクの値を使おうとする基本手続きに渡された時; 条件式の述語の値である時; 手続きとして作用させられる演算子の値である時である. 可能な選択の一つは, 3.5.1節の遅延オブジェクトでやったようにサンクを メモ化(memoize)するかどうかである. メモ化するとサンクが初めて強制される時, 計算した値を格納する. その後の強制は計算を繰り返すことなく, 単に格納した値を返すだけである. 多くの作用でより効率的であるから, われわれは解釈系にメモ化させよう. しかしそこにはむずかしい考察もある.36
((application? exp) (apply (actual-value (operator exp) env) (operands exp) env))のようになる. これは4.1.1節のevalのapplication?節と殆んど同じである. しかし遅延評価では評価して出来た引数ではなく, 被演算子の式に対してapplyを呼び出す. 引数が遅延させられるなら, サンクを作るための環境が必要になるので, それも渡さなければならない. applyはその型(基本と合成)に振り分けるため, 作用させる実際の手続きが必要なので, 演算子は評価し, それを作用させる.
式の実際の値が必要になった時には, 式の値がサンクならそれを強制するため, 単なるevalの代りに
(define (actual-value exp env) (force-it (eval exp env)))を使う.
applyの新版も4.1.1節のものと殆んど同じである. 相違はevalが評価していない被演算子の式を渡していることである: (ストリクトな)基本手続きに対しては, 基本手続きを作用させる前に引数をすべて評価する; (ノンストリクトな)合成手続きに対しては, 手続きを作用させる前にすべての引数を遅延させる.
(define (apply procedure arguments env) (cond ((primitive-procedure? procedure) (apply-primitive-procedure 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 env) ; 変更した (procedure-environment procedure)))) (else (error "Unknown procedure type -- APPLY" procedure))))引数を処理する手続きは, list-of-delayed-argsが引数を評価する代りに遅延し, list-of-arg-valuesがevalの代りにactual-value を使う点を除き, 4.1.1節のlist-of-valuesにそっくりである:
(define (list-of-arg-values exps env) (if (no-operands? exps) '() (cons (actual-value (first-operand exps) env) (list-of-arg-values (rest-operands exps) env)))) (define (list-of-delayed-args exps env) (if (no-operands? exps) '() (cons (delay-it (first-operand exps) env) (list-of-delayed-args (rest-operands exps) env))))
評価器で変更すべき他の場所はifの扱いである. そこでは述語が真か偽かテストする前に述語の値を得るため, evalの代りにactual-valueを使う必要がある:
(define (eval-if exp env) (if (true? (actual-value (if-predicate exp) env)) (eval (if-consequent exp) env) (eval (if-alternative exp) env)))
最後にdriver-loop手続き(4.1.4節)を変更し, 遅延した値が読込み-評価-印字ループに戻ってきたら, 印字の前に強制されるように, evalの代りにactual-valueを使うようにする. また, これが遅延評価器であることを示すため, 促進記号を変える:
(define input-prompt ";;; L-Eval input:") (define output-prompt ";;; L-Eval value:") (define (driver-loop) (prompt-for-input input-prompt) (let ((input (read))) (let ((output (actual-value input the-global-environment))) (announce-output output-prompt) (user-print output))) (driver-loop))
これらの変更を施すと, 評価器を起動しテストが出来る. 4.2.1節で論じたtry式の評価は成功し, 解釈系が遅延評価を実行していることを示している:
(define the-global-environment (setup-environment)) (driver-loop) ;;; L-Eval input: (define (try a b) (if (= a 0) 1 b)) ;;; L-Eval value: ok ;;; L-Eval input: (try 0 (/ 1 0)) ;;; L-Eval value: 1
(define (force-it obj) (if (thunk? obj) (actual-value (thunk-exp obj) (thunk-env obj)) obj))
式と環境を包み込む簡単な方法の一つは, 式と環境を含むリストを作ることである. そこでサンクを次のように作り出す:
(define (delay-it exp env) (list 'thunk exp env)) (define (thunk? obj) (tagged-list? obj 'thunk)) (define (thunk-exp thunk) (cadr thunk)) (define (thunk-env thunk) (caddr thunk))
実際, 解釈系に欲しいのは, これではなく, メモ化されたサンクである. サンクが強制された時, 格納された式を値に取り替え, 既に評価されたと認識出来るようにthunkのタグを変えて評価されたサンクに転換する. 37
(define (evaluated-thunk? obj) (tagged-list? obj 'evaluated-thunk)) (define (thunk-value evaluated-thunk) (cadr evaluated-thunk)) (define (force-it obj) (cond ((thunk? obj) (let ((result (actual-value (thunk-exp obj) (thunk-env obj)))) (set-car! obj 'evaluated-thunk) (set-car! (cdr obj) result) ; expをその値で置き換える (set-cdr! (cdr obj) '()) ; 不要なenvを忘れる result)) ((evaluated-thunk? obj) (thunk-value obj)) (else obj)))メモ化する, しないの両方で同じdelay-it手続きが働くことに注意しよう.
(define count 0) (define (id x) (set! count (+ count 1)) x)次の対話の例で欠けている値を補い, 答を説明せよ.38
(define w (id (id 10))) ;;; L-Eval input: count ;;; L-Eval value: 〈応答〉 ;;; L-Eval input: w ;;; L-Eval value: 〈応答〉 ;;; L-Eval input: count ;;; L-Eval value: 〈応答〉
(define (square x) (* x x)) ;;; L-Eval input: (square (id 10)) ;;; L-Eval value: 〈応答〉 ;;; L-Eval input: count ;;; L-Eval value: 〈応答〉評価器をメモ化した時としない時との応答を示せ.
(define (eval-sequence exps env) (cond ((last-exp? exps) (eval (first-exp exps) env)) (else (actual-value (first-exp exps) env) (eval-sequence (rest-exps exps) env))))a. Ben BitdiddleはCyは間違っていると考えた. 彼はCyに副作用のある並びの重要な例である問題2.23のfor-each手続きを示した:
(define (for-each proc items) (if (null? items) 'done (begin (proc (car items)) (for-each proc (cdr items)))))彼は本文中の(元々のeval-sequenceを持つ)評価器はこれを正しく扱うと主張した:
;;; L-Eval input: (for-each (lambda (x) (newline) (display x)) (list 57 321 88)) 57 321 88 ;;; L-Eval value: donefor-eachの振舞いについて, Benが正しい理由を述べよ.
(define (p1 x) (set! x (cons x '(2))) x) (define (p2 x) (define (p e) e x) (p (set! x (cons x '(2)))))元々のeval-sequenceでは(p1 1)と(p2 1)の値は何か. eval-sequenceへCyの提案する変更での値は何か.
(define (f a (b lazy) c (d lazy-memo)) ...)はfを四引数の手続きと定義し, 第一と第三引数は手続きが呼び出される時に評価し, 第二引数は遅延し, 第四引数は遅延とメモ化する. こうすると通常の手続き定義は通常のSchemeと同様な振舞いを生じ, それぞれの合成手続きの各パラメタへのlazy-memo宣言の追加は, 本節で定義した遅延評価器の振舞いを生じる. Schemeをそのように拡張するのに必要な変更を設計し, 実装せよ. defineの新しい構文を扱う新しい構文手続きを実装しなければならない. またevalやapplyに引数が遅延される時を判断し, それに従って引数を遅延または強制するように, また強制をメモ化するかどうかも適切に準備しなければならない.
34
サンク(thunk)という用語はAlgol 60の名前呼びの実装を議論していた非公式な作業グループが発明した. 彼らは式(「についての考え(シンク)」)の解析の殆んどは翻訳時に出来ることを知った;
そこで実行時には式はすでに「シンクされて(サンク)」いる
(Ingerman他 1960).
35
これは3章でストリームを表現する時に説明した, 遅延オブジェクトに対する
forceの使い方と類似している. ここでやろうとしていることと, 3章でやったことの重要な違いは, 遅延と強制を評価器に作り込み, これを言語を通じて一様かつ自動的にしようとしていることである.
36
メモ化と組み合せた遅延評価は, 名前呼び(call-by-name)引数渡しに対して,
必要呼び
(call-by-need)引数渡しということがある.
(Algol 60が導入した
名前呼びはメモ化しない遅延評価と似ている.) 言語設計者としてわれわれは評価器をメモ化するようにも, しないようにも, またプログラマの選択にすることにでも出来る(問題4.31). 3章から期待するように, この選択は代入のある場合には, 微妙で混乱した問題を惹き起す. (問題4.27と4.29参照)
Clinger(1982)のすぐれた論文は, ここに生じた混乱の多くを明確化しようとしている.
37
式の値が計算されると, サンクからenvを消すことに注意しよう. そうしても解釈系の返す値には違いがない. しかしスペースを節約するのに役立つ. いまや不要になったサンクからのenvへの参照を除去すると,
5.3節で論じるように, この構造は
ごみ集め(garbage collected)され, スペースは再生されるからである.
同様に3.5.1節のメモ化した遅延オブジェクト中の不要な環境を, その値を格納した後でmemo-procに(set! proc '())のようなことをさせ(delayが評価された環境を含んでいる)手続きprocを捨て, ゴミ集めさせることが出来た.
38
この問題は遅延評価と副作用の間の相互作用が非常に紛わしくなり得ることを示す. これは3章の議論から期待出来るものである.