我刚刚完成 Programming in Scala ,我一直在研究Scala 2.7和2.8之间的变化。似乎最重要的一个是continuation插件,但我不明白它对它有用或它是如何工作的。我已经看到它对异步I / O有好处,但我无法找到原因。关于这个主题的一些更受欢迎的资源是:
关于Stack Overflow的这个问题:
不幸的是,这些引用中没有一个试图定义什么是continuation或者shift / reset函数应该做什么,而且我没有找到任何引用。我无法猜测链接文章中的任何示例是如何工作的(或者他们做了什么),因此帮助我的一种方法可能是逐行浏览其中一个示例。即使是第三篇文章中的简单文章:
reset {
...
shift { k: (Int=>Int) => // The continuation k will be the '_ + 1' below.
k(7)
} + 1
}
// Result: 8
为什么结果为8?这可能会帮助我开始。
答案 0 :(得分:36)
我的blog确实解释了reset
和shift
的作用,因此您可能希望再次阅读。
我在博客中指出的另一个好消息来源是continuation passing style上的维基百科条目。到目前为止,这个问题是最明确的,尽管它没有使用Scala语法,并且显式传递了延续。
关于分隔延续的论文,我在博客中链接但似乎已经破碎,提供了很多用法示例。
但我认为分隔连续的概念的最好例子是Scala Swarm。在其中,库在某一点停止代码的执行,剩下的计算成为延续。然后,库会执行某些操作 - 在这种情况下,将计算转移到另一个主机,并将结果(已访问的变量的值)返回到已停止的计算。
现在,您甚至不了解Scala页面上的简单示例,因此做阅读我的博客。在其中,我仅关注解释这些基础知识,结果为8
。
答案 1 :(得分:31)
我发现现有的解释在解释这个概念方面不如我希望的那么有效。我希望这个清楚(并且正确。)我还没有使用延续。
当调用延续函数cf
时:
shift
块的其余部分,并在其结尾处再次开始
cf
的参数是执行继续时shift
块“评估”的参数。对于cf
reset
块结束(或直到调用reset
,如果没有阻止)
reset
块的结果(如果没有阻止,则为reset
()的参数)cf
返回cf
之后继续执行,直到shift
阻止reset
阻止结束(或重置呼叫?)因此,在此示例中,请遵循A到Z的字母
reset {
// A
shift { cf: (Int=>Int) =>
// B
val eleven = cf(10)
// E
println(eleven)
val oneHundredOne = cf(100)
// H
println(oneHundredOne)
oneHundredOne
}
// C execution continues here with the 10 as the context
// F execution continues here with 100
+ 1
// D 10.+(1) has been executed - 11 is returned from cf which gets assigned to eleven
// G 100.+(1) has been executed and 101 is returned and assigned to oneHundredOne
}
// I
打印:
11
101
答案 2 :(得分:9)
给出了来自research paper的Scala分隔延续的规范示例,稍加修改,因此shift
的函数输入名称为f
,因此不再是匿名的。
def f(k: Int => Int): Int = k(k(k(7)))
reset(
shift(f) + 1 // replace from here down with `f(k)` and move to `k`
) * 2
Scala插件转换此示例,以便计算(在reset
的输入参数内)从每个shift
开始到reset
的调用被替换将函数(例如f
)输入到shift
。
被替换的计算被移位(即移动)到函数k
中。函数f
输入函数k
,其中k
包含替换的计算,k
输入x: Int
,以及计算k
将shift(f)
替换为x
。
f(k) * 2
def k(x: Int): Int = x + 1
具有与以下相同的效果:
k(k(k(7))) * 2
def k(x: Int): Int = x + 1
注意输入参数Int
的类型x
(即k
的类型签名)由f
的输入参数的类型签名给出。< / p>
另一个borrowed示例具有概念上等效的抽象,即read
是shift
的函数输入:
def read(callback: Byte => Unit): Unit = myCallback = callback
reset {
val byte = "byte"
val byte1 = shift(read) // replace from here with `read(callback)` and move to `callback`
println(byte + "1 = " + byte1)
val byte2 = shift(read) // replace from here with `read(callback)` and move to `callback`
println(byte + "2 = " + byte2)
}
我相信这会被转换为逻辑等同于:
val byte = "byte"
read(callback)
def callback(x: Byte): Unit {
val byte1 = x
println(byte + "1 = " + byte1)
read(callback2)
def callback2(x: Byte): Unit {
val byte2 = x
println(byte + "2 = " + byte1)
}
}
我希望这可以阐明连贯的共同抽象,这些抽象通过先前介绍这两个例子而有些混淆。例如,规范的第一个示例在research paper中作为匿名函数呈现,而不是我的名为f
,因此一些读者并不清楚它与{{1}抽象类似在borrowed第二个例子中。
因此,分隔的延续会产生“你从read
之外打电话给我”到“我在reset
内打电话”的控制倒置错觉。
请注意,reset
的返回类型为f
,但k
不需要与reset
的返回类型相同,即f
可以自由只要k
返回与f
相同的类型,就会为reset
声明任何返回类型。同样适用于read
和capture
(另请参阅下面的ENV
)。
定界延续不会隐含地反转状态控制,例如read
和callback
不是纯函数。因此,调用者无法创建引用透明的表达式,因此没有declarative (a.k.a. transparent) control over intended imperative semantics。
我们可以使用分隔连续显式实现纯函数。
def aread(env: ENV): Tuple2[Byte,ENV] {
def read(callback: Tuple2[Byte,ENV] => ENV): ENV = env.myCallback(callback)
shift(read)
}
def pure(val env: ENV): ENV {
reset {
val (byte1, env) = aread(env)
val env = env.println("byte1 = " + byte1)
val (byte2, env) = aread(env)
val env = env.println("byte2 = " + byte2)
}
}
我相信这会被转换为逻辑等同于:
def read(callback: Tuple2[Byte,ENV] => ENV, env: ENV): ENV =
env.myCallback(callback)
def pure(val env: ENV): ENV {
read(callback,env)
def callback(x: Tuple2[Byte,ENV]): ENV {
val (byte1, env) = x
val env = env.println("byte1 = " + byte1)
read(callback2,env)
def callback2(x: Tuple2[Byte,ENV]): ENV {
val (byte2, env) = x
val env = env.println("byte2 = " + byte2)
}
}
}
由于环境明显,这会变得很吵。
切向注意,Scala没有Haskell的全局类型推断,因此据我所知,不能支持隐式提升到状态monad的unit
(作为隐藏显式环境的一种可能策略),因为Haskell的全局(Hindley-Milner)类型推断取决于not supporting diamond multiple virtual inheritance。
答案 3 :(得分:8)
继续捕获计算的状态,稍后再调用。
考虑离开shift表达式并将重置表达式保留为函数之间的计算。在shift表达式中,这个函数叫做k,它是continuation。您可以传递它,稍后调用它,甚至不止一次。
我认为重置表达式返回的值是=&gt;之后的shift表达式内的表达式的值,但关于这一点,我不太确定。
因此,对于continuation,您可以在函数中包含一个相当任意且非本地的代码片段。这可用于实现非标准控制流程,例如协同处理或回溯。
因此应该在系统级别使用continuation。通过你的应用程序代码喷洒它们将是噩梦的必然配方,比使用goto的最糟糕的意大利面条代码更糟糕。
免责声明:我对Scala中的延续没有深入的了解,我只是通过查看示例并了解Scheme中的延续来推断它。
答案 4 :(得分:4)
从我的观点来看,这里给出了最好的解释:http://jim-mcbeath.blogspot.ru/2010/08/delimited-continuations.html
其中一个例子:
为了更清楚地看到控制流程,您可以执行此操作 代码段:
reset {
println("A")
shift { k1: (Unit=>Unit) =>
println("B")
k1()
println("C")
}
println("D")
shift { k2: (Unit=>Unit) =>
println("E")
k2()
println("F")
}
println("G")
}
这是以上代码产生的输出:
A
B
D
E
G
F
C
答案 5 :(得分:1)
关于Scala延续的另一篇(最近的 - 2016年5月)文章是:
“Time Travel in Scala: CPS in Scala (scala’s continuation)”by
Shivansh Srivastava (shiv4nsh
)。
它还指Jim McBeath的article中提到的Dmitry Bespalov answer。
但在此之前,它描述了Continuations,如:
延续是计算机程序控制状态的抽象表示 所以它实际意味着它是一个数据结构,它代表了流程执行过程中给定点的计算过程;创建的数据结构可以通过编程语言访问,而不是隐藏在运行时环境中。
为了进一步解释,我们可以拥有一个最经典的例子,
说你在冰箱前的厨房里,想着一个三明治。你在那里继续,然后把它放在你的口袋里 然后你从冰箱里取出一些火鸡和面包,自己做一个三明治,现在坐在柜台上 你在口袋里调用了延续物,你发现自己再次站在冰箱前面,想着一个三明治。但幸运的是,柜台上有一个三明治,用来制作它的所有材料都不见了。所以你吃它。 : - )
在本说明中,
sandwich
是程序数据的一部分(例如,堆上的对象),而不是调用“make sandwich
”例程和然后回来,这个人称之为“make sandwich with current continuation
”例程,它会创建三明治,然后继续执行。
话虽如此,正如April 2014 for Scala 2.11.0-RC1
所宣布的那样我们正在寻找维护人员来接管以下模块:scala-swing,scala-continuations 如果没有找到新的维护者, 2.12将不包括它们 我们可能会继续维护其他模块(scala-xml,scala-parser-combinators),但仍然非常感谢帮助。
答案 6 :(得分:0)
通过有意义的示例进行标量延续
让我们定义from0to10
来表达从0到10的迭代思想:
def from0to10() = shift { (cont: Int => Unit) =>
for ( i <- 0 to 10 ) {
cont(i)
}
}
现在
reset {
val x = from0to10()
print(s"$x ")
}
println()
打印:
0 1 2 3 4 5 6 7 8 9 10
实际上,我们不需要x
:
reset {
print(s"${from0to10()} ")
}
println()
打印相同的结果。
和
reset {
print(s"(${from0to10()},${from0to10()}) ")
}
println()
打印所有对:
(0,0) (0,1) (0,2) (0,3) (0,4) (0,5) (0,6) (0,7) (0,8) (0,9) (0,10) (1,0) (1,1) (1,2) (1,3) (1,4) (1,5) (1,6) (1,7) (1,8) (1,9) (1,10) (2,0) (2,1) (2,2) (2,3) (2,4) (2,5) (2,6) (2,7) (2,8) (2,9) (2,10) (3,0) (3,1) (3,2) (3,3) (3,4) (3,5) (3,6) (3,7) (3,8) (3,9) (3,10) (4,0) (4,1) (4,2) (4,3) (4,4) (4,5) (4,6) (4,7) (4,8) (4,9) (4,10) (5,0) (5,1) (5,2) (5,3) (5,4) (5,5) (5,6) (5,7) (5,8) (5,9) (5,10) (6,0) (6,1) (6,2) (6,3) (6,4) (6,5) (6,6) (6,7) (6,8) (6,9) (6,10) (7,0) (7,1) (7,2) (7,3) (7,4) (7,5) (7,6) (7,7) (7,8) (7,9) (7,10) (8,0) (8,1) (8,2) (8,3) (8,4) (8,5) (8,6) (8,7) (8,8) (8,9) (8,10) (9,0) (9,1) (9,2) (9,3) (9,4) (9,5) (9,6) (9,7) (9,8) (9,9) (9,10) (10,0) (10,1) (10,2) (10,3) (10,4) (10,5) (10,6) (10,7) (10,8) (10,9) (10,10)
现在,它如何工作?
有称为代码,from0to10
和调用代码。在这种情况下,它是reset
之后的块。传递给被调用代码的参数之一是一个返回地址,该地址显示尚未执行的调用代码的哪一部分(**)。调用代码的那一部分是 continuation 。被调用的代码可以使用该参数执行任何决定:将控制权传递给该参数,或者忽略它,或者多次调用它。在这里from0to10
调用范围为0..10的每个整数的延续。
def from0to10() = shift { (cont: Int => Unit) =>
for ( i <- 0 to 10 ) {
cont(i) // call the continuation
}
}
但是延续在哪里结束?这很重要,因为连续中的最后一个return
将控制权返回给被调用的代码from0to10
。在Scala中,它在reset
块结束(*)处结束。
现在,我们看到延续被声明为cont: Int => Unit
。为什么?我们将from0to10
称为val x = from0to10()
,而Int
是传到x
的值的类型。 Unit
表示reset
之后的块必须不返回任何值(否则将出现类型错误)。通常,有4种类型签名:函数输入,延续输入,延续结果,函数结果。所有这四个必须与调用上下文匹配。
上面,我们打印了一对值。让我们打印乘法表。但是,如何在每行之后输出\n
?
函数back
让我们指定从继续到调用它的代码时,控制权返回时必须执行的操作。
def back(action: => Unit) = shift { (cont: Unit => Unit) =>
cont()
action
}
back
首先调用其延续,然后执行 action 。
reset {
val i = from0to10()
back { println() }
val j = from0to10
print(f"${i*j}%4d ") // printf-like formatted i*j
}
它打印:
0 0 0 0 0 0 0 0 0 0 0
0 1 2 3 4 5 6 7 8 9 10
0 2 4 6 8 10 12 14 16 18 20
0 3 6 9 12 15 18 21 24 27 30
0 4 8 12 16 20 24 28 32 36 40
0 5 10 15 20 25 30 35 40 45 50
0 6 12 18 24 30 36 42 48 54 60
0 7 14 21 28 35 42 49 56 63 70
0 8 16 24 32 40 48 56 64 72 80
0 9 18 27 36 45 54 63 72 81 90
0 10 20 30 40 50 60 70 80 90 100
好吧,现在是时候让一些脑筋急转弯了。 from0to10
有两个调用。第一个from0to10
的延续是什么?它在二进制代码中调用from0to10
之后,但是在源代码中它还包含赋值语句val i =
。它在reset
块结束处结束,但是reset
块的末尾没有将控制权返回到第一个from0to10
。 reset
块的末尾将控制权返回到第二个from0to10
,最终将控制权返回到back
,是back
才将控制权返回给第一个调用from0to10
。当第一个(是!第一个!)from0to10
退出时,整个reset
块都退出了。
这种将控制权返还的方法称为 backtracking ,这是一种非常古老的技术,至少在Prolog和面向AI的Lisp派生时代就知道了。
名称reset
和shift
是错误的名词。这些名称最好留给按位运算。 reset
定义了延续边界,shift
从调用堆栈中取得了延续。
说明
(*)在Scala中,延续在reset
块结束处结束。另一种可能的方法是让它在函数结束处结束。
(**)被调用代码的参数之一是一个返回地址,该地址显示尚未执行的调用代码的哪一部分。在Scala中,一系列返回地址用于此。多少?自进入reset
块以来,所有返回地址都放在调用堆栈中。
UPD 第2部分 放弃继续:过滤
def onEven(x:Int) = shift { (cont: Unit => Unit) =>
if ((x&1)==0) {
cont() // call continuation only for even numbers
}
}
reset {
back { println() }
val x = from0to10()
onEven(x)
print(s"$x ")
}
此打印:
0 2 4 6 8 10
让我们排除两个重要的操作:放弃连续(fail()
)并将控制权传递给它(succ()
):
// fail: just discard the continuation, force control to return back
def fail() = shift { (cont: Unit => Unit) => }
// succ: does nothing (well, passes control to the continuation), but has a funny signature
def succ():Unit @cpsParam[Unit,Unit] = { }
// def succ() = shift { (cont: Unit => Unit) => cont() }
succ()
的两个版本都可以正常工作。事实证明,shift
有一个有趣的签名,尽管succ()
不执行任何操作,但必须具有该签名才能实现类型平衡。
reset {
back { println() }
val x = from0to10()
if ((x&1)==0) {
succ()
} else {
fail()
}
print(s"$x ")
}
按预期,它将打印
0 2 4 6 8 10
在函数内,succ()
是不必要的:
def onTrue(b:Boolean) = {
if(!b) {
fail()
}
}
reset {
back { println() }
val x = from0to10()
onTrue ((x&1)==0)
print(s"$x ")
}
再次打印
0 2 4 6 8 10
现在,让我们通过onOdd()
定义onEven()
:
// negation: the hard way
class ControlTransferException extends Exception {}
def onOdd(x:Int) = shift { (cont: Unit => Unit) =>
try {
reset {
onEven(x)
throw new ControlTransferException() // return is not allowed here
}
cont()
} catch {
case e: ControlTransferException =>
case t: Throwable => throw t
}
}
reset {
back { println() }
val x = from0to10()
onOdd(x)
print(s"$x ")
}
以上,如果x
是偶数,则抛出异常,并且不调用延续。如果x
为奇数,则不引发异常,并且调用延续。
上面的代码显示:
1 3 5 7 9