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
。传递给被调用代码的参数之一是返回地址,它显示调用代码的哪一部分尚未执行(**)。调用代码的那部分是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
as 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 ") // 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
将控制权返回给 2nd 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() // 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()
via 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