Raku(do) 所依赖的延续的细节是什么?
What are the specifics about the continuations upon which Raku(do) relies?
在 1990 年代和 2000 年代,编程语言爱好者几乎没有讨论定界延续的话题。它最近重新成为编程语言讨论中的主要内容。
我希望有人至少可以权威地说 Rakudo(与 Raku 相比)的延续是否具有下面列出的六个特征中的每一个。我会在列表后多说一些我希望得到的答案。
从在线消息中逐字引用(经过格式修改)[1] 由负责添加工作的人撰写JVM 的延续:
非对称:当continuation挂起或yield时,执行returns给调用者(Continuation.run()
)。对称延续没有调用者的概念。当它们屈服时,它们必须指定另一个延续以将执行转移到。对称延续和非对称延续都不比彼此更强大,并且每个都可以用来模拟另一个。
Stackful:可以在调用堆栈的任何深度暂停延续,而不是在延续时定界上下文开始的同一子例程中是无堆栈的(就像 C# 中的情况一样)。也就是说,continuation 有自己的堆栈,而不仅仅是一个子程序框架。堆栈延续比无堆栈更强大。
Delimited:continuation 捕获以特定调用开始的执行上下文(在我们的例子中,是某个 runnable 的主体)而不是整个执行状态一直到 main()
。定界的延续严格来说比无定界的更强大 (http://okmij.org/ftp/continuations/undelimited.html), the latter considered "not practically useful" (http://okmij.org/ftp/continuations/against-callcc.html)。
Multi-prompt: Continuations 可以嵌套,在调用堆栈的任何地方,任何封闭的continutions 都可以被挂起。这类似于 try/catch 块的嵌套,并抛出某种类型的异常,将堆栈展开到最近的捕获 处理它 而不仅仅是最近的捕获。嵌套延续的一个例子是在虚拟线程中使用类似 Python 的生成器。生成器代码可以执行阻塞 IO 调用,这将暂停封闭的线程继续,而不仅仅是生成器:https://youtu.be/9vupFNsND6o?t=2188
One-shot/non-reentrant:每次我们继续一个挂起的继续时,它的状态都会发生变化,我们不能从同一个挂起状态多次继续它(即我们不能回到过去)。这与可重入延续不同,在可重入延续中,每次我们暂停它们时,都会 returned 一个代表特定暂停点的新的不可变延续对象。 IE。延续是一个单一的时间点,每次我们继续它,我们都会回到那个状态。可重入延续严格来说比不可重入延续更强大;也就是说,他们可以通过单次连续完成绝对不可能的事情。
Cloneable:如果我们能够克隆一次性延续,我们就可以提供与可重入延续相同的能力。即使每次我们继续它时延续都会发生变化,我们可以在继续创建那个时间点的快照之前克隆它的状态,我们可以 return 到以后。
Aiui 延续没有直接暴露在 Raku 中,因此与 Raku 相关的正确答案(相对于 Rakudo)可能是“没有延续”。但这对我来说并不清楚,所以在下面,如果我很幸运的话,我会在其中描述我希望的答案,我会假装在 Raku 和 Raku 的背景下谈论它们是有意义的和乐道作为两个截然不同的领域。
这是我想象的可能的答案(虽然我只是有点疯狂地猜测什么是真实的):
”作为“100年”语言设计,Raku的当前底层语义[执行?]模型需要,至少,stackless one-shot multi prompt delimited continuations。
从理论上的观点来看,Raku 的设计永远不会扩展到要求延续是可复制的,但它理论上可以展开以要求它们是堆叠的。
Rakudo 实现了当前所需的延续语义。
MoarVM 内置了对这些语义的支持,如果 Raku 的设计如此扩展,可以实际跟踪理论上可能的需求扩展。
JVM 和 JS 后端有合适的 shim 可以实现同样的目的,尽管是以牺牲性能为代价的。 JVM 后端可以切换到使用 JVM 原生的延续似乎是合理的,如果它通过它获得它们,当然前提是它们满足要求,但我目前的印象是它可能实际上可能需要十年在我们需要考虑过那座桥之前离开或更多。"
(或类似的东西。)
如果答案还提供了关于上述内容的更多详细信息,也许是一些代码链接,那将是一个特别棒的补充。
同样,如果一个答案包括几个简短的例子,说明这种持续的力量如何在当前的 Raku 功能中浮出水面,并推测它可能有一天,比如 10 年后,在其他功能中浮出水面,那将使一个过分精彩的答案。
PS。感谢@Larry,他对事物的理解足够深刻,知道延续需要成为图片的一部分;感谢 Stefan O'Rear 的贡献,包括我认为是一次性多提示分隔延续的初步实现;感谢 jnthn 让梦想成真。
脚注
1 目前正在进行将延续作为第一个 class 构造引入 JVM 的工作。这项工作的主要推动者是 Ron Pressler。以上是根据a message he wrote in November.
Rakudo 使用延续作为两个功能的实现策略:
gather
/take
- 用于实现惰性迭代器
- 使线程池上的
await
成为非阻塞
实现的continuations的特性遵循这些语言特性的要求。我将按照与上面略有不同的顺序来介绍它们,因为它便于解释。
- Stackful - 是的,因为我们需要能够在调用堆栈中相对于 [=] 的任何深度执行
take
或 await
10=] 或线程池工作者的工作循环。例如,您可以在 gather
内编写递归图遍历算法,然后在每个遇到的节点内编写 take
。对于 await
,这是 Raku 的 await
和 await
之间区别的核心,正如在许多其他语言中看到的那样:您不必一直重构调用堆栈。
- 定界 - 是的。继续重置操作安装一个标记(或“提示”),当我们进行继续控制操作时,我们在这个分隔符处对堆栈进行切片。我无法想象你如何在没有分隔的情况下实现所涉及的 Raku 功能。
- 多提示 - 是的,这是必需的,因为您可以在另一个
gather
中迭代由 gather
提供的一个数据源实施,或在 gather
. 中执行 await
- Asymmetric - 在继续执行之后,在
reset
指令之后继续执行。在 await
的情况下,我们去 worker 任务队列中寻找另一个任务,在 take
的情况下,我们回到迭代器的 pull-one
方法并且可以 return 取值。我认为这种方法非常适合只有少数功能使用延续的语言。
- One-shot/non-reentrant - 是的,至少在 MoarVM 中,运行时的内存安全取决于此 属性。它由原子比较和交换操作强制执行,因此如果两个线程竞相调用延续,则只有一个可以成功。没有任何 Raku 功能需要可重入延续所暗示的额外复杂性。
- Cloneable - 不,因为没有 Raku 功能需要它。从理论上讲,就说“是的,我们可以做到”而言,这在 MoarVM 中实现并不太糟糕,但我怀疑它会引发很多问题,例如“应该克隆多深”。如果您只是克隆了所有调用记录和类似记录,您仍然会在克隆之间共享
Scalar
容器、Array
s 等。
据我了解——尽管我是从远处观察的——JVM 延续至少部分地针对与 Raku await
机制相同的设计 space,所以我'如果他们最终没有提供 Raku 需要的东西,我会感到惊讶。这显然会简化 Raku 代码到 JVM 的编译(目前它像生成代码一样进行全局 CPS 转换,奇怪的是结果比我预期的要简单),而且它几乎肯定也会表现得更好,因为转换需要从 JIT 编译器的角度来看,可能会掩盖很多事情。
就代码而言,您可以看到 the current continuations implementation, which uses the continuation data structure which in turn has various bits of memory management。在撰写本文时,这些都已作为正在进行的调度程序工作所需的新调用堆栈表示的一部分进行了重大重构;这些更改确实使继续工作更有效率,但不会更改整个操作集。
在 1990 年代和 2000 年代,编程语言爱好者几乎没有讨论定界延续的话题。它最近重新成为编程语言讨论中的主要内容。
我希望有人至少可以权威地说 Rakudo(与 Raku 相比)的延续是否具有下面列出的六个特征中的每一个。我会在列表后多说一些我希望得到的答案。
从在线消息中逐字引用(经过格式修改)[1] 由负责添加工作的人撰写JVM 的延续:
非对称:当continuation挂起或yield时,执行returns给调用者(
Continuation.run()
)。对称延续没有调用者的概念。当它们屈服时,它们必须指定另一个延续以将执行转移到。对称延续和非对称延续都不比彼此更强大,并且每个都可以用来模拟另一个。Stackful:可以在调用堆栈的任何深度暂停延续,而不是在延续时定界上下文开始的同一子例程中是无堆栈的(就像 C# 中的情况一样)。也就是说,continuation 有自己的堆栈,而不仅仅是一个子程序框架。堆栈延续比无堆栈更强大。
Delimited:continuation 捕获以特定调用开始的执行上下文(在我们的例子中,是某个 runnable 的主体)而不是整个执行状态一直到
main()
。定界的延续严格来说比无定界的更强大 (http://okmij.org/ftp/continuations/undelimited.html), the latter considered "not practically useful" (http://okmij.org/ftp/continuations/against-callcc.html)。Multi-prompt: Continuations 可以嵌套,在调用堆栈的任何地方,任何封闭的continutions 都可以被挂起。这类似于 try/catch 块的嵌套,并抛出某种类型的异常,将堆栈展开到最近的捕获 处理它 而不仅仅是最近的捕获。嵌套延续的一个例子是在虚拟线程中使用类似 Python 的生成器。生成器代码可以执行阻塞 IO 调用,这将暂停封闭的线程继续,而不仅仅是生成器:https://youtu.be/9vupFNsND6o?t=2188
One-shot/non-reentrant:每次我们继续一个挂起的继续时,它的状态都会发生变化,我们不能从同一个挂起状态多次继续它(即我们不能回到过去)。这与可重入延续不同,在可重入延续中,每次我们暂停它们时,都会 returned 一个代表特定暂停点的新的不可变延续对象。 IE。延续是一个单一的时间点,每次我们继续它,我们都会回到那个状态。可重入延续严格来说比不可重入延续更强大;也就是说,他们可以通过单次连续完成绝对不可能的事情。
Cloneable:如果我们能够克隆一次性延续,我们就可以提供与可重入延续相同的能力。即使每次我们继续它时延续都会发生变化,我们可以在继续创建那个时间点的快照之前克隆它的状态,我们可以 return 到以后。
Aiui 延续没有直接暴露在 Raku 中,因此与 Raku 相关的正确答案(相对于 Rakudo)可能是“没有延续”。但这对我来说并不清楚,所以在下面,如果我很幸运的话,我会在其中描述我希望的答案,我会假装在 Raku 和 Raku 的背景下谈论它们是有意义的和乐道作为两个截然不同的领域。
这是我想象的可能的答案(虽然我只是有点疯狂地猜测什么是真实的):
”作为“100年”语言设计,Raku的当前底层语义[执行?]模型需要,至少,stackless one-shot multi prompt delimited continuations。
从理论上的观点来看,Raku 的设计永远不会扩展到要求延续是可复制的,但它理论上可以展开以要求它们是堆叠的。
Rakudo 实现了当前所需的延续语义。
MoarVM 内置了对这些语义的支持,如果 Raku 的设计如此扩展,可以实际跟踪理论上可能的需求扩展。
JVM 和 JS 后端有合适的 shim 可以实现同样的目的,尽管是以牺牲性能为代价的。 JVM 后端可以切换到使用 JVM 原生的延续似乎是合理的,如果它通过它获得它们,当然前提是它们满足要求,但我目前的印象是它可能实际上可能需要十年在我们需要考虑过那座桥之前离开或更多。"
(或类似的东西。)
如果答案还提供了关于上述内容的更多详细信息,也许是一些代码链接,那将是一个特别棒的补充。
同样,如果一个答案包括几个简短的例子,说明这种持续的力量如何在当前的 Raku 功能中浮出水面,并推测它可能有一天,比如 10 年后,在其他功能中浮出水面,那将使一个过分精彩的答案。
PS。感谢@Larry,他对事物的理解足够深刻,知道延续需要成为图片的一部分;感谢 Stefan O'Rear 的贡献,包括我认为是一次性多提示分隔延续的初步实现;感谢 jnthn 让梦想成真。
脚注
1 目前正在进行将延续作为第一个 class 构造引入 JVM 的工作。这项工作的主要推动者是 Ron Pressler。以上是根据a message he wrote in November.
Rakudo 使用延续作为两个功能的实现策略:
gather
/take
- 用于实现惰性迭代器- 使线程池上的
await
成为非阻塞
实现的continuations的特性遵循这些语言特性的要求。我将按照与上面略有不同的顺序来介绍它们,因为它便于解释。
- Stackful - 是的,因为我们需要能够在调用堆栈中相对于 [=] 的任何深度执行
take
或await
10=] 或线程池工作者的工作循环。例如,您可以在gather
内编写递归图遍历算法,然后在每个遇到的节点内编写take
。对于await
,这是 Raku 的await
和await
之间区别的核心,正如在许多其他语言中看到的那样:您不必一直重构调用堆栈。 - 定界 - 是的。继续重置操作安装一个标记(或“提示”),当我们进行继续控制操作时,我们在这个分隔符处对堆栈进行切片。我无法想象你如何在没有分隔的情况下实现所涉及的 Raku 功能。
- 多提示 - 是的,这是必需的,因为您可以在另一个
gather
中迭代由gather
提供的一个数据源实施,或在gather
. 中执行 - Asymmetric - 在继续执行之后,在
reset
指令之后继续执行。在await
的情况下,我们去 worker 任务队列中寻找另一个任务,在take
的情况下,我们回到迭代器的pull-one
方法并且可以 return 取值。我认为这种方法非常适合只有少数功能使用延续的语言。 - One-shot/non-reentrant - 是的,至少在 MoarVM 中,运行时的内存安全取决于此 属性。它由原子比较和交换操作强制执行,因此如果两个线程竞相调用延续,则只有一个可以成功。没有任何 Raku 功能需要可重入延续所暗示的额外复杂性。
- Cloneable - 不,因为没有 Raku 功能需要它。从理论上讲,就说“是的,我们可以做到”而言,这在 MoarVM 中实现并不太糟糕,但我怀疑它会引发很多问题,例如“应该克隆多深”。如果您只是克隆了所有调用记录和类似记录,您仍然会在克隆之间共享
Scalar
容器、Array
s 等。
await
据我了解——尽管我是从远处观察的——JVM 延续至少部分地针对与 Raku await
机制相同的设计 space,所以我'如果他们最终没有提供 Raku 需要的东西,我会感到惊讶。这显然会简化 Raku 代码到 JVM 的编译(目前它像生成代码一样进行全局 CPS 转换,奇怪的是结果比我预期的要简单),而且它几乎肯定也会表现得更好,因为转换需要从 JIT 编译器的角度来看,可能会掩盖很多事情。
就代码而言,您可以看到 the current continuations implementation, which uses the continuation data structure which in turn has various bits of memory management。在撰写本文时,这些都已作为正在进行的调度程序工作所需的新调用堆栈表示的一部分进行了重大重构;这些更改确实使继续工作更有效率,但不会更改整个操作集。