Clojure中的Continuations

29

我在某处读到Rich Hickey说:

"我认为延续理论上可能很好,但在实践中并不如此"

我不熟悉Clojure。
1. Clojure是否有延续?
2. 如果没有,你不需要延续吗?我看过很多很好的例子,特别是来自这位。有什么替代方案吗?
3. 如果有,是否有相关文档?


1
您的问题标题似乎与问题文本没有太多关系。 - anon
7个回答

27

在讨论continuations时,你需要区分两种不同的continuation:

  • 第一类continuation - 深度整合到语言中的continuation支持(Scheme或Ruby)。Clojure不支持第一类continuation。

  • 传递continuation风格(CPS)- CPS只是一种编码风格,任何支持匿名函数的语言都可以允许这种风格(也适用于Clojure)。

例子:

-- Standard function
double :: Int -> Int
double x = 2 * x

-- CPS-function – We pass the continuation explicitly
doubleCPS :: Int -> (Int -> res) -> res
doubleCPS x cont = cont (2 * x)
; Call
print (double 2)

; Call CPS: Continue execution with specified anonymous function
double 2 (\res -> print res)

阅读维基百科中关于continuation的内容

我认为continuations并不是一门好语言所必需的,但是在函数式语言如Haskell中拥有一级continuations和CPS可以非常有用(智能回溯示例)。


10
关于你提到的第二个问题,为了在一般情况下使用 CPS ,难道不需要进行尾调用优化吗?考虑一个标准函数,你将其包装在“while(!aborted)”块中以使其重复执行直到被中止。相应的CPS风格函数的使用方式不是将其本身作为续体发送吗?这样你会得到无限递归,并且需要进行尾调用优化。我只是在自言自语,如果有不清楚的地方请告诉我 :-) - harms

23

19

抽象延续

延续是一种抽象概念,用于描述控制流语义。在这个意义上,它们存在也不存在(记住,它们是抽象的),在任何提供控制运算符的语言中都是如此(因为任何图灵完备的语言必须如此),就像数字既存在(作为抽象实体)又不存在(作为有形实体)。

延续描述了诸如函数调用/返回、异常处理甚至goto之类的控制效果。一个良好的语言将会在许多方面上设计基于延续的抽象(例如异常)。 (也就是说,一个良好的语言将包含考虑到延续而设计的控制运算符。当然,让语言仅暴露延续作为唯一的控制抽象,允许用户在其之上构建自己的抽象,这也是完全合理的。)

一级延续

如果在一种语言中将续体的概念作为一级对象实现,那么我们就有了一个工具,可以构建各种控制效果。例如,如果一种语言具有一级续体,但没有异常,我们可以在续体之上构建异常。

一级续体的问题

虽然一级续体在许多情况下是一个强大而有用的工具,但在语言中暴露它们也存在一些缺点:

  • 在继续构建的不同抽象层上进行组合可能会导致意外/不直观的行为。 例如,如果我使用继续中止计算,则可能跳过finally块。
  • 如果可以随时请求当前继续,则语言运行时必须结构化,以便可以在任何时间产生当前继续的一些数据结构表示。这对于为好还是为坏经常被认为是“奇特”的功能,会给运行时带来一定的负担。如果语言是托管的(如Clojure托管在JVM上),则该表示必须能够适应主机平台提供的框架。还可能有其他语言希望保持的功能(例如C交互操作),其限制解决方案空间。这些问题增加了“阻抗不匹配”的潜力,并且可能严重复杂化开发性能良好的解决方案。

向语言添加一级继续

通过元编程,可以将对一级继续的支持添加到语言中。 通常,此方法涉及将代码转换为传递风格(CPS),其中当前继续作为显式参数传递给每个函数。

例如,David Nolen的delimc库通过一系列宏转换实现了Clojure程序部分限定续延。类似地,我编写了pulley.cps,这是一个宏编译器,将代码转换为CPS,并提供运行时库以支持更多核心Clojure功能(如异常处理)以及与本地Clojure代码的互操作。
这种方法的一个问题是如何处理本地(Clojure)代码和转换后(CPS)代码之间的边界。具体来说,由于无法捕获本地代码的续延,因此需要禁止(或以某种方式限制)与基础语言的互操作,或者让用户确保上下文允许捕获他们希望捕获的任何续延。 pulley.cps倾向于后者,尽管已经尝试让用户管理这个问题。例如,可以禁止CPS代码调用本地代码。此外,还提供了一种机制来提供现有本地函数的CPS版本。
在具备足够强类型系统(如Haskell)的语言中,可以使用类型系统将可能使用控制操作(即续延)的计算封装到函数式纯代码中。
概要
我们现在有了直接回答您三个问题所需的信息:
1. Clojure由于实际考虑不支持一级续延。 2. 所有语言从理论上都建立在续延之上,但很少有语言将续延作为一级对象公开。但是,可以通过将其转换为CPS等方式将续延添加到任何语言中。 3. 请查看delimc和/或pulley.cps的文档。

花费了将近七年的时间,但我们终于得到了一个正确而全面的答案。太好了!这应该成为被接受的答案。 - Ben Kovitz
@BenKovitz 谢谢。考虑到这个问题的年龄,我曾经犹豫是否提供答案。你的评论让我很高兴我这样做了。 - Nathan Davis

13

语言中是否需要“continuation”这一特性?

不需要。许多语言没有“continuation”的概念。

如果不需要,那你不需要“continuation”吗?我看到很多好的例子,尤其是来自这个人。有什么替代方案吗?

一个调用栈。


7
一个常见的使用continuations的场景是实现控制结构,例如:从函数返回、退出循环、异常处理等。大多数语言(如Java、C++等)将这些功能作为核心语言的一部分提供。有些语言则不提供(例如:Scheme)。相反,这些语言将continuations公开为一级对象,并让程序员定义新的控制结构。因此,Scheme应该被视为一个编程语言工具包,而不是一个完整的语言。
在Clojure中,我们几乎从不需要直接使用continuations,因为几乎所有的控制结构都由语言/VM组合提供。然而,在有能力的程序员手中,一级continuations可以成为强大的工具。特别是在Scheme中,continuations比其他语言中的等效对应物(如C中的setjmp/longjmp对)更好。这篇文章This详细介绍了这一点。
顺便说一下,了解Rich Hickey如何证明他对continuations的看法是很有趣的。有相关链接吗?

1
一个语言要被认为是“完整”的,需要具备哪些特征? - unj2
2
应该是“完整”而不是“全面”。当我说“完整”时,我并不是指图灵完备。我指的是人们通常从“现代”语言中期望的功能。例如,使用try-catch进行异常处理。Scheme没有提供这个功能,但是使用一级延续,您可以实现自己的异常处理机制。 - Vijay Mathew
8
优秀的Lisp程序员通过将语言与问题相结合来编写程序。换句话说,您将Lisp转化为最适合解决手头问题的语言。这是Lisp和其他语言之间的重要区别。Paul Graham在他的书《On Lisp》中详细解释了这一点。我不知道您是谁或从事什么工作,但我猜想您没有使用Lisp编写过任何重要的软件。否则我就不必解释这个了。我自己维护一个Scheme,并每天用它来解决实际问题。 - Vijay Mathew
3
顺便说一下,我认为在称某人的观点为“无意义的胡言乱语”之后,要求他解释自己的观点不是一个好主意。 - Vijay Mathew
8
关于“几乎所有的控制结构都由语言/虚拟机组合提供”,你忘记了补充说没有人会需要超过640kb的内存。 - Eli Barzilay

6

-1
嗯...Clojure的->实现了你想要的东西...但是是使用宏来实现的。

我认为这是一个有效的答案。一个步骤的结果被传递到下一个步骤的输入中。这种效果是通过代码重写实现的连续型机制。 - zcaudate
我不认为->能真正捕捉到OP想要的东西。最好的情况下,->类似于Identity Monad。现在,如果你可以改变->来使用任何任意的Monad,那么我会同意你的看法,认为->足以表达任何使用Continuations的计算(你只需使用Continuation Monad即可)。但是,在Clojure中实现的->没有提供挂起和恢复计算的方法。First-class Continuations使您完全控制计算的哪些部分运行以及何时运行。这在->中完全缺失。 - Nathan Davis

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接