【问题标题】:Continuation-passing style in ScalaScala 中的延续传递风格
【发布时间】:2016-06-12 21:09:02
【问题描述】:

我从表面上阅读了几篇关于延续传递风格的博客文章/维基百科。我的高级目标是找到一种系统化的技术来使任何递归函数(或者,如果有限制,请注意它们)尾递归。但是,我很难表达我的想法,我不确定我的尝试是否有意义。

出于示例的目的,我将提出一个简单的问题。目标是,给定一个唯一字符的排序列表,按字母顺序输出由这些字符组成的所有可能的单词。例如,sol("op".toList, 3) 应该返回 ooo,oop,opo,opp,poo,pop,ppo,ppp

我的递归解决方案如下:

def sol(chars: List[Char], n: Int) = {
    def recSol(n: Int): List[List[Char]] = (chars, n) match {
        case (_  , 0) => List(Nil)
        case (Nil, _) => Nil
        case (_  , _) =>
            val tail = recSol(n - 1)
            chars.map(ch => tail.map(ch :: _)).fold(Nil)(_ ::: _)
    }
    recSol(n).map(_.mkString).mkString(",")
}

我确实尝试通过添加一个函数作为参数来重写它,但我没有设法做出我确信是尾递归的东西。我不想在问题中包含我的尝试,因为我为他们的幼稚感到羞耻,所以请原谅我。

因此问题基本上是:上面的函数如何用 CPS 编写?

【问题讨论】:

    标签: scala tail-recursion continuation-passing


    【解决方案1】:

    试试看:

    import scala.annotation.tailrec
    def sol(chars: List[Char], n: Int) = {
      @tailrec
      def recSol(n: Int)(cont: (List[List[Char]]) => List[List[Char]]): List[List[Char]] = (chars, n) match {
        case (_  , 0) => cont(List(Nil))
        case (Nil, _) => cont(Nil)
        case (_  , _) =>
          recSol(n-1){ tail =>
            cont(chars.map(ch => tail.map(ch :: _)).fold(Nil)(_ ::: _))
          }
      }
      recSol(n)(identity).map(_.mkString).mkString(",")
    }
    

    【讨论】:

    • 嗯,确实很好用!我现在觉得有点傻 J 无论如何,这很有帮助,谢谢
    【解决方案2】:

    执行 CPS 转换的首要任务是确定延续的表示。我们可以将延续视为带有“洞”的暂停计算。当用一个值填充孔时,可以计算剩余的计算。因此,函数是表示延续的自然选择,至少对于玩具示例而言:

    type Cont[Hole,Result] = Hole => Result
    

    这里Hole代表需要填充的洞的类型,Result代表计算最终计算出来的值的类型。

    现在我们有了一种表示延续的方法,我们可以担心 CPS 变换本身。基本上,这涉及以下步骤:

    • 转换以递归方式应用于表达式,在“琐碎”表达式/函数调用处停止。在这种情况下,“琐碎”包括 Scala 定义的函数(因为它们不是 CPS 转换的,因此没有延续参数)。
    • 我们需要为每个函数添加一个Cont[Return,Result]类型的参数,其中Return是未转换函数的返回类型,Result是整体计算的最终结果类型。这个新参数代表当前的延续。转换后的函数的返回类型也更改为Result
    • 每个函数调用都需要转换以适应新的延续参数。调用之后的所有内容都需要放入一个延续函数中,然后将其添加到参数列表中。

    例如一个函数:

    def f(x : Int) : Int = x + 1
    

    变成:

    def fCps[Result](x : Int)(k : Cont[Int,Result]) : Result = k(x + 1)
    

    def g(x : Int) : Int = 2 * f(x)
    

    变成:

    def gCps[Result](x : Int)(k : Cont[Int,Result]) : Result = {
      fCps(x)(y => k(2 * y))
    }
    

    现在 gCps(5) 返回(通过柯里化)一个表示部分计算的函数。我们可以从这个部分计算中提取值,并通过提供一个延续函数来使用它。例如,我们可以使用恒等函数提取不变的值:

    gCps(5)(x => x)
    // 12
    

    或者,我们可以改用println 来打印它:

    gCps(5)(println)
    // prints 12
    

    将此应用于您的代码,我们得到:

    def solCps[Result](chars : List[Char], n : Int)(k : Cont[String, Result]) : Result = {
      @scala.annotation.tailrec
      def recSol[Result](n : Int)(k : Cont[List[List[Char]], Result]) : Result = (chars, n) match {
        case (_  , 0) => k(List(Nil))
        case (Nil, _) => k(Nil)
        case (_  , _) =>
          recSol(n - 1)(tail =>
                          k(chars.map(ch => tail.map(ch :: _)).fold(Nil)(_ ::: _)))
      }
    
      recSol(n)(result =>
                  k(result.map(_.mkString).mkString(",")))
    }
    

    如您所见,虽然recSol 现在是尾递归的,但它会带来在每次迭代中构建更复杂延续的成本。所以我们真正所做的只是用 JVM 控制栈上的空间换取堆上的空间——CPS 转换不会神奇地降低算法的空间复杂度。

    另外,recSol 只是尾递归的,因为对recSol 的递归调用恰好是recSol 执行的第一个(非平凡的)表达式。但是,一般来说,递归调用将发生在延续中。在有一个递归调用的情况下,我们可以通过将 only 对递归函数的调用转换为 CPS 来解决这个问题。即便如此,一般来说,我们仍然只是用堆栈空间来交换堆空间。

    【讨论】:

      猜你喜欢
      • 2011-06-30
      • 2012-04-19
      • 1970-01-01
      • 2010-12-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-12-12
      • 2011-12-16
      相关资源
      最近更新 更多