ChatGPT解决这个技术问题 Extra ChatGPT

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

我刚刚完成Programming in Scala,我一直在研究 Scala 2.7 和 2.8 之间的变化。似乎最重要的是延续插件,但我不明白它有什么用处或它是如何工作的。我已经看到它对异步 I/O 有好处,但我无法找出原因。关于这个主题的一些更受欢迎的资源是:

定界延续和 Scala

转到 Scala

品味 2.8:继续

定界延续解释(在 Scala 中)

还有这个关于 Stack Overflow 的问题:

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

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

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

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


D
Daniel C. Sobral

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

另一个很好的来源(我也在我的博客中指出)是 continuation passing style 上的 Wikipedia 条目。到目前为止,该主题是最清楚的,尽管它不使用 Scala 语法,并且显式传递了延续。

关于定界延续的论文,我在我的博客中链接到,但似乎已经损坏,给出了许多使用示例。

但我认为分隔延续概念的最佳示例是 Scala Swarm。在其中,库在某一时刻停止执行您的代码,剩余的计算成为继续。然后库做一些事情——在这种情况下,将计算转移到另一个主机,并将结果(访问的变量的值)返回给停止的计算。

现在,你连 Scala 页面上的简单示例都看不懂,所以阅读我的博客。其中我关心解释这些基础知识,以及为什么结果是 8


我重新阅读了您的博客条目,这次我坚持了下来——我想我对发生的事情有了更好的了解。我没有从 Wikipedia 页面获得太多信息(我已经知道 Lisp 延续),但重置/移位延迟样式或任何它所称的样式让我感到难过。对于不耐烦的人(即我自己),您的描述还可以,但人们必须确保坚持“重置的结果是移位代码的结果”。段...直到那一刻我绝望地迷失了,但它确实变得更清楚了!我将看看 Swarm,因为我仍然很好奇它的用途。谢谢!
是的,事情开始变得有意义确实需要时间。我觉得我无法更快地做出解释。
当我意识到“重置界定了延续的范围。(即:要包含的变量和语句)时,这一切都为我所用。)
你的解释很冗长,没有达到理解的本质。这些例子很长,我在第一段中没有得到足够的理解来激发我阅读它。所以我投了反对票。 SO 在我投票后显示一条消息,要求我添加评论,所以我遵守了。为我的坦率道歉。
我已经在博客中关注了这一点,重点是理解控制流(没有讨论实现的细节)。 wherenullpoints.com/2014/04/scala-continuations.html
G
George

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

当调用延续函数 cf 时:

执行跳过移位块的其余部分并在其末尾重新开始,传递给 cf 的参数是移位块在执行继续时“评估”的内容。这对于每次调用 cf 可能是不同的 执行一直持续到重置块结束(或者直到调用重置,如果没有块)重置块的结果(或者参数重置()如果没有块) 是什么 cf 在 cf 之后继续执行,直到 shift 块结束执行跳过直到 reset 块结束(或调用 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

当我试图编译它时,我有一个错误说“无法计算 CPS 转换函数结果的类型”。我不确定它是什么,也不确定如何修复它
@Fabio Veronez 在班次末尾添加一个返回语句:将 println(oneHundredOne) } 更改为 println(oneHundredOne); oneHundredOne }
一个可怕的语法很好的解释。延续函数的声明奇怪地与它的主体分离。我不愿意与其他人分享这种令人头疼的代码。
为避免 cannot compute type for CPS-transformed function result 错误,+1 应紧跟在 oneHundredOne} 之后。当前存在于它们之间的注释以某种方式破坏了语法。
S
Shelby Moore III

鉴于 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 插件对这个示例进行了转换,使得从每个 shift 开始到 reset 调用的计算(在 reset 的输入参数内)替换为函数(例如 f ) 输入到 shift

被替换的计算被转移(即移动)到函数k中。函数 f 输入函数 k,其中 k 包含 被替换的计算,k 输入 x: Intk 中的计算将 shift(f) 替换为 {9 }。

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

这与以下效果相同:

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

注意输入参数 x 的类型 Int(即 k 的类型签名)由 f 的输入参数的类型签名给出。

另一个具有概念等效抽象的 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,因此一些读者并不清楚它是否抽象地类似于 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


I am proposingreset/shift 更改为 delimit/replace。按照惯例,freadwithkcallbackreplacedcapturedcontinuationcallback
with 是一个关键字。 PS你的一些重置有()应该是{}无论如何很棒的文章!
@nafg 谢谢,所以我会建议 replacement 而不是 with。 Afaik,() 也被允许? Afaik,{}"Scala's lightweight syntax for closures",它隐藏了底层函数调用。例如,看看 I rewrote Daniel's sequence 如何(请注意,代码从未编译或测试过,所以请随时纠正我)。
一个块——即一个包含多个语句的表达式——需要花括号。
@nafg,正确。 Afaik shift reset 是库函数,而不是关键字。因此,当 function expects only one parameter 时可以使用 {}()。 Scala 具有 By-name 参数(请参见 Programming in Scala,第 2 版,第 218 页的“9.5 控制抽象”部分),如果参数是 () => ... 类型,则可以消除 () =>。我假设 Unit 而不是按名称,因为该块应该在调用 reset 之前评估,但我需要 {} 用于多个语句。我对 shift 的用法是正确的,因为它显然输入了一个函数类型。
s
starblue

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

考虑离开移位表达式和将重置表达式作为函数之间的计算。在移位表达式中,这个函数被称为 k,它是延续。您可以传递它,稍后调用它,甚至不止一次。

我认为重置表达式返回的值是 => 之后的移位表达式内的表达式的值,但对此我不太确定。

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

因此,应该在系统级别上使用延续。将它们洒在你的应用程序代码中肯定会成为噩梦,比使用 goto 的最糟糕的意大利面条代码更糟糕。

免责声明:我对 Scala 中的延续没有深入的了解,我只是通过查看示例和了解 Scheme 中的延续来推断它。


D
Dmitry Bespalov

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

C
Community

另一篇(最近的——2016 年 5 月)关于 Scala 延续的文章是:
Shivansh Srivastava (shiv4nsh) 的“Time Travel in Scala: CPS in Scala (scala’s continuation)”。
它还指 Dmitry Bespalov 中提到的 Jim McBeatharticle answer

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

延续是计算机程序控制状态的抽象表示。所以它实际上的意思是它是一个数据结构,代表了进程执行中给定点的计算过程;创建的数据结构可以被编程语言访问,而不是隐藏在运行时环境中。为了进一步解释,我们可以举一个最经典的例子,假设你在冰箱前的厨房里,想着一个三明治。你在那里拿一个延续,把它放在你的口袋里。然后你从冰箱里拿出一些火鸡和面包,给自己做一个三明治,现在放在柜台上。你调用你口袋里的延续,你发现自己又站在冰箱前,想着一个三明治。不过好在柜台上有一个三明治,所有用来做三明治的材料都没有了。所以你吃它。 :-) 在这个描述中,三明治是程序数据的一部分(例如,堆上的一个对象),而不是调用“制作三明治”例程然后返回,该人称为“制作三明治并具有当前延续”例程,它创建三明治,然后从执行停止的地方继续。

话虽如此,正如 April 2014 for Scala 2.11.0-RC1 中宣布的那样

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


1
18446744073709551615

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 调用为 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 块的结尾不会将控制权返回给第一个 from0to10reset 块的末尾将控制权返回给第二个 from0to10,而第二个 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

现在,让我们通过 onEven() 定义 onOdd()

// 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