通过有意义的示例进行Scala延续
让我们定义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
。传递给被调用代码的参数之一是一个返回地址,该地址显示尚未执行的调用代码的哪一部分(**)。调用代码的那一部分是延续。被调用的代码可以使用该参数执行任何决定:将控制权传递给该参数,或者忽略它,或者多次调用它。此处from0to10
为范围为0..10的每个整数调用该延续。
def from0to10() = shift { (cont: Int => Unit) =>
for ( i <- 0 to 10 ) {
cont(i)
}
}
但是延续在哪里结束?这很重要,因为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
首先调用其延续,然后执行操作。
reset {
val i = from0to10()
back { println() }
val j = from0to10
print(f"${i*j}%4d ")
}
它打印:
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
不会将控制权返回给first from0to10
。reset
块的末尾将控制权返回到第二个from0to10
,而第二个控制权最终又将控制权返回给back
,并且是back
将控制权返回到的第一次调用from0to10
。当第一个(是!第一个!)from0to10
退出时,整个reset
块都退出。
这种返回控制权的方法称为回溯,这是一种非常古老的技术,至少从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()
}
}
reset {
back { println() }
val x = from0to10()
onEven(x)
print(s"$x ")
}
打印:
0 2 4 6 8 10
让我们分解出两个重要的操作:丢弃继续(fail()
)和将控制权传递给继续(succ()
):
def fail() = shift { (cont: Unit => Unit) => }
def succ():Unit @cpsParam[Unit,Unit] = { }
的两个版本 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()
以下:
class ControlTransferException extends Exception {}
def onOdd(x:Int) = shift { (cont: Unit => Unit) =>
try {
reset {
onEven(x)
throw new ControlTransferException()
}
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