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

时间:2009-10-03 05:54:38

标签: scala scala-2.8 continuations delimited-continuations

我刚刚完成 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?这可能会帮助我开始。

7 个答案:

答案 0 :(得分:36)

我的blog确实解释了resetshift的作用,因此您可能希望再次阅读。

我在博客中指出的另一个好消息来源是continuation passing style上的维基百科条目。到目前为止,这个问题是最明确的,尽管它没有使用Scala语法,并且显式传递了延续。

关于分隔延续的论文,我在博客中链接但似乎已经破碎,提供了很多用法示例。

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

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

答案 1 :(得分:31)

我发现现有的解释在解释这个概念方面不如我希望的那么有效。我希望这个清楚(并且正确。)我还没有使用延续。

当调用延续函数cf时:

  1. 执行会跳过shift块的其余部分,并在其结尾处再次开始
    • 传递给cf的参数是执行继续时shift块“评估”的参数。对于cf
    • 的每次调用,这可能会有所不同
  2. 执行一直持续到reset块结束(或直到调用reset,如果没有阻止)
    • reset块的结果(如果没有阻止,则为reset()的参数)cf返回
  3. cf之后继续执行,直到shift阻止
  4. 结束
  5. 执行会跳过reset阻止结束(或重置呼叫?)
  6. 因此,在此示例中,请遵循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,以及计算kshift(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示例具有概念上等效的抽象,即readshift的函数输入:

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声明任何返回类型。同样适用于readcapture(另请参阅下面的ENV)。


定界延续不会隐含地反转状态控制,例如readcallback不是纯函数。因此,调用者无法创建引用透明的表达式,因此没有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 McBeatharticle中提到的Dmitry Bespalov answer

但在此之前,它描述了Continuations,如:

  

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

     

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

     

说你在冰箱前的厨房里,想着一个三明治。你在那里继续,然后把它放在你的口袋里   然后你从冰箱里取出一些火鸡和面包,自己做一个三明治,现在坐在柜台上   你在口袋里调用了延续物,你发现自己再次站在冰箱前面,想着一个三明治。但幸运的是,柜台上有一个三明治,用来制作它的所有材料都不见了。所以你吃它。 : - )

     

在本说明中,sandwich程序数据的一部分(例如,堆上的对象),而不是调用“make sandwich”例程和然后回来,这个人称之为“make sandwich with current continuation”例程,它会创建三明治,然后继续执行。

话虽如此,正如April 2014 for Scala 2.11.0-RC1

所宣布的那样
  

我们正在寻找维护人员来接管以下模块:scala-swingscala-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块的末尾没有将控制权返回到第一个from0to10reset块的末尾将控制权返回到第二个from0to10,最终将控制权返回到back,是back才将控制权返回给第一个调用from0to10。当第一个(是!第一个!)from0to10退出时,整个reset块都退出了。

这种将控制权返还的方法称为 backtracking ,这是一种非常古老的技术,至少在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