さかもとのブログ

つらつらと

SICP4.3のamb評価器に関しての簡単な説明

問題4.50

分岐を左から右ではなく,ランダムに探す他はambと同じである,新しい特殊形式rambを実装せよ

ここで,そもそも分岐の候補はどこで作られているかが問題になる.
いままでいまいち動きがわからなかったけど,少しわかったので,自分用にメモ.

簡単のため

(amb 1 2 3 4 5)

の動きに関して説明する.
必要になる関数は以下のもの

(define (analyze exp)
  (cond [(self-evaluating? exp)
         (analyze-self-evaluating exp)]
        :
        [(amb? exp) (analyze-amb exp)]
                          :
        [else
         (error "Unknown expression type -- ANALYZE" exp)]))

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

(define (amb-choices exp) (cdr exp))

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

(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))))

(define (analyze-self-evaluating exp)
  (lambda (env succeed fail)
    (succeed exp fail)))

(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 success
                     (lambda (val next-alternative)
                       (announce-output output-prompt)
                       (user-print val)
                       (internal-loop next-alternative))
                     ;; ambeval failuer
                     (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))))

説明

driver-loopが起動している状態で,(amb 1 2 3 4 5)をamb評価器に食わせると,
inputに(amb 1 2 3 4 5)が代入され,これはtry-againではないので,ambevalが呼び出される.
ambevalの各引数には,

exp = (amb 1 2 3 4 5)
env = the-global-environment
succeed = (lambda (val next-alternative)
                               (announce-output output-prompt)
                               (user-print val)
                               (internal-loop next-alternative))

fail = (lambda ()
                       (announce-output
                        ";;; There are no more values of ")
                       (user-print input)
                       (driver-loop)))))))

が入る.(analyze exp)により,analyze-ambが呼び出される. (let ((cprocs (map analyze (amb-choices exp))))により,expの引数が,(map analyze)され,cprocsに入る.この時点で,cprocsには,

#<closure (analyze-self-evaluating analyze-self-evaluating)>

のリストが入っている.その後

(lambda (env succeed fail)
      (define (try-next choices)
        (if (null? choices)
            (fail)
            ((car choices) env
                                  succeed
                                  (lambda ()
                                  (try-next (cdr choices))))))

により,env, succeed, failを引数とするclosureが定義される.これが(analyze exp)の結果である.そして,ambevalに戻り,このclosureにenv,succeed,failが代入される.
ここで重要なのが,上記try-nextである.これは,演算子に(car choices), 被演算子に,ambevalに渡されたenv, succeed,lambda式を取る.
それぞれは,以下のようになっている.

(car choices) = #<closure (analyze-self-evaluating analyze-self-evaluating)>
env = the-global-environment
succeed = (lambda (val next-alternative)
                               (announce-output output-prompt)
                               (user-print val)
                               (internal-loop next-alternative))
lambda式 = (lambda () try-next (cdr choices))

ところで,analyze-self-evaluatingはexpを引数にとり,env, succeed,failを引数にとるclosureを返す.そのclosureの中身は,(succeed exp fail).
これをこの例題に即して展開すると,

(succeed exp fail)
      
((lambda (val next-alternative)
               (announce-output output-prompt)
               (user-print val)
               (internal-loop next-alternative)))
   1                                                  ;val
   (lambda () try-next (cdr choices)) ;next-alternative
   )

すると,先頭のlambda式が実行され,(user-print 1)より,1が表示され,(internal-loop next-alternative)より,再び入力待ちの状態になる.
ここで,internal-loopの引数はtry-againとなる.この入力待ちの状態で,amb評価器にtry-againを食わせると,

(if (eq? input 'try-again)
          (try-again)

より,先ほど保存したnext-alternative, つまり,(lambda () try-next (cdr choices))が実行され,2が表示される.
try-againの度に,next-alternativeが呼び出され,最終的にchoicesがnullになる.nullになると,analyze-ambに渡されたfail, つまり,

fail = (lambda ()
                       (announce-output
                        ";;; There are no more values of ")
                       (user-print input)
                       (driver-loop)))))))

が実行され,";;; There are no more values of "がamb評価器に表示される.そして,(user-print input)により,もとの式自体表示され, 再び(driver-loop)が実行されて,
入力待ち状態になる.

これはまさに継続の動きで,どこに飛ぶかを引数として維持しながら,計算をしていく.そのため成功継続,失敗継続という言葉が使われる.

分岐の順序は(amb-choices)が作り出すので,例えば右から左に分岐を作りたい場合,以下のどれでもできる.

  1. (define (amb-choices exp) (reverse (cdr exp)))
  2. (let ((cprocs (map analyze (reverse (amb-choices exp)))))
  3. (let ((cprocs (reverse (map analyze (amb-choices exp)))))

amb-choicesがまさに分岐選択なので,1番がいいと思う.