34

我试图在 Scheme 中掌握 call/cc 的语义,Wikipedia page on continuations 以阴阳谜题为例:

(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

6 回答 6

22

理解方案

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

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

考虑到这一点,构造(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) bodyas 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 的经验。所以,让我们来弄清楚这实际上是如何工作的。

延续入门

观察 yin 和 yang 的奇特公式化核心:它定义了一个函数,然后立即调用它。它看起来就像(function(a,b) { return a+b; })(2, 3),可以简化为5。但是简化 yin/yang 内部的调用是错误的,因为我们没有传递一个普通的值。我们正在向函数传递一个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。我们现在已经打印了@*.

接下来,我们调用 continuation 并yin传递它yang。这使我们跳转到第 1 行,就在 get-cc 内部,并让它返回yang。的值yang现在被传递给函数,该函数打印@,然后返回 的值yang。现在yin被分配了那个延续yang。接下来我们继续执行第 2 行:获取 c/c,打印*,将 c/c 存储在yang. 我们现在有@*@*. 最后,我们转到第 3 行。

请记住,yin现在从第 2 行第一次执行时开始继续。所以我们跳到第 2 行,打印第二个*并更新yang. 我们现在有@*@**. 最后,yin再次调用 continuation,它将跳转到第 1 行,打印一个@. 等等。坦率地说,此时我的大脑抛出了 OutOfMemory 异常,我忘记了一切。但至少我们得到了@*@**

显然,这很难理解,甚至更难解释。做到这一点的完美方法是在一个可以表示延续的调试器中单步执行它,但是唉,我不知道有什么。我希望你喜欢这个;我当然有。

于 2012-03-22T11:15:58.410 回答
16

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

  • 第一个 @ 和 * 在yinyang第一次绑定在let*. (yin yang)被应用,并在第一次调用/抄送完成后立即返回顶部。
  • 打印下一个 @ 和 *,然后打印另一个 *,因为这一次,yin重新绑定到第二个 call/cc 的值。
  • (yin yang)再次应用,但这次它在原始yang环境中执行,其中yin绑定到第一个 call/cc,因此控制返回到打印另一个 @。该yang参数包含在第二次通过时重新捕获的延续,正如我们已经看到的,这将导致打印**。所以在第三遍时,@*将被打印,然后这个双星打印延续被调用,所以它以 3 星结束,然后这个三重星延续被重新捕获,......
于 2010-04-23T20:32:13.627 回答
6

首先沉思,最后可能的答案。

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

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

我们从这里开始。

第一次通过你 getyin=Ayang=Basyin并且yang正在被初始化。

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延续,直到回到 whenyin=A并显示@。我们循环遍历堆栈,每次B写入 a 。*

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

谢谢你的问题。

于 2010-04-25T03:11:00.357 回答
6

阴阳谜题是用 Scheme 编写的。我假设你知道 Scheme 的基本语法。

但是我假设您不知道let*or call-with-current-continuation,我将解释这两个关键字。

解释let*

如果你已经知道了,你可以跳到Explain call-with-current-continuation

let*,它看起来像let,行为像,但会一一let评估其定义的变量((yin ...)和),并且急切地求值。(yang ...)这意味着,它将首先评估yin,然后评估yang

你可以在这里阅读更多内容: 在方案中使用 Let

解释call-with-current-continuation

如果您已经知道,可以跳到Yin-Yang puzzle

这有点难以解释call-with-current-continuation。所以我会用一个比喻来解释。

想象一个知道咒语的巫师,那是call-with-current-continuation。一旦他施了咒语,他就会创造一个新的宇宙,并把自己送到那里。但在新的宇宙中,他什么也做不了,只能等待有人呼唤他的名字。一旦被召唤,巫师就会回到原来的宇宙,手里拿着那个可怜的人——“某人”,继续他的巫师生活。如果没有被召唤,当新宇宙结束时,巫师也回到了原来的宇宙。

好吧,让我们更技术。

call-with-current-continuation是一个接受函数作为参数的函数。call-with-current-continuation用函数调用后F,会将当前调用的运行环境打包current-continuation为参数C,发送给函数F,然后执行F。所以整个程序就变成了(F C)。或者更多的 JavaScript F(C);:. C就像一个函数。如果C没有被调用F,那么它是一个普通程序,F返回时,call-with-current-continuation有一个值作为F的返回值。但是如果C用参数调用V,它会再次改变整个程序。程序在被调用时变回状态。call-with-current-continuation但现在call-with-current-continuation产生一个值,即V. 程序还在继续。

让我们举个例子。

(define (f return)
  (return 2)
  3)
(display (f whatever)) ;; 3
(display (call-with-current-continuation f)) ;; 2
(display (call-with-current-continuation (lambda (x) 4))) ;; 4

第一个display输出3,原因。

但是第二个display输出2。为什么?

让我们深入了解它。

评价(display (call-with-current-continuation f))时,先评价(call-with-current-continuation f)。我们知道它会将整个程序更改为

(f C)

考虑到 的定义f,它有一个(return 2)。我们必须评估(C 2)。那是continuation被调用的时候。所以它将整个程序改回

(display (call-with-current-continuation f))

但现在,call-with-current-continuation有价值2。所以程序变成了:

(display 2)

阴阳谜题

让我们看看谜题。

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

让我们让它更具可读性。

(define (id c) c)
(define (f cc) (display #\@) cc)
(define (g cc) (display #\*) cc)
(let* ((yin
         (f (call-with-current-continuation id)))
       (yang
         (g (call-with-current-continuation id))))
      (yin yang))

让我们在大脑中运行程序。

第0轮

let*让我们先评估一下yinyin

(f (call-with-current-continuation id))

所以我们先评估(call-with-current-continuation id)。它包装了当前环境,我们称之为C_0与时间线中的其他延续区分开来,它进入了一个全新的宇宙:id. 但id只是返回C_0

我们应该记住是什么C_0C_0是这样的程序:

(let* ((yin
         (f ###))
       (yang
         (g (call-with-current-continuation id))))
      (yin yang))

###是一个占位符,以后会被取回的值填充C_0

id只是返回C_0。它不叫C_0。如果它调用,我们将进入C_0's 的宇宙。但它没有,所以我们继续评估yin

(f C_0) ;; yields C_0

f是一个类似的函数id,但它有一个副作用——输出@

所以程序输出@和 let yinto be C_0。现在程序变成了

(let* ((yin C_0)
       (yang
         (g (call-with-current-continuation id))))
      (yin yang))

评估后yin,我们开始评估yangyang

(g (call-with-current-continuation id))

call-with-current-continuation这里创建另一个延续,命名为C_1. C_1是:

(let* ((yin C_0)
       (yang
         (g ###)))
      (yin yang))

###是占位符。请注意,在此继续中,yin' 的值是确定的(这就是let*这样做的)。我们确信它yin的价值就C_0在这里。

因为(id C_1)C_1,所以yang的价值是

(g C_1)

g有一个副作用——输出*。所以程序确实如此。

yang的价值是现在C_1

至此,我们已经展示了@*

所以现在变成了:

(let* ((yin C_0)
       (yang C_1))
      (yin yang))

由于yinyang都已解决,我们应该评估(yin yang)。它是

(C_0 C_1)

天哪!

但最后,C_0被调用。所以我们飞入C_0宇宙,忘记了这些狗屎。我们再也不会回到这个宇宙了。

第1轮

C_0C_1回去。现在程序变成了(如果你忘记了C_0代表什么,回去看看):

(let* ((yin
         (f C_1))
       (yang
         (g (call-with-current-continuation id))))
      (yin yang))

啊,我们发现yin的值还没有确定。所以我们评估它。在评估的过程中yin,我们输出了一个@asf的副作用。我们知道现在yinC_1

我们开始评估yang,我们又遇到call-with-current-continuation了。我们被练习了。我们创建一个延续C_2,它代表:

(let* ((yin C_1)
       (yang
         (g ###)))
      (yin yang))

我们将 a 显示*g正在执行。我们来到这里

(let* ((yin C_1)
       (yang C_2))
      (yin yang))

所以我们得到了:

(C_1 C_2)

你知道我们要去哪里。我们要去C_1的宇宙。我们从记忆中回忆它(或从网页复制和粘贴)。就是现在:

(let* ((yin C_0)
       (yang
         (g C_2)))
      (yin yang))

我们知道,在C_1'宇宙中,yin'的价值已经确定。所以我们开始评估yang。随着我们的实践,我会直接告诉你它显示*并变成:

(C_0 C_2)

现在我们已经打印出来@*@**了,我们将C_0带着它去宇宙C_2

第二轮

随着我们的实践,我会告诉你,我们显示 '@', yinis C_2,并且我们创建一个新的 continuation C_3,它代表:

(let* ((yin C_2)
       (yang
         (g ###)))
      (yin yang))

我们显示*, yangis C_3, 它变成

(C_2 C_3)

我们可以继续。但我会停在这里,我已经向你展示了阴阳谜题的前几个输出是什么。

为什么*增加次数?

现在你的脑袋里充满了细节。我给你做个总结。

我将使用类似 Haskell 的语法来简化。并且cccall-with-current-continuation.

#C_i#被 括起来时#,表示在此处创建延续。;表示输出


yin = f cc id
yang = g cc id
yin yang

---

yin = f #C_0# ; @
yang = g cc id
yin yang

---

yin = C_0
yang = g #C_1# ; *
yin yang

---

C_0 C_1

---

yin = f C_1 ; @
yang = g #C_2# ; *
yin yang

---

C_1 C_2

---

yin = C_0
yang = g C_2 ; *
yin yang

---

C_0 C_2

---

yin = f C_2 ; @
yang = g #C_3#; *
yin yang

---

C_2 C_3

---

yin = C_1
yang = g C_3 ; *
yin yang

---

C_1 C_3

---

yin = C_0
yang = g C_3 ; *
yin yang

---

C_0 C_3

如果你仔细观察,你会很明显

  1. 有很多宇宙(实际上是无限的),但C_0它是唯一一个开始于f. 其他由 启动g
  2. C_0 C_n总是有新的延续C_max。因为C_0是第一个g cc id没有处决的宇宙。
  3. C_0 C_n也显示@C_n C_m其中 n 不为 0 将显示*
  4. 程序一次又一次推演为C_0 C_n,我将证明它C_0 C_n被越来越多的其他表达式分隔,这导致@*@**@***...

一点数学

假设C_n(n != 0) 是所有延续中最大的编号,然后C_0 C_n被调用。

假设:当C_0 C_n被调用时,C_n是当前最大编号的延续。

现在C_{n+1}C_0 C_n这样创建的:

yin = f C_n ; @
yang = g #C_{n+1}#
yin yang

所以我们得出结论:

定理 I. 如果C_0 C_n被调用,它将产生一个延续C_{n+1},其中yinC_n

然后下一步是C_n C_{n+1}

yin = C_{n-1}
yang = g C_{n+1} ; *
yin yang

原因yinC_{n-1}它在C_n创建时遵循定理 I

然后C_{n-1} C_{n+1}被调用,我们知道什么时候C_{n-1}被创建,它也服从定理 I。所以我们有C_{n-2} C_{n+1}

C_{n+1}是变体。所以我们有第二个定理:

定理二。如果调用C_n C_mwhichn < mn > 0,它将变为C_{n-1} C_m

我们已经手动检查了C_0 C_1 C_2 C_3。他们遵守假设和所有定理。我们知道如何首先创建@*创建。

所以我们可以在下面写模式。

C_0 C_1 ; @ *
C_[1-0] C_2 ; @ * *
C_[2-0] C_3 ; @ * * *
...

它不是那么严格,但我想写:

量子点

于 2016-04-09T07:22:59.770 回答
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 只是一个与副作用相关的相同函数。我们称这些函数f为 (for display #\@) 和g(for display #\*)。

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

接下来我们需要制定评估顺序。为了清楚起见,我将介绍一个“步骤表达式”,它使每个评估步骤都变得明确。首先我们要问:上面的功能需要什么?

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

s0 f g =>

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

粗略地说,get-cc给你一个代表“当前延续”的值。假设这是s1因为这是下一步。所以我们写

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

请注意,参数是无范围的,这意味着fand gins0和ins1不必相同,它们只能在当前步骤中使用。这使得上下文信息明确。现在,价值是cc多少?由于它是“当前的延续”,因此它与相同的值相同s1并且f绑定g到相同的值。

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 => 

最后,最后一步是申请yangyin

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

详细的求值顺序(这里的体内的 lambdas2被简单地表示为部分求值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)))
=> ...

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

于 2015-08-13T03:11:29.440 回答
1

这是混淆大师 David Madore 的一个古老谜题,他创造了 Unlambda。这个谜题已多次讨论 comp.lang.scheme。

泰勒坎贝尔的一个很好的解决方案: https ://groups.google.com/d/msg/comp.lang.scheme/pUedvrKYY5w/uIjTc_T1LOEJ

David Madore (1999) 的原始帖子: https ://groups.google.com/d/msg/comp.lang.scheme/Fysq_Wplxsw/awxEZ_uxW20J

于 2016-04-09T10:47:50.413 回答