Raku(do) 所依赖的延续的具体细节是什么?

rai*_*iph 14 rakudo delimited-continuations raku

在 1990 年代和 2000 年代,编程语言爱好者几乎没有讨论过分隔延续的话题。它最近重新成为编程语言讨论中的一个主要问题。

我希望有人至少可以权威地说出 Rakudo 背后的延续(与 Raku 相比)是否具有下面列出的六个特征中的每一个。在列表之后,我会多说一些我希望得到的答案。

从在线消息[1] 中逐字引用(带有格式修饰),该消息由推动向 JVM 添加延续的工作的人撰写:

  • 非对称:当延续挂起或让步时,执行返回到调用者(of Continuation.run())。对称延续没有调用者的概念。当他们让步时,他们必须指定另一个延续以将执行转移到。对称延续和非对称延续都不比彼此更强大,每个都可以用来模拟另一个。

  • Stackful:延续可以在调用堆栈中的任何深度挂起,而不是在延续是无堆栈时(如 C# 中的情况)在分隔上下文开始的同一子例程中。即延续有自己的堆栈,而不仅仅是单个子程序帧。有堆栈的延续比无堆栈的更强大。

  • Delimited:continuation 捕获从特定调用开始的执行上下文(在我们的例子中,是某个 runnable 的主体),而不是一直到 的整个执行状态main()。带分隔符的延续严格来说比未分隔的延续更强大 ( http://okmij.org/ftp/continuations/undelimited.html ),后者被认为“没有实际用处”( http://okmij.org/ftp/continuations/against- callcc.html)。

  • Multi-prompt:Continuations 可以嵌套,并且在调用堆栈中的任何地方,任何封闭的 continuations 都可以被挂起。这类似于 try/catch 块的嵌套,并抛出某种类型的异常,将堆栈展开到最近的处理它的catch而不仅仅是最近的 catch。嵌套延续的一个例子是在虚拟线程中使用类似 Python 的生成器。生成器代码可以做一个阻塞 IO 调用,这将挂起封闭线程的延续,而不仅仅是生成器:https : //youtu.be/9vupFNsND6o?t=2188

  • 一次性/不可重入:每次我们继续一个暂停的延续时,它的状态都会发生变化,我们不能从同一个暂停状态多次继续它(即我们不能回到过去)。这与可重入延续不同,每次我们暂停它们时,都会返回一个表示特定暂停点的新的不可变延续对象。即延续是一个时间点,每次我们继续它时,我们都会回到那个状态。可重入的延续严格来说比不可重入的更强大;即他们可以做一些完全不可能的事情,只需一次连续即可。

  • Cloneable:如果我们能够克隆一次性延续,我们可以提供与可重入延续相同的能力。即使每次我们继续它时延续都会发生变化,我们可以在继续创建该时间点的快照之前克隆它的状态,以便稍后返回。


Aiui continuations 没有直接暴露在 Raku 中,所以可能与 Raku 相关的正确答案(与 Rakudo 相对)是“没有延续”。但这对我来说并不清楚,所以在下文中,如果我很幸运,我会在其中描述我希望答案中的内容,我会假装在 Raku 的上下文中谈论它们是有道理的和乐堂是两个截然不同的领域。

这是我想象的那种答案是可能的(尽管我只是有点疯狂地猜测实际情况):

  • “作为“100 年”语言设计,Raku当前的底层语义 [执行?] 模型至少需要无堆栈的一次性多提示分隔的延续。

  • 从理论上的观点来看,Raku 的设计永远不能扩展到要求延续是可克隆的,但理论上可以扩展到要求它们是可堆叠的。

  • Rakudo 实现了当前所需的延续语义。

  • MoarVM 内置了对这些语义的支持,如果 Raku 的设计如此扩展,它可以实际跟踪理论上可能的需求扩展。

  • JVM 和 JS 后端有合适的 shims 来实现相同的目标,尽管会降低性能。JVM 后端可以切换到使用 JVM 原生的延续(如果它得到它们)似乎是合理的,当然前提是它们满足要求,但我目前的印象是它实际上可能需要十年在我们需要考虑过那座桥之前,离开或更多。”

(或者类似的东西。)

如果答案还提供了有关上述内容的更多详细信息,也许是一些代码链接,那将是一个特别棒的补充。

类似地,如果答案包括几个简短的例子,说明这种持续能力如何在当前的 Raku 特征中出现,并推测它可能有一天,比如说 10 年后,如何在其他特征中出现,那么答案就会变得过分——顶级的辉煌。

附注。感谢@Larry,他对事物的理解足够深刻,知道需要成为图片的一部分的延续;感谢 Stefan O'Rear 的贡献,包括我认为是一次性多提示分隔延续的初始实现;并感谢 jnthn 让梦想成真。

脚注

1目前正在进行将延续作为第一类构造引入 JVM 的工作。这项工作的主要推动者是 Ron Pressler。以上是根据他在11月写的一条消息

Jon*_*ton 12

Rakudo 使用 continuation 作为两个特性的实现策略:

  • gather/ take- 用于实现惰性迭代器
  • 使await线程池非阻塞

实现的延续的特性遵循这些语言特性的要求。我将以与上面略有不同的顺序浏览它们,因为它易于解释。

  • Stackful -是的,因为我们需要能够做的take还是await在相对于调用堆栈中的任何深度gather或线程池工人的工作循环。例如,您可以在 a 中编写递归图遍历算法gather,然后在take每个遇到的节点中编写。对于await,这是 Rakuawaitawait许多其他语言之间差异的核心:您不必一直向上重构调用堆栈。
  • 分隔- 是的。延续重置操作会安装一个标签(或“提示”),当我们进行延续控制操作时,我们会在这个分隔符处对堆栈进行切片。我无法想象如何在没有分隔的情况下实现所涉及的 Raku 功能。
  • 多提示- 是的,这是必需的,因为您可以迭代gather另一个gather实现内部提供的数据源,或者awaitgather.
  • 非对称- 执行延续后,在reset指令之后继续执行。在这种await情况下,我们去工作任务队列中寻找另一个任务,在这种take情况下,我们回到pull-one迭代器的方法中,可以返回获取的值。我认为这种方法非常适合只有少数功能使用延续的语言。
  • 一次性/不可重入- 是的,至少在 MoarVM 中,运行时的内存安全取决于此属性。它由原子比较和交换操作强制执行,因此如果两个线程要竞争调用延续,则只有一个线程会成功。没有 Raku 功能需要可重入延续所暗示的额外复杂性。
  • 可克隆- 不,因为没有 Raku 功能需要它。从理论上讲,就说“是的,我们可以做到”而言,这在 MoarVM 中实现并不是太糟糕,但我怀疑它会引发很多问题,例如“应该克隆多深”。如果你只是克隆了所有的调用记录和类似的东西,你仍然会在克隆之间共享Scalar容器、Arrays 等。

据我了解 - 尽管我是从远处观察 - JVM 延续至少部分针对与 Rakuawait机制相同的设计空间,所以如果他们最终没有提供 Raku 需要的东西,我会感到惊讶. 这显然会简化 Raku 代码到 JVM 的编译(目前它在执行代码生成时执行全局 CPS 转换,奇怪的是结果比我预期的要简单),而且几乎可以肯定它的性能也会好得多,因为需要转换从 JIT 编译器的角度来看,可能会掩盖很多事情。

就代码而言,您可以看到当前的 continuations 实现,它使用continuation 数据结构,而该结构又具有各种内存管理位。在撰写本文时,这些都已被显着重构,作为正在进行的调度程序工作所需的新调用堆栈表示的一部分;这些更改确实使使用 continuation 更有效率,但不会更改整个操作集。

  • 啊,我提到的两个功能确实是连续使用的东西的完整列表。:-) 异常在堆栈顶部运行,并且我们在运行“CATCH”块之后展开,因此可恢复的异常只不过是不进行展开,因此不需要继续。 (4认同)
  • 谢谢你!我*认为*你是说 Raku 的延续一定是 **不对称**,但我不是 100% 确定。另外,据我所知,整个异常机制(尤其是警告后的“.resume”等)都依赖于这些延续,对吧?所以 `gather` / `take` 和 `await` 只是几个/许多例子中的两个,对吧? (2认同)
  • @raiph 正如您在原来的帖子中所指出的,“对称延续和不对称延续都不比彼此更强大”,在这种情况下,这里没有“必须”。我之前没有考虑过对称方法,尽管我并没有真正看到它在实现 Raku 的背景下可能提供什么优势(如果你的顶层可能是一个事件循环,我可以看到它......) (2认同)