第二章

amb 的 Scheme 实现

2.1  初始化

amb-fail 是最近一个失败的分支设置的函数。如果执行没有参数的 (amb) 就会转到这个 amb-fail.

这个例子里,我们把 amb-fail 被初始化为打印 "amb tree exhausted"。

(define amb-fail '*)

(define initialize-amb-fail
  (lambda ()
    (set! amb-fail
      (lambda ()
        (error "amb tree exhausted")))))

(initialize-amb-fail)

2.2  amb 的 syntax-rules 实现

我们用 R5RS 的 syntax-rules 来实现 amb 操作符:

(define-syntax amb
  (syntax-rules ()
    ((amb alt ...)
     (let ((prev-amb-fail amb-fail))
       (call/cc
        (lambda (sk)

          (call/cc
           (lambda (fk)
             (set! amb-fail
                   (lambda ()
                     (set! amb-fail prev-amb-fail)
                     (fk 'fail)))
             (sk alt))) ...
             
             (prev-amb-fail)))))))

2.3  分析

有些不容易看懂,实际上它的功能就是把

(amb #f #t)

这样的输入,转换成:

(let ((prev-amb-fail amb-fail))
  (call/cc
   (lambda (sk)

     ; branch 1
     (call/cc
      (lambda (fk)
        (set! amb-fail
              (lambda ()
                (set! amb-fail prev-amb-fail)
                (fk 'fail)))
        (sk #f)))

     ; branch 2
     (call/cc
      (lambda (fk)
        (set! amb-fail
              (lambda ()
                (set! amb-fail prev-amb-fail)
                (fk 'fail)))
        (sk #t)))
        
     (prev-amb-fail))))

表达式先把 amb-fail 的值保存在局部变量 prev-amb-fail 里,这 样当整个 amb 表达式失败时,它可以通过 prev-amb-fail 通知上 一个 amb 表达式改变它的值。

整个 amb 表达式的 continuation 存放在 sk 里。对于每一个参数, 使用了一个 call/cc 得到它的 continuation. 并且保存在 fk 里。 我们把这些参数对应的 call/cc 暂且叫做 *分支* 好了。看上面的 "; branch 1" 和 "; branch 2".

当某一个分支得到一个值,它就通过整个 amb 的 continuation(sk) 把这个值返回出去。这样 amb 就返回一个值。

每一个分支在第一次执行时,有两项工作:

  1. 把当前的 amb-fail 设置为一个函数。这个 *内部函数* 的作 用就是把 amb-fail 的值恢复到进入 amb 以前的值:

    (lambda ()
      (set! amb-fail prev-amb-fail)
      (fk 'fail))
    

  2. 立即通过 amb 表达式的 continuation(sk) 返回自己的分支 的值。从而引起 amb 表达式中途返回。

注意,每一个分支执行时都会引起 amb 立即返回。后面的分支都还 没有执行!

2.3.1  举例

(if (amb #f #t)
    1
    (amb))

就用最开头的那个最简单的例子,这样容易理解:

(let ((prev-amb-fail amb-fail))
  (call/cc
   (lambda (sk)

     ; branch 1
     (call/cc
      (lambda (fk)
        (set! amb-fail
              (lambda ()
                (set! amb-fail prev-amb-fail)
                (fk 'fail)))
        (sk #f)))

     ; branch 2
     (call/cc
      (lambda (fk)
        (set! amb-fail
              (lambda ()
                (set! amb-fail prev-amb-fail)
                (fk 'fail)))
        (sk #t)))
        
     (prev-amb-fail))))

第一个 amb 被展开,就成了上面那个样子。#f 和 #t 是两个分支。 然后 #f 对应的分支将被运行。这个分支的 call/cc 把 amb-fail 绑定到自己的内部函数,然后马上使用

(sk #f)

返回分支的值。

接着 if 得到这个值,从而引起第二个没有参数的 (amb) 的执行。 这就是一个“失败”。(amb) 的执行没有参数,所以没有分支。它被 展开成:

(let ((prev-amb-fail amb-fail))
  (call/cc
   (lambda (sk)
        
     (prev-amb-fail))))

它马上就会执行最下面的

(prev-amb-fail)

而 prev-amb-fail 在进入这个 (amb) 的时候被绑定到了 amb-fail, 也就是最近一个失败函数。这里 amb-fail 其实就是第一个 amb 表 达式的 #f 分支设置的。

所以,我们将执行 #f 的分支设置的 amb-fail 函数。这就是 #f 分 支的内部函数,它先把 amb-fail 的值设置成 prev-amb-fail 也就 是进入 (amb #f #t) 以前的值,然后使用 (fk fail) 返回 fail 到分支的 continuation.

接着 (amb #f #t) 的第二个分支开始执行。它在设置好 amb-fail 为自己的内部函数之后,返回了 #t 给 if. 那么 if 就会返回 1. 使得整个 if 表达式没有“失败”。

2.3.2  简化的实现

其实上面的 amb 实现似乎复杂了一些。我们可以简化如下:

(define-syntax amb
  (syntax-rules ()
    ((amb alt ...)
     (let ((prev-amb-fail amb-fail))
       (call/cc
        (lambda (sk)

          (call/cc
           (lambda (fk)
             (set! amb-fail fk)
             (sk alt))) ...

             (prev-amb-fail)))))))