什么是 Scala 延续以及为什么使用它们?
我刚刚完成Scala 编程,并且我一直在研究Scala 2.7 和 2.8 之间的变化。似乎最重要的是 Continuations 插件,但我不明白它有什么用处或它是如何工作的。我发现它对于异步 I/O 有好处,但我一直无法找出原因。关于该主题的一些更受欢迎的资源如下:
StackOverflow 上的这个问题:
不幸的是,这些参考文献都没有尝试定义延续的用途或移位/重置函数应该做什么,而且我还没有找到任何这样做的参考文献。我无法猜测链接文章中的任何示例如何工作(或它们的作用),因此帮助我的一种方法可能是逐行浏览其中一个示例。即使是第三篇文章中的这个简单的问题:
reset {
...
shift { k: (Int=>Int) => // The continuation k will be the '_ + 1' below.
k(7)
} + 1
}
// Result: 8
为什么结果是 8?这可能会帮助我开始。
I just finished Programming in Scala, and I've been looking into the changes between Scala 2.7 and 2.8. The one that seems to be the most important is the continuations plugin, but I don't understand what it's useful for or how it works. I've seen that it's good for asynchronous I/O, but I haven't been able to find out why. Some of the more popular resources on the subject are these:
- Delimited continuations and Scala
- Goto in Scala
- A Taste of 2.8: Continuations
- Delimited Continuations Explained (in Scala)
And this question on Stack Overflow:
Unfortunately, none of these references try to define what continuations are for or what the shift/reset functions are supposed to do, and I haven't found any references that do. I haven't been able to guess how any of the examples in the linked articles work (or what they do), so one way to help me out could be to go line-by-line through one of those samples. Even this simple one from the third article:
reset {
...
shift { k: (Int=>Int) => // The continuation k will be the '_ + 1' below.
k(7)
} + 1
}
// Result: 8
Why is the result 8? That would probably help me to get started.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(7)
我的博客确实解释了
重置 和
shift
会执行此操作,因此您可能需要再次阅读该内容。另一个很好的来源(我也在我的博客中指出)是关于连续传递样式的维基百科条目。到目前为止,这个是关于这个主题最清晰的,尽管它不使用 Scala 语法,并且延续是显式传递的。
关于定界延续的论文(我在博客中链接到该论文但似乎已损坏)给出了许多用法示例。
但我认为分隔延续概念的最好例子是 Scala Swarm。在其中,库停止代码在某一时刻的执行,而剩余的计算则成为继续。然后,库会执行某些操作 - 在本例中,将计算传输到另一台主机,并将结果(所访问的变量的值)返回到已停止的计算。
现在,您甚至不理解 Scala 页面上的简单示例,所以请阅读我的博客。在其中,我仅关心解释这些基础知识,以及为什么结果是
8
。My blog does explain what
reset
andshift
do, so you may want to read that again.Another good source, which I also point in my blog, is the Wikipedia entry on continuation passing style. That one is, by far, the most clear on the subject, though it does not use Scala syntax, and the continuation is explicitly passed.
The paper on delimited continuations, which I link to in my blog but seems to have become broken, gives many examples of usage.
But I think the best example of the concept of delimited continuations is Scala Swarm. In it, the library stops the execution of your code at one point, and the remaining computation becomes the continuation. The library then does something -- in this case, transferring the computation to another host, and returns the result (the value of the variable which was accessed) to the computation that was stopped.
Now, you don't understand even the simple example on the Scala page, so do read my blog. In it I'm only concerned with explaining these basics, of why the result is
8
.我发现现有的解释在解释这个概念方面不如我希望的那么有效。我希望这一点是清楚的(并且正确的)。我还没有使用过延续。
当调用延续函数
cf
时:shift
块的其余部分,并在其末尾再次开始cf
的参数是shift
块在执行继续时“评估”的结果。每次调用cf
时,这可能会有所不同
reset
块结束(或者直到调用reset
,如果没有堵塞)reset
块的结果(如果没有块,则为reset
() 的参数)是cf
返回的内容李>shift
块结束reset
块结束(或者调用重置?)在
cf
之后继续, 在此示例中,按照从 A 到 Z 的字母打印:
I found the existing explanations to be less effective at explaining the concept than I would hope. I hope this one is clear (and correct.) I have not used continuations yet.
When a continuation function
cf
is called:shift
block and begins again at the end of itcf
is what theshift
block "evaluates" to as execution continues. this can be different for every call tocf
reset
block (or until a call toreset
if there is no block)reset
block (or the parameter toreset
() if there is no block) is whatcf
returnscf
until the end of theshift
blockreset
block (or a call to reset?)So in this example, follow the letters from A to Z
This prints:
给出研究论文中关于 Scala 分隔延续的典型示例,稍加修改,使
shift
的函数输入被命名为f
,因此不再是匿名的。Scala 插件转换此示例,使得从每次
shift
到调用reset
的计算(在reset
的输入参数内)为 < em>替换为shift
输入的函数(例如f
)。替换的计算转移(即移动)到函数
k
中。函数f
输入函数k
,其中k
包含替换的计算,k
> 输入x: Int
,k
中的计算将shift(f)
替换为x
。其效果与以下内容相同:
注意输入参数 x 的类型 Int(即 k 的类型签名)由类型给出
f
输入参数的签名。另一个 借用 具有概念上等效抽象的示例,即
read
是shift
的函数输入:我相信这将被翻译为逻辑上的等价物:
我希望这阐明了连贯的公共抽象,而先前的这两个示例的演示在某种程度上使该抽象变得模糊不清。例如,规范的第一个示例在研究论文 作为一个匿名函数,而不是我命名的
f
,因此一些读者并不能立即清楚它抽象地类似于 借用第二个示例。因此,分隔的延续创造了一种控制反转的错觉,从“你从
reset
外部呼叫我”到“我在reset
内部呼叫你”。注意
f
的返回类型是,而k
不是,要求与reset
的返回类型相同,即f
可以自由地为k
声明任何返回类型,只要f
返回与reset
相同的类型。读取
和捕获
也是如此(另请参阅下面的ENV
)。分隔延续不会隐式反转状态控制,例如,read 和callback 不是纯函数。因此,调用者无法创建引用透明的表达式,因此无法对预期的命令进行声明式(也称为透明)控制语义。
我们可以显式地实现带有分隔延续的纯函数。
我相信这将被翻译成逻辑上的等价物:
由于显式环境,这变得越来越嘈杂。
顺便指出,Scala 没有 Haskell 的全局类型推断,因此据我所知无法支持隐式提升到状态 monad 的
unit
(作为隐藏显式环境的一种可能策略),因为 Haskell 的全局 (Hindley-Milner) 类型推断取决于不支持钻石多重虚拟继承。Given the canonical example from the research paper for Scala's delimited continuations, modified slightly so the function input to
shift
is given the namef
and thus is no longer anonymous.The Scala plugin transforms this example such that the computation (within the input argument of
reset
) starting from eachshift
to the invocation ofreset
is replaced with the function (e.g.f
) input toshift
.The replaced computation is shifted (i.e. moved) into a function
k
. The functionf
inputs the functionk
, wherek
contains the replaced computation,k
inputsx: Int
, and the computation ink
replacesshift(f)
withx
.Which has the same effect as:
Note the type
Int
of the input parameterx
(i.e. the type signature ofk
) was given by the type signature of the input parameter off
.Another borrowed example with the conceptually equivalent abstraction, i.e.
read
is the function input toshift
:I believe this would be translated to the logical equivalent of:
I hope this elucidates the coherent common abstraction which was somewhat obfuscated by prior presentation of these two examples. For example, the canonical first example was presented in the research paper as an anonymous function, instead of my named
f
, thus it was not immediately clear to some readers that it was abstractly analogous to theread
in the borrowed second example.Thus delimited continuations create the illusion of an inversion-of-control from "you call me from outside of
reset
" to "I call you insidereset
".Note the return type of
f
is, butk
is not, required to be the same as the return type ofreset
, i.e.f
has the freedom to declare any return type fork
as long asf
returns the same type asreset
. Ditto forread
andcapture
(see alsoENV
below).Delimited continuations do not implicitly invert the control of state, e.g.
read
andcallback
are not pure functions. Thus the caller can not create referentially transparent expressions and thus does not have declarative (a.k.a. transparent) control over intended imperative semantics.We can explicitly achieve pure functions with delimited continuations.
I believe this would be translated to the logical equivalent of:
This is getting noisy, because of the explicit environment.
Tangentially note, Scala does not have Haskell's global type inference and thus as far as I know couldn't support implicit lifting to a state monad's
unit
(as one possible strategy for hiding the explicit environment), because Haskell's global (Hindley-Milner) type inference depends on not supporting diamond multiple virtual inheritance.延续捕获计算的状态,以便稍后调用。
将移位表达式和重置表达式保留为函数之间的计算。在移位表达式中,这个函数称为 k,它是延续。您可以传递它,稍后调用它,甚至不止一次。
我认为重置表达式返回的值是 => 之后的移位表达式内的表达式的值,但对此我不太确定。
因此,通过延续,您可以将一段相当任意且非本地的代码包装在函数中。这可用于实现非标准控制流,例如协程或回溯。
因此应该在系统级别使用延续。将它们散布在应用程序代码中肯定会带来噩梦,比使用 goto 的最糟糕的意大利面条代码更糟糕。
免责声明:我对Scala中的延续没有深入的了解,我只是通过查看示例并了解Scheme的延续来推断。
Continuation capture the state of a computation, to be invoked later.
Think of the computation between leaving the shift expression and leaving the reset expression as a function. Inside the shift expression this function is called k, it is the continuation. You can pass it around, invoke it later, even more than once.
I think the value returned by the reset expression is the value of the expression inside the shift expression after the =>, but about this I'm not quite sure.
So with continuations you can wrap up a rather arbitrary and non-local piece of code in a function. This can be used to implement non-standard control flow, such as coroutining or backtracking.
So continuations should be used on a system level. Sprinkling them through your application code would be a sure recipe for nightmares, much worse than the worst spaghetti code using goto could ever be.
Disclaimer: I have no in depth understanding of continuations in Scala, I just inferred it from looking at the examples and knowing continuations from Scheme.
从我的角度来看,这里给出了最好的解释: http:// jim-mcbeath.blogspot.ru/2010/08/delimited-continuations.html
示例之一:
From my point of view, the best explanation was given here: http://jim-mcbeath.blogspot.ru/2010/08/delimited-continuations.html
One of examples:
另一篇关于 Scala 延续的文章(最近的 - 2016 年 5 月)是:
"时间旅行in Scala:Scala 中的 CPS(scala 的延续)”
Shivansh Srivastava (
shiv4nsh
)。它还引用了 Jim McBeath 的 Dmitry Bespalov< 中提到的文章 /a> 的答案。
但在此之前,它是这样描述Continuations的:
话虽这么说,正如 2014 年 4 月,Scala 2.11.0-RC1
Another (more recent -- May 2016) article on Scala continuations is:
"Time Travel in Scala: CPS in Scala (scala’s continuation)" by
Shivansh Srivastava (
shiv4nsh
).It also refers to Jim McBeath's article mentioned in Dmitry Bespalov's answer.
But before that, it describes Continuations like so:
That being said, as announced in April 2014 for Scala 2.11.0-RC1
通过有意义的示例实现 Scala Continuations
让我们定义
from0to10
来表达从 0 到 10 的迭代思想:现在,
输出:
事实上,我们不需要
x< /code>:
打印相同的结果。
并
打印所有对:
现在,这是如何工作的?
有被调用代码、
from0to10
和调用代码。在本例中,它是reset
之后的块。传递给被调用代码的参数之一是返回地址,它显示调用代码的哪一部分尚未执行 (**)。调用代码的该部分是延续。被调用的代码可以对该参数执行任何它决定的操作:将控制权传递给它,或忽略,或多次调用它。这里from0to10
为 0..10 范围内的每个整数调用该延续。但延续到哪里结束呢?这很重要,因为延续中的最后一个
返回
将控制权返回给被调用的代码from0to10
。在 Scala 中,它在reset
块结束处结束 (*)。现在,我们看到延续被声明为
cont: Int =>;单位
。为什么?我们将from0to10
调用为val x = from0to10()
,而Int
是转到x
的值类型。Unit
表示reset
后的块必须不返回任何值(否则会出现类型错误)。一般来说,有 4 种类型签名:函数输入、延续输入、延续结果、函数结果。所有四个都必须与调用上下文匹配。上面,我们打印了值对。让我们打印乘法口诀表。但是我们如何在每一行之后输出
\n
呢?函数
back
让我们指定当控制返回时必须执行的操作,从延续到调用它的代码。back
首先调用其延续,然后执行操作。它打印出:
好吧,现在是时候做一些脑筋急转弯了。有两次
from0to10
调用。第一个from0to10
的延续是什么?它遵循二进制代码中from0to10
的调用,但在源代码中它还包含赋值语句val i =
。它在reset
块结束处结束,但reset
块的末尾不会将控制返回给第一个from0to10
。reset
块的末尾将控制权返回给第二个from0to10
,后者最终将控制权返回给back
,它是back
将控制权返回给第一次调用from0to10
。当第一个(是的!第一个!)from0to10
退出时,整个reset
块就会退出。这种返回控制权的方法称为回溯,这是一种非常古老的技术,至少从 Prolog 和面向人工智能的 Lisp 衍生产品时代就为人所知。
reset
和shift
这两个名称用词不当。这些名称最好留给按位运算。reset
定义延续边界,而shift
从调用堆栈中获取延续。注意 (
*) 在 Scala 中,延续在
reset
块结束的地方结束。另一种可能的方法是让它在函数结束的地方结束。(**) 被调用代码的参数之一是返回地址,它显示调用代码的哪一部分尚未执行。在 Scala 中,返回地址序列用于那。多少?自进入
reset
块以来放置在调用堆栈上的所有返回地址。UPD 第 2 部分
丢弃延续:过滤
这将打印:
让我们分解两个重要的操作:丢弃延续 (
fail()
) 并将控制权传递给它 (succ()
):两个版本的
succ()
(上面)都可以工作。事实证明,shift
有一个有趣的签名,尽管succ()
不执行任何操作,但它必须具有该签名以实现类型平衡。正如预期的那样,它打印出
Within a function,
succ()
is not required:再次,它打印出来
现在,让我们通过
onEven()< 定义
onOdd()
/code>:上面,如果
x
是偶数,则抛出异常并且不调用延续;如果 x 为奇数,则不会引发异常并调用延续。上面的代码打印:
Scala Continuations via Meaningful Examples
Let us define
from0to10
that expresses the idea of iteration from 0 to 10:Now,
prints:
In fact, we do not need
x
:prints the same result.
And
prints all pairs:
Now, how does that work?
There is the called code,
from0to10
, and the calling code. In this case, it is the block that followsreset
. One of the parameters passed to the called code is a return address that shows what part of the calling code has not yet been executed (**). That part of the calling code is the continuation. The called code can do with that parameter whatever it decides to: pass control to it, or ignore, or call it multiple times. Herefrom0to10
calls that continuation for each integer in the range 0..10.But where does the continuation end? This is important because the last
return
from the continuation returns control to the called code,from0to10
. In Scala, it ends where thereset
block ends (*).Now, we see that the continuation is declared as
cont: Int => Unit
. Why? We invokefrom0to10
asval x = from0to10()
, andInt
is the type of value that goes tox
.Unit
means that the block afterreset
must return no value (otherwise there will be a type error). In general, there are 4 type signatures: function input, continuation input, continuation result, function result. All four must match the invocation context.Above, we printed pairs of values. Let us print the multiplication table. But how do we output
\n
after each row?The function
back
lets us specify what must be done when control returns back, from the continuation to the code that called it.back
first calls its continuation, and then performs the action.It prints:
Well, now it's time for some brain-twisters. There are two invocations of
from0to10
. What is the continuation for the firstfrom0to10
? It follows the invocation offrom0to10
in the binary code, but in the source code it also includes the assignment statementval i =
. It ends where thereset
block ends, but the end of thereset
block does not return control to the firstfrom0to10
. The end of thereset
block returns control to the 2ndfrom0to10
, that in turn eventually returns control toback
, and it isback
that returns control to the first invocation offrom0to10
. When the first (yes! 1st!)from0to10
exits, the wholereset
block is exited.Such method of returning control back is called backtracking, it is a very old technique, known at least from the times of Prolog and AI-oriented Lisp derivatives.
The names
reset
andshift
are misnomers. These names should better have been left for the bitwise operations.reset
defines continuation boundaries, andshift
takes a continuation from the call stack.Note(s)
(*) In Scala, the continuation ends where the
reset
block ends. Another possible approach would be to let it end where the function ends.(**) One of the parameters of the called code is a return address that shows what part of the calling code has not yet been executed. Well, in Scala, a sequence of return addresses is used for that. How many? All of the return addresses placed on the call stack since entering the
reset
block.UPD Part 2
Discarding Continuations: Filtering
This prints:
Let us factor out two important operations: discarding the continuation (
fail()
) and passing control on to it (succ()
):Both versions of
succ()
(above) work. It turns out thatshift
has a funny signature, and althoughsucc()
does nothing, it must have that signature for type balance.as expected, it prints
Within a function,
succ()
is not necessary:again, it prints
Now, let us define
onOdd()
viaonEven()
:Above, if
x
is even, an exception is thrown and the continuation is not called; ifx
is odd, the exception is not thrown and the continuation is called.The above code prints: