阴阳拼图是如何运作的?

时间:2010-04-22 21:32:39

标签: scheme callcc

我正在尝试在Scheme中掌握call / cc的语义,并且关于continuation的Wikipedia页面以阴阳拼图为例:

(let* ((yin
         ((lambda (cc) (display #\@) cc) (call-with-current-continuation (lambda (c) c))))
       (yang
         ((lambda (cc) (display #\*) cc) (call-with-current-continuation (lambda (c) c)))) )
    (yin yang))

应输出@*@**@***@****@..., 但我不明白为什么;我希望它输出@*@********* ...

有人可以详细解释为什么阴阳拼图的工作方式有效吗?

4 个答案:

答案 0 :(得分:18)

了解方案

我认为理解这个难题的问题至少有一半是Scheme语法,大多数人都不熟悉。

首先,我个人认为call/cc x比同等替代x get/cc更难理解。它仍然调用x,将其传递给当前的延续,但不知何故更适合在我的大脑电路中表示。

考虑到这一点,构造(call-with-current-continuation (lambda (c) c))变得简单get-cc。我们现在要做到这一点:

(let* ((yin
         ((lambda (cc) (display #\@) cc) get-cc))
       (yang
         ((lambda (cc) (display #\*) cc) get-cc)) )
    (yin yang))

下一步是内在的lambda的身体。 (display #\@) cc,用更熟悉的语法(对我来说,无论如何)意味着print @; return cc;。在我们处理它的同时,让我们将lambda (cc) body重写为function (arg) { body },删除一堆括号,并将函数调用更改为类C语法,以获得此结果:

(let*  yin =
         (function(arg) { print @; return arg; })(get-cc)
       yang =
         (function(arg) { print *; return arg; })(get-cc)
    yin(yang))

现在开始变得更有意义了。现在,将这个完全重写为类似C语法(或类似JavaScript,如果您愿意),这是一个小步骤,以获得这个:

var yin, yang;
yin = (function(arg) { print @; return arg; })(get-cc);
yang = (function(arg) { print *; return arg; })(get-cc);
yin(yang);

最困难的部分现在结束了,我们已经从Scheme解码了这个!开玩笑;这很难,因为我以前没有使用过Scheme的经验。所以,让我们弄清楚这实际上是如何运作的。

关于延续的入门

观察阴阳的奇怪结构:它定义了一个函数,然后立即调用它。它看起来就像(function(a,b) { return a+b; })(2, 3),可以简化为5。但简化阴/阳内的调用将是一个错误,因为我们并没有将它传递给普通值。我们将函数传递给 continuation

延续是一见钟情的奇怪野兽。考虑更简单的程序:

var x = get-cc;
print x;
x(5);

最初x设置为当前的延续对象(跟我一起),print x执行,打印类似<ContinuationObject>的内容。到目前为止一切都很好。

但延续就像一个功能;它可以用一个参数调用。它的作用是:接受参数,然后跳转到创建延续的任何地方,恢复所有上下文,并使其get-cc返回此参数。

在我们的示例中,参数为5,因此我们基本上会跳回到var x = get-cc语句的中间,只有这一次get-cc返回5。因此x变为5,下一个语句继续打印5.之后我们尝试调用5(5),这是一个类型错误,程序崩溃。

观察到调用延续是跳转,而不是通话。它永远不会返回到继续被调用的地方。这很重要。

该计划如何运作

如果你遵循这一点,那就不要抱有希望:这部分真的是最难的。这是我们的程序,再次删除变量声明,因为这仍然是伪代码:

yin = (function(arg) { print @; return arg; })(get-cc);
yang = (function(arg) { print *; return arg; })(get-cc);
yin(yang);

第一次第1行和第2行被击中时,它们现在很简单:获取延续,调用函数(arg),打印@,返回,将该延续存储在yin中。与yang相同。我们现在打印@*

接下来,我们在yin中调用延续,并将其传递给yang。这使我们跳到第1行,就在get-cc里面,然后让它返回yangyang的值现在传递给函数,该函数打印@,然后返回yang的值。现在yin被分配了yang所具有的延续。接下来我们继续第2行:获取c / c,打印*,将c / c存储在yang中。我们现在有@*@*。最后,我们进入第3行。

请记住,yin现在具有第2行首次执行时的延续。因此,我们跳转到第2行,打印第二个*并更新yang。我们现在有@*@**。最后,再次调用yin延续,这将跳转到第1行,打印@。等等。坦率地说,在这一点上,我的大脑抛出了OutOfMemory异常,我忘记了所有事情。但至少我们到了@*@**

显然,这很难理解,甚至更难解释。完成这项工作的最佳方法是在调试器中逐步执行它,这可以代表延续,但是,我不知道。我希望你喜欢这个;我当然有。

答案 1 :(得分:16)

我认为我并不完全理解这一点,但我只能想到一个(非常手工波浪)的解释:

  • yinyang首次绑定let*时,会打印第一个@和*。 (yin yang)已应用,并在第一次调用/ cc完成后立即返回到顶部。
  • 打印下一个@和*,然后打印另一个*,因为此时间yin重新绑定到第二个调用/ cc的值。
  • (yin yang)再次应用,但这次它正在原始yang的环境中执行,其中yin绑定到第一个调用/ cc ,所以控制回到打印另一个@。 yang参数包含在第二次传递时重新捕获的延续,正如我们已经看到的那样,将导致打印**。因此,在第三遍中,@*将被打印,然后这个双星打印延续被调用,所以最终得到3颗星,然后重新捕获这个三星级延续,... < / LI>

答案 2 :(得分:6)

首先是Musings,最后是可能的答案。

我认为代码可以像这样重写:

; call (yin yang)
(define (yy yin yang) (yin yang))

; run (call-yy) to set it off
(define (call-yy)
    (yy
        ( (lambda (cc) (display #\@) cc) (call/cc (lambda (c) c)) )
        ( (lambda (cc) (display #\*) cc) (call/cc (lambda (c) c)) )
     )
)

或者使用一些额外的显示语句来帮助查看正在发生的事情:

; create current continuation and tell us when you do
(define (ccc)
    (display "call/cc=")
    (call-with-current-continuation (lambda (c) (display c) (newline) c))
)

; call (yin yang)
(define (yy yin yang) (yin yang))

; run (call-yy) to set it off
(define (call-yy)
    (yy
        ( (lambda (cc) (display "yin : ") (display #\@) (display cc) (newline) cc) 
            (ccc) )
        ( (lambda (cc) (display "yang : ") (display #\*) (display cc) (newline) cc) 
            (ccc) )
     )
)

或者像这样:

(define (ccc2) (call/cc (lambda (c) c)) )
(define (call-yy2)
    (
        ( (lambda (cc) (display #\@) cc) (ccc2) )
        ( (lambda (cc) (display #\*) cc) (ccc2) )
    )
)

可能的答案

这可能不对,但我会去。

我认为关键点是'被叫'延续会将堆栈返回到之前的某个状态 - 好像没有其他事情发生过一样。当然,它不知道我们通过显示@*字符来监控它。

我们最初将yin定义为将执行此操作的续篇A

1. restore the stack to some previous point
2. display @
3. assign a continuation to yin
4. compute a continuation X, display * and assign X to yang
5. evaluate yin with the continuation value of yang - (yin yang)

但如果我们调用yang延续,则会发生这种情况:

1. restore the stack to some point where yin was defined
2. display *
3. assign a continuation to yang
4. evaluate yin with the continuation value of yang - (yin yang)

我们从这里开始。

第一次通过yin=Ayang=Byinyang正在初始化。

The output is @*

(计算AB个连续值。)

现在,(yin yang)首次被评估为(A B)

我们知道A的作用。它这样做:

1. restores the stack - back to the point where yin and yang were being initialised.
2. display @
3. assign a continuation to yin - this time, it is B, we don't compute it.
4. compute another continuation B', display * and assign B' to yang

The output is now @*@*

5. evaluate yin (B) with the continuation value of yang (B')

现在(yin yang)被评估为(B B')

我们知道B的作用。它这样做:

1. restore the stack - back to the point where yin was already initialised.
2. display *
3. assign a continuation to yang - this time, it is B'

The output is now @*@**

4. evaluate yin with the continuation value of yang (B')

由于堆栈已恢复到yin=A(yin yang)被评估为(A B')

我们知道A的作用。它这样做:

1. restores the stack - back to the point where yin and yang were being initialised.
2. display @
3. assign a continuation to yin - this time, it is B', we don't compute it.
4. compute another continuation B", display * and assign B" to yang

The output is now @*@**@*

5. evaluate yin (B') with the continuation value of yang (B")

我们知道B'的作用。它这样做:

1. restore the stack - back to the point where yin=B.
2. display *
3. assign a continuation to yang - this time, it is B"

The output is now @*@**@**

4. evaluate yin (B) with the continuation value of yang (B")

现在(yin yang)被评估为(B B")

我们知道B的作用。它这样做:

1. restore the stack - back to the point where yin=A and yang were being initialised.
2. display *
3. assign a continuation to yang - this time, it is B'"

The output is now @*@**@***

4. evaluate yin with the continuation value of yang (B'")

由于堆栈已恢复到yin=A(yin yang)被评估为(A B'")

.......

我认为我们现在有一个模式。

每次我们致电(yin yang)时,我们都会循环播放一堆B个续集,直到我们回到yin=A时,我们会显示@。我们遍历B个连续的堆栈,每次都写*

(如果大致正确,我会很高兴!)

感谢您提出问题。

答案 3 :(得分:1)

正如另一个答案所说,我们首先使用(call-with-current-continuation (lambda (c) c))简化get-cc

(let* ((yin
         ((lambda (cc) (display #\@) cc) get-cc))
       (yang
         ((lambda (cc) (display #\*) cc) get-cc)) )
    (yin yang))

现在,两个lambda只是一个与副作用相关的功能。我们称这些函数为fdisplay #\@}和gdisplay #\*)。

(let* ((yin (f get-cc))
       (yang (g get-cc)))
    (yin yang))

接下来,我们需要制定评估顺序。为了清楚起见,我将介绍一个&#34;步骤表达&#34;这使得每个评估步骤都明确。首先让我们问:上述功能需要什么?

它需要fg的定义。在步骤表达式中,我们写

s0 f g =>

第一步是计算yin,但需要评估(f get-cc),后者需要get-cc

粗略地说,get-cc为您提供了一个代表当前延续的值#34;让我们说这是s1,因为这是下一步。所以我们写了

s0 f g => s1 f g ?
s1 f g cc =>

请注意,这些参数是无范围的,这意味着fg中的s0s1不一定是必需的,它们只能在目前的步骤。这使得上下文信息显式化。现在,cc的价值是多少?由于它是&#34;当前延续&#34;,它与s1 fg绑定到相同的值相同。

s0 f g => s1 f g (s1 f g)
s1 f g cc =>

获得cc后,我们可以评估f get-cc。此外,由于以下代码中未使用f,因此我们不必传递此值。

s0 f g => s1 f g (s1 f g)
s1 f g cc => s2 g (f cc)
s2 g yin =>

下一个是yang的类似内容。但现在我们还有一个值可以传递:yin

s0 f g => s1 f g (s1 f g)
s1 f g cc => s2 g (f cc)
s2 g yin => s3 g yin (s3 g yin)
s3 g yin cc => s4 yin (g cc)
s4 yin yang => 

最后,最后一步是将yang应用于yin

s0 f g => s1 f g (s1 f g)
s1 f g cc => s2 g (f cc)
s2 g yin => s3 g yin (s3 g yin)
s3 g yin cc => s4 yin (g cc)
s4 yin yang => yin yang

这完成了步骤表达式的构造。把它翻译成Scheme很简单:

(let* ([s4 (lambda (yin yang) (yin yang))]
       [s3 (lambda (yin cc) (s4 yin (g cc))]
       [s2 (lambda (yin) (s3 yin ((lambda (cc) (s3 yin cc))))]
       [s1 (lambda (cc) (s2 (f cc)))])
      (s1 s1))

详细的评估顺序(此处s2正文中的lambda仅表示为部分评估s3 yin而不是(lambda (cc) (s3 yin cc))):

(s1 s1)
=> (s2 (f s1))
=> @|(s2 s1)
=> @|(s3 s1 (s3 s1))
=> @|(s4 s1 (g (s3 s1)))
=> @*|(s4 s1 (s3 s1))
=> @*|(s1 (s3 s1))
=> @*|(s2 (f (s3 s1)))
=> @*@|(s2 (s3 s1))
=> @*@|(s2 (s3 s1))
=> @*@|(s3 (s3 s1) (s3 (s3 s1)))
=> @*@|(s4 (s3 s1) (g (s3 (s3 s1))))
=> @*@*|(s4 (s3 s1) (s3 (s3 s1)))
=> @*@*|(s3 s1 (s3 (s3 s1)))
=> @*@*|(s4 s1 (g (s3 (s3 s1))))
=> @*@**|(s4 s1 (s3 (s3 s1)))
=> @*@**|(s1 (s3 (s3 s1)))
=> ...

(请记住,在评估s2s4时,将首先评估参数