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


85

我刚刚完成了Scala编程,并且一直在研究Scala 2.7和2.8之间的变化。似乎最重要的一个是continuations插件,但我不了解它的作用或作用。我已经看到了异步I / O的好处,但是我还无法找出原因。在这个问题上一些比较流行的资源是:

还有关于堆栈溢出的问题:

不幸的是,这些参考文献都没有尝试定义连续的含义或移位/重置功能应该做什么,并且我还没有找到任何能做的参考文献。我一直无法猜测链接文章中的任何示例如何工作(或它们如何工作),因此帮助我的一种方法可能是逐行浏览这些示例之一。即使是第三篇文章中的简单内容:

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

为什么结果是8?那可能会帮助我入门。


Answers:


38

我的博客不会解释什么resetshift做什么,所以你可能需要再次阅读。

我也在我的博客中指出的另一个很好的来源是有关延续传递样式的Wikipedia条目。到目前为止,尽管它没有使用Scala语法,但它是该主题上最清楚的一个,并且延续已显式传递。

我在自己的博客中链接了有关定界延续的论文,但该论文似乎已经损坏了,提供了许多用法示例。

但是我认为定界延续概念的最好例子是Scala Swarm。在其中,库在某一点停止了代码的执行,剩下的计算成为了继续。然后,库执行某些操作-在这种情况下,将计算转移到另一台主机,然后将结果(访问的变量的值)返回到停止的计算。

现在,您甚至都不了解Scala页面上的简单示例,因此阅读我的博客。在本文中,我关注解释这些基础知识,为什么结果是8


我重新阅读了您的博客条目,但这次我坚持使用它–我认为我对发生的事情有了更好的了解。我从Wikipedia页面上没有得到多少(我已经知道Lisp的续篇),但是reset / shift延迟样式或它的任何名称都让我感到沮丧。对于没有耐心的人(即我自己),您的描述还可以,但是人们必须确保坚持“重置的结果是代码内部移位的结果”。段落...在那之前,我无可救药地迷失了自己,但事实确实更加清楚了!我将看看Swarm,因为我仍然好奇这是干什么的。谢谢!
戴夫

是的,确实需要一些时间才能开始变得有意义。我觉得我无法逃脱做出更快的解释。
Daniel C. Sobral

当我意识到“重置限制了延续的范围时(即:要包括的变量和语句。),这一切对我来说都是一样的。”
JeffV

1
您的解释很冗长,没有理解的本质。这些例子很长,在第一段中我没有足够的理解力来激发我阅读所有内容。所以我投了反对票。SO在我投票后显示味精,要求我添加评论,所以我遵守了。抱歉,我很坦白。
谢尔比摩尔三世

1
我已经在博客上发表了这篇文章,重点是了解控制流(不讨论实现的细节)。wherenullpoints.com/2014/04/scala-continuations.html
Alexandros

31

我发现现有的解释在解释这个概念上没有我希望的那么有效。我希望这一点是明确的(并且是正确的。)我还没有使用延续。

cf调用延续函数时:

  1. 执行将跳过shift块的其余部分,并在块的末尾再次开始
    • 继续执行时,传递给参数的cfshift块“求值”的对象。每次致电都会不同cf
  2. 执行将一直持续到reset块结束(或直到reset没有块 的情况下调用)为止
    • reset块的结果reset(如果没有块则为()的参数)是cf返回的结果
  3. 执行一直持续cfshift块结束为止
  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

2
我有一个错误说“无法计算类型CPS转化函数结果”当我试图编译它..我不知道它是什么既不是如何解决它
法比奥Veronez

@Fabio Veronez在班次结束时添加return语句:更改println(oneHundredOne) }println(oneHundredOne); oneHundredOne }
folone

不错的语法解释。连续函数的声明奇怪地从其主体中分离出来。我不愿意与其他人共享这样的令人头疼的代码。
joeytwiddle 2012年

为避免cannot compute type for CPS-transformed function result错误,请+1紧随其后oneHundredOne}。当前在它们之间的注释以某种方式破坏了语法。
lcn

9

研究论文中给出了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

另一个借用的示例具有概念上等效的抽象,即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)
  }
}

我希望这能阐明前后一致的通用抽象,而这两个示例的先前介绍对此有些混淆。例如,规范的第一个示例在研究论文中作为匿名函数而不是我的named提出f,因此对于某些读者而言,尚不清楚read该示例与借用的第二个示例抽象类似。

因此,定界的连续性会产生一种控制反转的错觉,原因是“您从外部叫我” reset”到“我在内部呼叫您reset”。

请注意,的返回类型必须f(但k不是必须)与的返回类型相同reset,即,只要返回与相同的类型,f就可以自由声明任何返回类型。与和同上(另请参见下文)。kfresetreadcaptureENV


定界的连续不会隐式地反转状态控制,例如readcallback它们也不是纯函数。因此,调用者无法创建参照透明的表达式,因此没有对预期的命令式语义声明式(aka透明)控制

我们可以用定界的延续显式地实现纯函数。

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)类型推断取决于不支持钻石的多重虚拟继承


我提议reset/shift更改为delimit/ replace。按照惯例,这freadwith,和kcallbackreplacedcapturedcontinuation,或callback
谢尔比·摩尔三世

with是一个关键字。附言:您的某些重置有()应该是{},无论如何写得好!
nafg 2012年

@nafg谢谢,所以我将提议replacement而不是with。Afaik,()也可以吗?Afaik{}“ Scala的闭包轻量级语法”,它隐藏了底层函数调用。例如,查看sequence如何重写Daniel的代码(请注意,代码从未经过编译或测试,因此请随时纠正我)。
谢尔比·摩尔三世

1
块(即包含多个语句的表达式)需要花括号。
nafg 2012年

@nafg,正确。Afaikshift reset是库函数,而不是关键字。因此当函数只需要一个参数时可以使用{}或。Scala具有“按名称”参数(请参见《 Scala编程》第二版第218页的“ 9.5控制抽象”部分),如果该参数为类型,则可以将其消除。我假设不是按名称命名,因为在调用该块之前应该对该块求值,但是我需要多个语句。我的用法是正确的,因为它显然输入了函数类型。()() => ...() =>Unitreset{}shift
谢尔比·摩尔

8

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

考虑离开移位表达式和离开复位表达式之间的函数计算。在平移表达式中,此函数称为k,它是延续。您可以传递它,稍后调用它,甚至可以多次。

我认为由reset表达式返回的值是=>之后的shift表达式内的表达式的值,但是对此我不太确定。

因此,通过继续操作,您可以在函数中包装相当随意且非本地的代码段。这可用于实现非标准控制流程,例如协程或回溯。

因此,应在系统级别上使用延续。将它们散布在您的应用程序代码中绝对是噩梦的秘诀,比使用goto的最糟糕的意大利面条代码更糟糕。

免责声明:我对Scala中的延续没有深入的了解,我只是通过查看示例并了解Scheme的延续得出的。


5

从我的角度来看,最好的解释在这里给出: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

1

Shivansh Srivastava(撰写的另一篇有关Scala延续的文章(最近-2016年5月)是:
Scala中的时间旅行:Scala中的CPS(scala的延续) ” 。 它还引用了Dmitry Bespalov回答中提到的Jim McBeath文章shiv4nsh

但在此之前,它像这样描述连续性:

延续是计算机程序控制状态的抽象表示
因此,这实际上意味着它是一个数据结构,它表示流程执行过程中给定点的计算流程。可以通过编程语言访问创建的数据结构,而不是将其隐藏在运行时环境中。

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

假设您在冰箱前的厨房里,正在考虑三明治。您可以在那继续拍摄并贴在口袋里。
然后,您从冰箱中取出一些火鸡和面包,然后自己做一个三明治,该三明治现在正坐在柜台上。
您在口袋中调用延续,然后发现自己再次站在冰箱前,正在考虑三明治。但幸运的是,柜台上有一个三明治,所有用于制作它的材料都已用完。所以你吃了。:-)

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

话虽如此,如20144月宣布的Scala 2.11.0-RC1

我们正在寻找维护者来接管以下模块:scala-swingscala-continuations
如果找不到新的维护者,则2.12将不包括它们
我们可能会继续维护其他模块(scala-xml,scala-parser-combinators),但是仍然非常感谢您的帮助。


0

通过有意义的示例进行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) // call the continuation
   }
}

但是延续在哪里结束?这很重要,因为return从延续中获得的最后一个将控制权返回给被调用的代码from0to10。在Scala中,它在reset块结束(*)处结束。

现在,我们看到延续被声明为cont: Int => Unit。为什么?我们将调用from0to10val x = from0to10()和,这Int是到达的值的类型xUnit表示之后的块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不会将控制权返回给first from0to10reset块的末尾将控制权返回到第二个from0to10,而第二个控制权最终又将控制权返回给back,并且是back将控制权返回到的第一次调用from0to10。当第一个(是!第一个!)from0to10退出时,整个reset块都退出。

这种返回控制权的方法称为回溯,这是一种非常古老的技术,至少从Prolog和面向AI的Lisp派生时代就已知道。

名称resetshift是错误的名词。这些名称最好留给按位运算。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 
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.