Scala中的延续传递风格

时间:2016-06-12 21:09:03

标签: scala tail-recursion continuation-passing

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

出于示例的目的,我将提出一个简单的问题。给定一个独特字符的排序列表,目标是按字母顺序输出由这些字符组成的所有可能单词。例如,<cfquery name="data"> SELECT NULL AS plainNull, CAST(NULL AS datetime) AS Due_Date </cfquery> 应返回sol("op".toList, 3)

我的递归解决方案如下:

ooo,oop,opo,opp,poo,pop,ppo,ppp

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

因此问题基本上是:如何在CPS中编写上述函数?

2 个答案:

答案 0 :(得分:2)

试试:

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(",")
}

答案 1 :(得分:0)

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

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

此处Hole表示需要填充的洞的类型,Result表示计算最终计算的值的类型。

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

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

例如,一个函数:

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)返回(通过currying)表示部分计算的函数。我们可以从这个部分计算中提取值,并通过提供延续函数来使用它。例如,我们可以使用identity函数来提取未更改的值:

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执行的。但是,一般情况下,递归调用将在延续中进行。在有一个递归调用的情况下,我们可以通过将调用转换为CPS的递归函数来解决这个问题。即便如此,一般来说,我们仍然只是在堆空间交换堆空间。