什么是Scala的延续,为什么使用它们?

我刚刚完成了Scala编程 ,我正在研究Scala 2.7和2.8之间的变化。 看起来最重要的是continuation插件,但是我不明白它对于它的作用有多大用处。 我已经看到,这对于asynchronousI / O非常有用,但我一直无法找出原因。 这方面一些比较受欢迎的资源是:

  • 划定延续和斯卡拉
  • 转到斯卡拉
  • 2.8的味道:延续
  • 定界延续解释(在斯卡拉)

而这个问题堆栈溢出:

  • Scala 2.8和Scala 2.7最大的区别是什么?

不幸的是,这些参考文献都没有试图定义什么延续或者什么是移位/重置function,我还没有find任何参考。 我还没有猜到链接文章中的任何例子是如何工作的(或者他们做了什么),所以帮助我的一种方法可能是通过其中一个样本逐行进行。 即使是第三篇文章中的这个简单的一个:

reset { ... shift { k: (Int=>Int) => // The continuation k will be the '_ + 1' below. k(7) } + 1 } // Result: 8 

为什么是结果8? 这可能会帮助我开始。

我的博客确实解释了什么是resetshift ,所以你可能想再次阅读。

我在博客中提到的另一个很好的来源是维基百科关于延续传球的文章 。 到目前为止,这是一个最清晰的主题,虽然它不使用Scala语法,并且明确地传递了延续。

关于我在博客中链接但似乎已经被破坏的分隔延续的论文给出了很多使用的例子。

但是我认为划定延续概念的最好例子是Scala Swarm。 其中,图书馆停止执行你的代码,其余的计算成为延续。 然后,库执行一些操作 – 在这种情况下,将计算传输到另一个主机,并将结果(被访问的variables的值)返回到已停止的计算。

现在,你甚至不明白Scala页面上的简单例子,所以阅读我的博客。 其中我关心解释这些基础知识,为什么结果是8

我发现现有的解释在解释这个概念方面比我所希望的要less。 我希望这个清楚(而且正确)。我还没有使用延续。

当一个连续函数cf被调用时:

  1. 执行跳过shift块的其余部分,并在结束时重新开始
    • 传递给cf的参数是shift块“评估”为执行的持续时间。 对于cf每一个调用,这可以是不同的
  2. 执行继续,直到reset块结束(或者直到没有块的reset调用)
    • reset块的结果(或者如果没有块则返回reset ()的参数)是cf返回的结果
  3. cf之后继续执行,直到shift块结束
  4. 执行跳过直到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 

继续捕获计算的状态,稍后调用。

考虑离开移位expression式和保留复位expression式作为函数之间的计算。 在shiftexpression式中,这个函数被称为k,它是延续。 你可以传递它,稍后调用,甚至不止一次。

我认为重置expression式返回的值是=>后面的expression式中的expression式的值,但是对此我不太确定。

所以通过延续,你可以在一个函数中包装一个相当随意的,非本地的代码片断。 这可以用来实现非标准的控制stream程,如协调或回溯。

所以应该在系统级别使用延续。 通过你的应用程序代码喷洒他们将是一个噩梦的肯定秘诀,比使用goto最糟糕的意大利面代码更糟糕。

免责声明:我没有深入理解Scala中的延续,我只是从计划中的例子和继续中推断出它。

鉴于Scala定界延续的研究论文中的典型例子,稍作修改,所以给shift的函数input名称为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插件转换这个例子,使得从每次shiftreset调用的计算(在reset的input参数内)被replace为用于shift的函数(例如f )。

被replace的计算被移位 (即移动)为函数k 。 函数finput函数k ,其中k 包含被replace的计算, kinputx: Int ,并且k的计算用x代替shift(f)

 f(k) * 2 def k(x: Int): Int = x + 1 

其效果如下:

 k(k(k(7))) * 2 def k(x: Int): Int = x + 1 

注意input参数x的typesInt (即k的types签名)由f的input参数的types签名给出。

另一个借用概念等价抽象的例子,即readshift的函数input:

 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) } } 

我希望通过事先介绍这两个例子来澄清一些有趣的共同抽象。 例如,规范的第一个例子在研究论文中被作为一个匿名函数提出,而不是我的f ,因此,一些读者不清楚它是否与借用的第二个例子中的read类似。

因此,划定的延续会产生一种由“你从调整范围之外叫我”到“我叫你在调整中”的控制反转的错觉。

注意f的返回types是k ,但是k不是必须和reset的返回types相同,即只要f返回与reset相同的types, f就可以自由地为k声明任何返回types。 同上readcapture (另请参阅下面的ENV )。


分隔延续不会隐式地反转状态的控制,例如readcallback不是纯函数。 因此,调用者不能创build引用透明的expression式,因此不具有针对预期的命令性语义的声明性(又称透明)控制 。

我们可以明确地实现带有分隔延续的纯函数。

 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) } } } 

由于明确的环境,这变得嘈杂。

切向地指出,斯卡拉没有Haskell的全局types推断,因此据我所知,不能支持隐式提升到一个unit的状态(作为隐藏显式环境的一个可能的策略),因为哈斯克尔的全球(Hindley-Milner)types推论依赖于不支持钻石的多重虚拟inheritance 。

从我的angular度来看,最好的解释是在这里给出的: http : //jim-mcbeath.blogspot.ru/2010/08/delimited-continuations.html

其中一个例子:

要更清楚地看到控制stream程,可以执行以下代码片段:

 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 

另一篇(更近期 – 2016年5月)关于Scala延续的文章是:
“ Scala中的时间旅行:Scala中的CPS(scala的延续) ” Shivansh Srivastava( shiv4nsh ) 。
它也提到了Jim McBeath在Dmitry Bespalov的回答中提到的文章 。

但在此之前,它是这样描述Continuations的:

延续是计算机程序控制状态的抽象表示
所以它实际上意味着它是一个数据结构,它代表了进程执行过程中给定点的计算过程; 所创build的数据结构可以被编程语言访问,而不是隐藏在运行时环境中。

为了进一步解释,我们可以有一个最经典的例子,

假设你在冰箱前的厨房里,想着三明治。 你在那里继续下去,把它放在你的口袋里。
然后,你从冰箱里取出一些火鸡和面包,做一个三明治,现在坐在柜台上。
你在口袋里伸出援手,发现自己又站在冰箱前,想着三明治。 幸运的是,柜台上有一个三明治,所有用来制作它的材料都没有了。 所以你吃了 🙂

在这个描述中, sandwich程序数据的一部分(例如,堆上的一个对象),而不是调用“ make sandwich ”程序然后返回,这个人称为“ make sandwich with current continuation ”程序,创build三明治,然后继续执行停止的地方。

这就是说,正如在2014年4月宣布的Scala 2.11.0-RC1

我们正在寻找维护者接pipe以下模块: scala-swing , scala-continuations 。
如果没有find新的维护者,2.12将不包括他们
我们可能会继续维护其他模块(scala-xml,scala-parser-combinators),但仍然不胜感激。

Interesting Posts