【问题标题】:how to approach implementing TCO'ed recursion如何实现 TCO 的递归
【发布时间】:2012-01-04 13:06:30
【问题描述】:

我一直在研究递归和 TCO。似乎 TCO 可以使代码变得冗长并影响性能。例如我已经实现了接收 7 位电话号码并返回所有可能的单词排列的代码,例如464-7328 可以是“GMGPDAS ... IMGREAT ... IOIRFCU” 这是代码。

/*Generate the alphabet table*/
  val alphabet = (for (ch <- 'a' to 'z') yield ch.toString).toList

/*Given the number, return the possible alphabet List of String(Instead of Char for convenience)*/
  def getChars(num : Int) : List[String] = {
      if (num > 1) return List[String](alphabet((num - 2) * 3), alphabet((num - 2) * 3 + 1), alphabet((num - 2) * 3 + 2))
      List[String](num.toString)
  }

/*Recursion without TCO*/
  def getTelWords(input : List[Int]) : List[String] = {
    if (input.length == 1) return getChars(input.head)
      getChars(input.head).foldLeft(List[String]()) {
        (l, ch) => getTelWords(input.tail).foldLeft(List[String]()) { (ll, x) => ch + x :: ll } ++ l
      }
  }

它很短,我不必花太多时间在这上面。但是,当我尝试在尾调用递归中执行此操作以获取 TCO 时。我必须花费大量时间并且代码变得非常冗长。我不会摆出整个代码来节省空间。 Here is a link to git repo link。可以肯定的是,你们中的很多人都可以写出比我更好、更简洁的尾递归代码。我仍然认为总体而言 TCO 更冗长(例如,阶乘和斐波那契尾调用递归具有额外的参数累加器。)但是,需要 TCO 来防止堆栈溢出。我想知道您将如何处理 TCO 和递归。 this thread 中具有 TCO 的 Akermann 的 Scheme 实现是我的问题陈述的缩影。

【问题讨论】:

  • 总拥有成本与递归有什么关系? ;)
  • 链接线程中没有 Haskell。您似乎正在讨论的代码在方案中。
  • 事实上,在 Haskell 中,通常但并非总是如此,尾调用并不那么重要。例如,您上面的问题实际上是关于生成结果流(即惰性列表)。它可以非常简单地用 Haskell 中的标准列表工具编写。
  • 为什么要强制使用尾调用优化?并非所有递归例程都应该是尾调用递归的。如果每个递归调用都有自己的状态,那么它要么不是尾调用,要么您必须维护自己的状态并传递它(这就是您的实现所做的)。在任何一种情况下,空间消耗行为都是相同的。如果你真的需要担心堆栈溢出,就使用迭代版本,没有任何法律禁止它。
  • @Kim Stebel,递归与维护者(所有者)的总拥有成本有关,你知道的。你不想让它在你手中炸毁(堆栈溢出);)

标签: scala recursion functional-programming scheme tail-call-optimization


【解决方案1】:

正如 cmets 中提到的 sclv ,尾递归对于 Haskell 中的这个示例毫无意义。使用 list monad 可以简洁高效地编写问题的简单实现。

import Data.Char
getChars n | n > 1     = [chr (ord 'a' + 3*(n-2)+i) | i <- [0..2]]
           | otherwise = ""
getTelNum = mapM getChars

【讨论】:

  • 如何调用函数? getTelNum 123 给我一个类型错误。谢谢。
  • 应该是 getTelNum [1, 2, 3]
  • @Chris:那是因为1 数字不对应任何字母。例如,试试getTelNum [2, 3, 4]
  • @hammar 谢谢。我一个月前才开始接触 Scala,之前没有任何 FP 知识。我需要一些时间来阅读您的 Haskell 代码并了解 list monad。
【解决方案2】:

正如其他人所说,我不会担心这种情况下的尾调用,因为与输出的大小相比,它不会非常深入地递归(输入的长度)。在你出栈之前你应该内存不足(或耐心)

我可能会用

之类的东西来实现
def getTelWords(input: List[Int]): List[String]  = input match {
   case Nil => List("")
   case x :: xs => {
      val heads = getChars(x)
      val tails = getTelWords(xs)
      for(c <- heads; cs <- tails) yield c + cs
   }
}

如果你坚持使用尾递归,那可能是基于

def helper(reversedPrefixes: List[String], input: List[Int]): List[String] 
  = input match {
    case  Nil => reversedPrefixes.map(_.reverse)
    case (x :: xs) =>  helper(
      for(c <- getChars(x); rp <- reversedPrefixes) yield c + rp,
      xs)
  }

(实际的例程应该调用helper(List(""), input)

【讨论】:

  • 非常感谢。您的代码确实大大缩短了执行时间,并且您的尾调用递归与标准递归一样简洁。然而,与标准递归相比,您的 TCO 版本也落后于性能。无论如何,再次感谢 Scala(FP?) 的惯用表达方式。
【解决方案3】:

您是否有可能使用术语“尾调用优化”,而实际上您的意思实际上是指以迭代递归样式或继续传递样式编写函数,以便所有递归调用都是尾调用?

实施 TCO 是语言实施者的工作;一篇讨论如何高效完成的论文是经典的 Lambda: the Ultimate GOTO 论文。

尾调用优化是您的语言评估员会为您做的事情。另一方面,您的问题听起来像是在询问如何以特定样式表达函数,以便程序的形状允许您的评估者执行尾调用优化。

【讨论】:

  • dyoo,我不确定 scheme 和 lisp 编译器是否为所有递归执行 TCO。但是,在 Scala 中,递归必须是尾调用,以便编译器对其进行优化,因为 Scala 在 JVM 上运行,而 JVM 本身不支持递归优化。这是关于 Scala 要求递归是尾调用的博客 bit.ly/s9LQIF。所以,是的,我的问题是关于通过“递归”获得“尾调用递归”的平易近人的方式
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-06-16
  • 1970-01-01
  • 2010-11-24
  • 2017-01-31
  • 2020-07-21
  • 2019-11-05
相关资源
最近更新 更多