首页 文章

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

提问于
浏览
84

我刚刚完成了Programming in Scala,并且我明白它已经看到了它已经找到了原因 . 它已经找到了原因 . 关于这个主题的一些更受欢迎的资源是:

关于Stack Overflow的这个问题:

不幸的是,这些引用中没有一个试图定义什么是continuation或者shift / reset函数应该做什么,而且我没有找到任何引用 . 我无法猜测链接文章中的任何示例是如何工作的(或者他们做了什么),因此帮助我的一种方法可能是逐行浏览其中一个示例 . 即使是第三篇文章中的简单文章:

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

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

6 回答

  • 1

    我的blog确实解释了 resetshift 的作用,所以你可能想再读一遍 .

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

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

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

    现在,您甚至不了解Scala页面上的简单示例,所以请阅读我的博客 . 在其中我只关心解释这些基础知识,为什么结果是 8 .

  • 31

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

    当调用延续函数_2640429时:

    • 执行跳过 shift 块的其余部分,并在其结尾处再次开始

    • 传递给 cf 的参数是 shift 阻止"evaluates"继续执行的内容 . 对于每次拨打 cf ,这可能会有所不同

    • 执行一直持续到 reset 块结束(或者直到调用 reset ,如果没有阻塞)

    • reset 块的结果(如果没有块,则为 reset ()的参数) cf 返回

    • cf 之后继续执行,直到 shift 块结束

    • 执行跳过 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
    
  • 8

    鉴于来自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 的输入参数内)被输入到 shift 的函数(例如 f )替换 .

    被替换的计算被移位(即移动)到函数 k 中 . 函数 f 输入函数 k ,其中 k 包含替换的计算, k 输入 x: Intk 中的计算用 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
    

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

    另一个具有概念等效抽象的示例,即 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 ,因此一些读者并不清楚它是抽象地类似于 readborrowed第二个例子中 .

    因此,分隔的延续创造了一种控制反转的错觉,“你叫我从 reset " to "我叫你在 reset ” .

    注意 f 的返回类型是,但 k 不是,需要与 reset 的返回类型相同,即 f 可以自由地为 k 声明任何返回类型,只要 f 返回与 reset 相同的类型 . 同上 readcapture (另见下面的 ENV ) .


    定界延续不会隐含地反转状态控制,例如 readcallback 不是纯函数 . 因此调用者不能创建引用透明的表达式,因此没有declarative (a.k.a. transparent) control over intended imperative semantics .

    我们可以使用分隔的continuation显式地实现纯函数 .

    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 .

  • 36

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

    考虑离开shift表达式并将重置表达式保留为函数之间的计算 . 在shift表达式中,这个函数叫做k,它是continuation . 您可以传递它,稍后调用它,甚至不止一次 .

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

    因此,通过continuation,您可以在函数中包含一个相当任意且非本地的代码片段 . 这可用于实现非标准控制流程,例如协同处理或回溯 .

    因此,应该在系统级别上使用continuation . 通过你的应用程序代码喷洒它们将是噩梦的必然配方,比使用goto的最糟糕的意大利面条代码更糟糕 .

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

  • 9

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

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

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

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

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

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

相关问题