4.3.3 实现一个AMB的解释器

4.3.3 实现一个AMB的解释器
一个普通的scheme表达式解释可能返回一个值,可能没有终止,
或者可能报告一个错误。在非确定性scheme,一个表达式的解释,
可以额外地导致一个死胡同的发现,在这种情况下的解释,必须
回溯到之前的选择点。非确定性scheme的解释,在这种特殊的情况下,
是复杂的。

为了非确定性scheme,通过修改4.1.7部分中的分析解释器,我们将
组装amb的解释器。正如在分析解释器中,通过调用那个表达式
的分析生成的一个执行程序完成表达式的解释。在执行程序中,在普通
scheme的程序的解释与非确定性scheme的解释之间的不同是完全不同的。

*执行程序与继续执行
对于普通的解释器,重调用执行程序,要一个参数,是执行的环境。
相反,在amb的解释器中的执行程序有三个参数,环境和两个程序叫
可继续的程序。通过调用这两个可继续的程序之一,将完成表达式的解释。
如果解释结果在一个值中,用那个值调用成功时的继续程序,如果解释结果
在一个死胡同中发现,失败的继续程序被调用。组装和调用合适的继续程序
是被非确定性解释器实现的回溯机制。

成功的继续程序的工作是接受一个值,并且执行相应的计算。
根据那个值,如果那个值导致了进入死胡同,成功的继续程序
被传到另一个失败的继续程序。

失败的继续程序的工作是试着非确定性的过程的另一个分支。
非确定性语言的本质是表达式可能有多个选择的选项的事实。
即使不知道哪个选择将是一个可选择的结果,这样的表达式
的解释必须执行显示出的可选项之一。为了处理这个任务,
解释器选择其中之一的选项,并且把值传递给成功的继续程序。
为了选择一个不同的可选项,结合这个值,解释器组装和传递给
一个失败的继续程序,这个继续程序能稍后被调用.

当一个用户的程序显式的拒绝攻击的当前行,在解释中,一个
失败被触发了。也就是一个失败的继续程序被调用.攻击是指
例如对require调用可能在amb的执行中,一个表达式总是失败的。
失败的继续程序在那个点上,将引起最近的选择点选择其它的可选项。
如果在那个选择点上,没有更多的选择项能被考虑时,在更前的选择点处
的一个失败被触发,等等.驱动循环也调用了失败的继续程序,相对应的是
一个try-again请求,来找到表达式的另一个值。

此外,如果一个有副作用的操作(例如对变量的赋值)发生在选择的过程
的一个分支,当过程发现了一个死胡同时,它可能是必需的,在做一个新的
选择之前,用来撤销有副作用的操作。通过有副作用的操作生成一个失败的
继续程序来撤销副作用和传播失败,来完成这个任务。

总之,失败的继续程序由如下的部分组成:

·amb表达式-如果amb表达式做的现在的选择陷入了死胡同,
提供一个机制来做可选的选择。
·顶层的驱动--当选择用尽时,提供一个机制来报告失败。
·赋值---在回溯期间,接受失败并且做撤销的赋值。

仅当遇到死胡同时,失败被初始化,这发生如下的事:
·如果用户程序执行amb
·如果用户在顶层的驱动上输入try-again.

在一个失败的执行过程期间,失败的继续程序也能被调用:
·一个完成了从副作用中恢复的赋值,它创建了失败继续程序的时候,
它中断它调用失败继续程序,为了把失败传播回这个赋值所属的选择点,
或者是顶层。
·当amb用尽了所有的选择,它调用amb的原来的失败继续程序,
为了把失败传播回上一个选择点或者得顶层。

*解释器的结构
对于amb解释器,语法与数据表示的程序,还有基本的分析程序
都与4.1.7部分中的解释器的内容一致,除了我们需要额外的语法
程序来识别amb关键字的事实:

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

在分析一个短句子时,我们也必须添加一个分发,来
识别这个标识符,生成一个合适的执行程序:

((amb? exp) (analyze-amb exp))

顶级程序ambeval(与部分中的给定的eval版本的相似)
分析,给定的表达式,应用结果的执行程序给给定的环境,
结合两个给定的继续程序:

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

一个成功的继续程序是一个有两个参数的程序:刚得到的值和
如果那个值导致下一个失败的情况下,能用到的另一个失败的
继续程序。一个失败的继续程序是一个没有参数的程序。所以
一个执行程序的通用的形式如下:

(lambda (env succeed fail)
 ;; succeed is (lambda (value fail) ...)
 ;;  fail is (lambda ()  ...)
...)

例如,执行如下的:

(ambeval <exp>
    the-global-environment
    (lambda (value fail) value)
    (lambda () '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的成功的继续程序负责检查判断式的值是否为真,再执行真值或者是假值的程序部分。如果pproc
的执行是失败的,条件表达式的原来的失败程序被调用。

(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
        (lambda (pred-value fail2)
              (if (true? (pred-value env))
     (cproc env succeed fail2)
     (aproc env succeed fail2))
         )
        fail)
     )
))

序列的处理也与之前的解释器相似,除了子程序的顺序执行部分要求加上继续程序。从命名上看,
顺序地执行程序a,b,我们调用a,a带有一个成功的继续程序,这个成功的继续程序调用b.

(define (analyze-sequence exps)
   (define (sequentially proc1 proc2)
         (lambda (env succeed fail) 
                 (proc1 env 
                            (lambda  (proc1-value fail2)  (proc2 env  succeed fail2))
                            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-value! var val env) 
             (succeed 'ok fail2))
                       fail)
         )
   )
)

赋值是更有趣的了。这是我们真实地使用继续程序而不是仅仅把它们传递出去的第一个地方。
赋值的执行程序开始很像一个定义。它首先尝试得到一个新的值,再把它赋给一个变量。如果
执行程序vproc的解释失败了,赋值就失败了。

如果vproc成功了,然而,我们继续进行赋值,我们必须考虑计算的这个分支
在稍后的时候失败的可能性,这将要求我们撤销赋值的过程。因此,我们必须安排撤销赋值
作为回滚过程的一部分。

给定的vproc的一个成功的继续程序完成的任务有在对变量赋一个新的值之前,
保存变量的旧的值,再继续赋值过程。失败的继续程序被传递,带着变量的值,
在继续失败的过程前,恢复变量的旧的值。也就是说,一个成功的赋值提供了
一个失败的继续程序,它将接受一个内部的失败;无论哪个失败,都调用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) 
                     (let ((old-value  (lookup-variable var env)))
                          (set-variable-value! var val env) 
           (succeed 'ok   (lambda () 
                                                           (set-variable-value!  var old-value env)
                                                             (fail2)))))
               fail)
        )
   )
)

*程序应用
程序应用的执行程序没有包括新的思想,除了管理继续程序的技术复杂性。
在analyze-application中这种复杂性增加了,归于对成功的继续程序与
失败的继续程序保持跟踪的需要,像我们解释操作数一样。我们使用一个
程序get-args,来解释操作数的列表,而不是像在普通的解释器中的一个
简单的map.

(define (analyze-application exp)
   (let ((fproc (analyze (operator exp)))
         (aprocs (map analyze (operands exp))))
       (lambda  (env  succeed fail)
           (fproc env
                     (lambda (proc fail2)
                          (get-args aprocs
                                          env
                                         (lambda (args fail3)
                                              (execute-application proc args succeed fail3
                                )
                                          )
                                          fail2
                          )
                     )
                      fail))
          )
)

在get-args,值得注意的是,通过调用列表中的任何一个aproc,aproc带着一个成功的继续程序,
这继续程序递归调用get-args, 完成了aproc执行程序的列表的如何取尾操作和args的结果列表如何
组装的操作。对get-args的任何递归调用的一次,都一个成功的继续程序,它的值是最新得到的实参与
累计的参数的列表的组装:

(define (get-args aprocs   env  succeed fail)
      (if  (null? aprocs) 
                (succeed '() fail)
                ((car aprocs)   env  
                                       (lambda  (args fail2) 
                                                      (get-args  (cdr aprocs)
                                                                        env 
                                                                        (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的执行程序定义了一个循环try-next,循环通过执行程序
找amb表达式的所有的可能的值。任何一个执行程序被调用都带有一个失败的继续程序,
这个失败的继续程序将试图找下一个可能的值。当没有更多的可能的值时,整个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的解释器的驱动循环是复杂的,归于允许用户在解释表达式时重新尝试的机制。
驱动使用一个程序叫做internal-loop,它以一个程序try-again作为参数。在非确定性的解释中,
调用程序try-again的目的是继续找再一个未试过的可选择项。internal-loop任何一次调用
程序try-again,对应于在驱动循环中用户输入try-again ,或者是调用ambeval开始了一个新的解释。

ambeval的这个调用的失败继续程序通知用户,没有更多的值了并且重新调用驱动循环。

ambeval的这个调用的成功继续程序是更加微妙的。我们打印了得到的值然后再调用内部循环,
并带有程序try-again,try-again能够尝试下一个可选项。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  ";;;;  Start a new problem") 
                                   (ambeval input 
                                                   the-global-environment
                                                   (lambda  (val next-alternative) 
                                                            (announce-output out-prompt) 
                                                            (user-print val)
                                                            (internal-loop  next-alternative))
                                                   (lambda  ()
                                                                  (announce-output   "") 
                                                                  (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
实现一个新的标识符ramb,它像amb,除了它以随机的顺序搜索可选项,而不是从左到右。显示这
如何能有利于解决 在练习4.49中的阿丽莎的问题?

练习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)

如果我们使用set!而不是permanent-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-pairs  '(1 3 5 8)  '(20 35 110))))  
                      (permanent-set!  pairs (cons p pairs)) 
                     (amb))
              pairs)
)

练习4.54
如果我们没有意识到require能被实现为一个普通的程序,使用amb,被用户定义,
作为非确定性的一部分,我们将不得不把它实现为一个标识符,这是require的语法程序。

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

(define  (require-predicate exp)   (cadr  exp))

在analyze中的分发中的一个子句

((require?  exp)  (analyze-require  exp))

正如程序analyze-require处理require表达式,完成如下的程序analyze-require的定义。

(define  (analyze-require  exp)
   (let  ((pproc  (analyze  (require-predicate))))
            (lambda (env succeed fail)
                 (pproc  env 
                              (lambda  (pred-value  fail2)
                              (if  <??> 
                                     <??> 
                                    (succeed  'ok fail2)))
                              fail))
   )
)

猜你喜欢

转载自blog.csdn.net/gggwfn1982/article/details/82957505