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

4.3.3 amb評価器の実装



通常のSchemeの式を評価すると値が返るか, 停止しないか, エラーになるかする. 非決定性Schemeでは, 式の評価はこれに加えて, 袋小路の発見になったりする. 袋小路では評価の直前の選択点へバックトラックしなければならない. 非決定性Schemeの解釈はこの例外的場合のため, 複雑である.

   われわれは4.1.7節の解析評価器を修正し, 非決定性Schemeのamb評価器を構成しよう.55 その式の評価は, 解析評価器のように式の解析で作った 実行手続きを呼び出すことで実現する. 通常のSchemeの解釈と非決定性Schemeの解釈の相違は, すべて実行手続きにある.

実行手続きと継続
通常の評価器の実行手続きは一つの引数: 実行の環境をとることを思いだそう. それに対し, amb評価器の実行手続きは三つの引数: 環境と二つの継続手続き(continuation procedures)という手続きをとる. 式の評価は二つの継続のうち, 一つを呼び出すことで完了する: 評価の結果が値ならその値をもって 成功継続(success continuation)を呼び出す; 評価の結果が袋小路の発見なら, 失敗継続(failure continuation)を呼び出す. 適切な継続を構成し, 呼び出すことが, 非決定性評価器がバックトラックを実装する機構である.

   値を受け取り, 計算を続行するのが成功継続の仕事である. 値とともに, 成功継続は失敗継続も受けとる. それは後に, その値が袋小路に導いた時, 呼び出される.

   非決定性プロセスの別の道を試みるのが失敗継続の仕事である. 非決定性言語の本質は, 式は分岐の選択を表現出来るという事実にある. そういう式の評価は, その選択が受け入れ可能な結果に至るかどうかが前もって分らなくても, 示された分岐選択の一つへ進まなければならない. これに対処すべく, 評価器は分岐の一つを取り上げ, その値を成功継続へ渡す. この値とともに, 評価器は後に別の分岐を選ぶ時に呼び出される失敗継続を構成し, 渡す.

   評価の最中に, (例えばrequireの呼出しは結果として常に失敗する式(amb)の実行になる---4.3.1節参照)利用者のプログラムが積極的に現在の試行を拒絶した時, 失敗が惹き起される(つまり, 失敗継続が呼び出される). その時点で手元にある失敗継続は, 直前の選択点に別の分岐を選択させる. この選択点に, 考慮すべき分岐が残っていなければ, その前の選択点での失敗が惹き起され, これを繰り返す. 失敗継続はまた駆動ループから, try-again要求への応答として, 式の別の値を見出すべく呼び出される.

   加えて(変数への代入のような)副作用の演算が選択の結果のプロセスの枝で起きると, プロセスが袋小路に出会った時, 新しい選択をする前に, この副作用をやり戻す必要があるかも知れない. これは副作用に, 副作用をやり戻し, 失敗を伝搬する失敗継続を作らせることで実現出来る.

   要約すると失敗継続が構成されるのは次による.

amb式---amb式の作った現在の選択が袋小路に至った時, 別の選択をする機構を用意するため;

• トップレベルの駆動ループ---選択を使い切った時, 失敗を報告する機構を用意するため;

• 代入---失敗を横取りし, バックトラック中に代入を元へ戻すため.


   失敗は袋小路に出会った時だけ起動される. これが起きるのは次の場合である.

• 利用者プログラムが(amb)を実行した時;

• 利用者が駆動ループでtry-againを入力した時.


   失敗継続は, 失敗の処理中にも呼び出される.

• 代入が作り出した失敗継続が, 副作用のやり戻しを完了した時, この代入に至った選択点まで, または駆動ループまで失敗を伝搬し戻すため, それが横取りした失敗継続を呼び出す.

• あるambの失敗継続が, 選択を使い切った時, 直前の選択点か駆動ループまで失敗を伝搬し戻すため, このambに元々与えられた失敗継続を呼び出す.

評価器の構造
amb評価器の構文手続き, およびデータ表現手続きと, また基本的なanalyze手続きは, ambの特殊形式を判定する追加の構文手続きが必要という点を除き, 4.1.7節の評価器のそれらと同様である:56
(define (amb? exp) (tagged-list? exp 'amb))

(define (amb-choices exp) (cdr exp))
またanalyzeの振分け部に, この特殊形式を認識し, 適切な実行手続きを生成する節を追加しなければならない:
((amb? exp) (analyze-amb exp))

   トップレベルの手続きambeval(4.1.7節にあったevalの版と同様) は, 与えられた式を解析し, 結果の実行手続きを与えられた環境と二つの与えられた継続とに作用させる:


(define (ambeval exp env succeed fail)
  ((analyze exp) env succeed fail))

   成功継続は二引数: まさに得られた値と, この値がその後の失敗に導く時に使うべき, もう一つの失敗継続をとる手続きである. 失敗継続は引数なしの手続きである. そこで 実行手続きの一般形は

(lambda (env succeed fail)
  ;; succeed は (lambda (value fail) ...)
  ;; fail は (lambda () ...)
  ...)
である.

   例えば

(ambeval ⟨exp⟩
         the-global-environment
         (lambda (value fail) value)
         (lambda () 'failed))
の実行は, 与えられた式⟨exp⟩を評価しようとし, (評価が成功すれば)式の値か, (評価が失敗すれば)記号failedかのいずれかを返す. 次に示す駆動ループでambevalを呼び出すと, ループを継続し, try-againの要求に応じる, 遥かに複雑な継続手続きを使う.

   amb評価器の複雑さの殆んどは実行手続きが互いに呼び出す時の, 継続を引き渡す機構に由来する. 次のプログラムに従って, 実行手続きのそれぞれを, 4.1.7節の通常の評価器の対応する手続きと比べるべきである.

単純式
もっとも単純な式の実行手続きは, 継続を管理しなければならない点の他は, 本質的に通常の評価器のそれらと同じである. 実行手続きは, 渡された失敗手続きを渡しながら, 式の値とともに, 単に成功する.
(define (analyze-self-evaluating exp)
  (lambda (env succeed fail)
    (succeed exp fail)))

(define (analyze-quoted exp)
  (let ((qval (text-of-quotation exp)))
    (lambda (env succeed fail)
      (succeed qval fail))))

(define (analyze-variable exp)
  (lambda (env succeed fail)
    (succeed (lookup-variable-value exp env)
             fail)))

(define (analyze-lambda exp)
  (let ((vars (lambda-parameters exp))
        (bproc (analyze-sequence (lambda-body exp))))
    (lambda (env succeed fail)
      (succeed (make-procedure vars bproc env)
               fail))))

   変数の探索は常に「成功する」ことに注意しよう. lookup-variable-valueが変数を見出すことが出来なければ, いつものようにエラーとする. そういう「失敗」は, プログラムの虫---未束縛の変数の参照---であって, これはわれわれは現在試みているものの代りに, 他の非決定性選択を試みるべきであるという表示ではない.

条件式と並び
条件式も通常の評価器におけるのと同様に扱う. analyze-ifで生成した実行手続きは, 述語の値が真であるかどうかチェックし, 帰結部か代替部かのいずれかを実行しに行く成功継続を引数として, 述語実行手続きpprocを起動する. pprocの実行が失敗すると, if式のための元々の失敗継続を呼び出す.
(define (analyze-if exp)
  (let ((pproc (analyze (if-predicate exp)))
        (cproc (analyze (if-consequent exp)))
        (aproc (analyze (if-alternative exp))))
    (lambda (env succeed fail)
      (pproc env
             ;; pred-valueを得るための
             ;; 述語の評価の成功継続
             (lambda (pred-value fail2)
               (if (true? pred-value)
                   (cproc env succeed fail2)
                   (aproc env succeed fail2)))
             ;; 述語の評価の失敗継続
             fail))))

   並びも下請け手続きsequentiallyの継続渡しに必要な計略を除き, 以前の評価器におけるのと同様に扱う. つまりaに続きbを逐次的に実行するには, bを呼び出す成功手続きをもってaを呼び出す.

(define (analyze-sequence exps)
  (define (sequentially a b)
    (lambda (env succeed fail)
      (a env
         ;; aを呼び出す時の成功継続
         (lambda (a-value fail2)
           (b env succeed fail2))
         ;; aを呼び出す時の失敗継続
         fail)))
  (define (loop first-proc rest-procs)
    (if (null? rest-procs)
        first-proc
        (loop (sequentially first-proc (car rest-procs))
              (cdr rest-procs))))
  (let ((procs (map analyze exps)))
    (if (null? procs)
        (error "Empty sequence -- ANALYZE"))
    (loop (car procs) (cdr procs))))
定義と代入
定義は, 新しい変数を実際に定義する前に, 定義する値の式を評価する必要があるので, 継続を管理するのに苦労しなければならないもう一つの場合である. これを実現するには, 環境, 成功継続および失敗継続を引数として, 定義の値の実行手続きvprocを呼び出す. vprocの実行が成功すれば, 定義される変数に対する値valを得て, この変数は定義され, 成功は伝搬される:
(define (analyze-definition exp)
  (let ((var (definition-variable exp))
        (vproc (analyze (definition-value exp))))
    (lambda (env succeed fail)
      (vproc env                        
             (lambda (val fail2)
               (define-variable! var val env)
               (succeed 'ok fail2))
             fail))))

   代入は更に面白い. これは継続をたらい回しにするのでなく, 実際に使う初めての場所である. 代入の実行手続きは, 定義のそれのように始る: 変数に代入すべき新しい値を得ようと試みる. このvprocの評価が失敗すれば, 代入は失敗する.

   しかしvprocが成功し, 代入の実行に取り掛るなら, 計算のこの道が後に失敗し, 代入のバックトラックが必要になる可能性を考えなければならない. そこで, バックトラック処理の一部として, 代入のやり戻しの準備をする.57

   このことは(次に注釈*1*で示すように)変数に新しい値を代入し, 代入から進行するする前に昔の値を保存する成功継続をvprocに与えることで実現する. 代入の値とともに渡される(次に注釈*2*で示す)失敗継続は, 失敗を継続する前に, 変数を昔の値に戻す. つまり成功する代入は, 後の失敗を横取りする失敗継続を準備する; 失敗が何を呼び出しても, fail2はその代りにこの手続きを呼び出し, 実際にfail2を呼び出す前に, 代入を戻す.

(define (analyze-assignment exp)
  (let ((var (assignment-variable exp))
        (vproc (analyze (assignment-value exp))))
    (lambda (env succeed fail)
      (vproc env
             (lambda (val fail2)        ; *1*
               (let ((old-value
                      (lookup-variable-value var env))) 
                 (set-variable-value! var val env)
                 (succeed 'ok
                          (lambda ()    ; *2*
                            (set-variable-value! var
                                                 old-value
                                                 env)
                            (fail2)))))
             fail))))
手続きの作用
作用の実行手続きには, 継続を管理する技術的な複雑さ以外に新しいものはない. この複雑さがanalyze-applicationで生じるのは, 被演算子の評価で成功と失敗の継続を覚えておく必要があるからである. 被演算子のリストを評価するのに通常の評価器でのような単純なmapでなく, 手続きget-argsを使う.
(define (analyze-application exp)
  (let ((pproc (analyze (operator exp)))
        (aprocs (map analyze (operands exp))))
    (lambda (env succeed fail)
      (pproc env
             (lambda (proc fail2)
               (get-args aprocs
                         env
                         (lambda (args fail3)
                           (execute-application
                            proc args succeed fail3))
                         fail2))
             fail))))

   get-argsでは, aproc実行手続きのリストのcdrダウンと, 結果のargsのリストのconsアップが, リスト中の各aproc を再帰的にget-argsを呼び出す成功継続を引数として呼び出すことで実現していることに注意しよう. get-argsの再帰的呼出しのそれぞれは, その値が新しく得られた引数のすでに蓄積された引数リストへのconsである成功手続きを持っている.

(define (get-args aprocs env succeed fail)
  (if (null? aprocs)
      (succeed '() fail)
      ((car aprocs) env
                    ;; このaprocの成功継続
                    (lambda (arg fail2)
                      (get-args (cdr aprocs)
                                env
                                ;; get-argsの再帰呼出しの
                                ;; 成功継続
                                (lambda (args fail3)
                                  (succeed (cons arg args)
                                           fail3))
                                fail2))
                    fail)))

   execute-applicationで実行される実際の手続き作用は, 継続を管理する必要がある他は, 通常の評価器と同様に実現される.


(define (execute-application proc args succeed fail)
  (cond ((primitive-procedure? proc)
         (succeed (apply-primitive-procedure proc args)
                  fail))
        ((compound-procedure? proc)
         ((procedure-body proc)
          (extend-environment (procedure-parameters proc)
                              args
                              (procedure-environment proc))
          succeed
          fail))
        (else
         (error
          "Unknown procedure type -- EXECUTE-APPLICATION"
          proc))))
amb式の評価
amb特殊形式は非決定性言語の鍵となる要素である. ここでは解釈プロセスの本質と, 継続を覚えておく理由を見る. ambの実行手続きは, amb式のすべての可能な値の実行手続きを経て巡回するループtry-nextを定義する. 各実行手続きは, 次のものを試みようとする失敗継続をもって呼び出される. 試みるべき分岐がなくなると, amb式全体は失敗する.

(define (analyze-amb exp)
  (let ((cprocs (map analyze (amb-choices exp))))
    (lambda (env succeed fail)
      (define (try-next choices)
        (if (null? choices)
            (fail)
            ((car choices) env
                           succeed
                           (lambda ()
                             (try-next (cdr choices))))))
      (try-next cprocs))))
駆動ループ
amb評価器の駆動ループは, 式の評価において利用者に再試行を許す機構のため, 複雑である. 駆動器は, 引数として手続きtry-againをとるinternal-loopという手続きを使う. その意図は, try-againの呼出しは, 非決定性評価における次の未試行の道へ行くべきだということである. internal-loopは, 利用者が駆動ループに対しtry-againを入力した応答としてtry-againを呼び出すか, ambevalを呼び出して新しい評価を始める.

   このambevalの呼出しに対する失敗継続は, 利用者にこれ以上の値はないことを知らせ, 駆動ループを再起動する.

   ambevalの呼出しに対する成功継続は, 遥かに分りにくい. 得られた値を印字し, 次の分岐next-alternativeが試みられるよう, try-again手続きをもって内部ループinternal-loopを呼び出す. next-alternative手続きは成功継続へ渡された第二引数である. 通常は, この第二引数は, 現在の評価の枝が後に失敗した時に使う, 失敗継続と考える. しかし今の場合, 成功の評価は完了し, 次の成功評価を求めるためには「失敗」の分岐を起動することになる.


(define input-prompt ";;; Amb-Eval input:")
(define output-prompt ";;; Amb-Eval value:")


(define (driver-loop)
  (define (internal-loop try-again)
    (prompt-for-input input-prompt)
    (let ((input (read)))
      (if (eq? input 'try-again)
          (try-again)
          (begin
            (newline)
            (display ";;; Starting a new problem ")
            (ambeval input
                     the-global-environment
                     ;; ambeval 成功
                     (lambda (val next-alternative)
                       (announce-output output-prompt)
                       (user-print val)
                       (internal-loop next-alternative))
                     ;; ambeval 失敗
                     (lambda ()
                       (announce-output
                        ";;; There are no more values of")
                       (user-print input)
                       (driver-loop)))))))
  (internal-loop
   (lambda ()
     (newline)
     (display ";;; There is no current problem")
     (driver-loop))))
internal-loopの最初の呼出しには, 現在の問題はないと文句をいって駆動ループを再起動するtry-again手続きを使う. これは進行中の評価がない時に, 利用者がtry-againを入力すると起きる振舞いである.


問題 4.50


分岐を左から右ではなく, ランダムな順に探す他はambと同じな新しい特殊形式rambを実装せよ. これが問題{4.49}のAlyssaの問題を救うことを示せ.

問題 4.51


失敗の時にもやり戻さないpermanent-set!という新しい種類の代入を実装せよ. 例えば次のように, リストから二つの異る要素を選び, 成功した選択が出来るまでに必要な試行数が得られる.
(define count 0)

(let ((x (an-element-of '(a b c)))
      (y (an-element-of '(a b c))))
  (permanent-set! count (+ count 1))
  (require (not (eq? x y)))
  (list x y count))
;;; Starting a new problem
;;; Amb-Eval value:
(a b 2)

;;; Amb-Eval input:
try-again
;;; Amb-Eval value:
(a c 3)
ここでpermanent-set!の代りにset!を使ったら, どのような値が出力されるだろうか.

問題 4.52


利用者に式の失敗を捕らえることが出来るif-failという新しい構造を実装せよ. if-failは二つの式をとる. 第一の式を通常に評価し, 評価が成功すれば通常に戻る. しかし評価が失敗すれば, 次の例のように第二の式が戻される:
;;; Amb-Eval input:
(if-fail (let ((x (an-element-of '(1 3 5))))
           (require (even? x))
           x)
         'all-odd)
;;; Starting a new problem
;;; Amb-Eval value:
all-odd

;;; Amb-Eval input:
(if-fail (let ((x (an-element-of '(1 3 5 8))))
           (require (even? x))
           x)
         'all-odd)
;;; Starting a new problem
;;; Amb-Eval value:
8


問題 4.53


問題4.51に述べたようなpermanent-set!と, 問題4.52のようなif-failを使うと
(let ((pairs '()))
  (if-fail (let ((p (prime-sum-pair '(1 3 5 8) '(20 35 110))))
             (permanent-set! pairs (cons p pairs))
             (amb))
           pairs))
の評価の結果はどのようであろうか.

問題 4.54


利用者が非決定性プログラムの一部として定義するrequireが, ambを使う通常の手続きとして実装出来ると気づかなかったら, これを特殊形式として実装したであろう. これには構文手続き
(define (require? exp) (tagged-list? exp 'require))

(define (require-predicate exp) (cadr exp))
analyzeの振分けの新しい節
((require? exp) (analyze-require exp))
require式を扱う手続きanalyze-requireが必要である. analyze-requireの次の定義を完成せよ.
(define (analyze-require exp)
  (let ((pproc (analyze (require-predicate exp))))
    (lambda (env succeed fail)
      (pproc env
             (lambda (pred-value fail2)
               (if ⟨??⟩
                   ⟨??⟩
                   (succeed 'ok fail2)))
             fail))))




55 4.2節の遅延評価器の実装には4.1.1節の通常の超循環評価器の修正を選んだ. それに対しamb評価器の基本を4.1.7節の解析評価器に置く. それはその評価器の実行手続きがバックトラック実装に便利な枠組を提供しているからである.

56 評価器では, 非決定性プログラムで使ったlet(問題4.22参照)が使えることを仮定する.

57 定義の方は, 内部定義は掃き出されると仮定するので, やり戻す心配はない(4.1.6節).

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