【问题标题】:Some specific sort of tail-recursion in scalascala中的某种特定类型的尾递归
【发布时间】:2018-01-31 02:35:24
【问题描述】:

我想了解,这些尾递归将如何在后台执行,我最近在一个用 scala 编写的大数据分配中看到。我还提供了两个附加版本的实现,我更愿意如何编写这段代码 - 其中一个也是尾递归的,另一个 - 不是;但是,我想了解第一个实现如何提供相同的结果。

所以,这里是(目的是在列表(ls)中找到特定名称(标签)的索引):

def firstLangInTag(tag: Option[String], ls: List[String]): Option[Int] = {
if (tag.isEmpty) None
else if (ls.isEmpty) None
else if (tag.get == ls.head) Some(0)
else {
  val tmp = firstLangInTag(tag, ls.tail)
  tmp match {
     case None => None
     case Some(i) => Some(i + 1)
     }
  }
}

当我们考虑执行时,将看到参数'tag'定义为Option(“Scala”),而'ls'定义为List(“Java”,“PHP”,“Scala”):

  • val tmp = firstLangInTag(Scala, (PHP, Scala)) => 返回 Some(2)
  • val tmp = firstLangInTag(Scala, (Scala)) => 返回 Some(1)

所以我们有一个答案 Some(2) 并且它是正确的,但是有人可以解释一下,在执行过程中 var 'i'(隐含的 var 'tmp')保存在哪里。是因为尾递归为每次递归执行提供了一个堆栈,而“i”只是保存在内存中并且每次迭代期间都会更新?为什么 var 'tmp' 不仅会被每次迭代覆盖,而且结果会累积(+ 1)。如果您查看以下使用 'accumulator' 的实现,但再次递归,那么很明显,结果已存储在名为 'acc' 的变量中,因此 'acc' 返回此函数的结果:

def firstLangInTag(tag: Option[String], ls: List[String]): Option[Int] = {
   def counter(acc: Int, tag: Option[String], ls: List[String]): Int = {
     if (tag.isEmpty) acc
     else if (ls.isEmpty) acc
     else if (tag.get == ls.head) acc
     else counter(acc + 1, tag, ls.tail) 
   }
   Option(counter(0, tag, ls))

同样,我们也可以实现与非尾递归函数相同的结果(前提是它会返回 Int 而不是 Option):

...
else 1 + firstLangInTag(tag, ls.tail);

但是,请有人向我解释一下第一个函数以及 scala 如何将结果存储在 VAL 中并在每次下一次迭代时更新它;

提前谢谢你!

【问题讨论】:

    标签: scala tail-recursion


    【解决方案1】:

    第一个给定的函数不是尾递归的。给它加上@scala.annotation.tailrec注解,编译失败。您对处理 tmpi 的部分如何工作持怀疑态度是正确的,因为这正是阻止它尾递归的部分。

    【讨论】:

    • 不完全准确。它仍然不是尾递归,因为返回值被进一步处理(匹配)而不是立即返回。
    • 我也有点怀疑,第一个例子是否是尾递归......
    【解决方案2】:

    例如val tmp = firstLangInTag(Some("Scala"), List("PHP", "C#", "Scala")) => returns Some(2)

    在上面的示例中,您将调用您的函数的三个调用。前面每一个返回结果都得跟着他等待执行:

    • 第一个firstLangInTag(Some("Scala"), List("PHP", "C#", "Scala"))
    • 第二个firstLangInTag(Some("Scala"), List("C#", "Scala"))
    • 第三个firstLangInTag(Some("Scala"), List("Scala"))

    最后一次调用将返回Some(0),这将传播到第二次调用,其中tmp 将被解析为Some(0),并返回到第一次调用Some(1)

    最后,第一次调用tmp 将被解析为Some(1) 然后匹配并且函数将返回Some(2)

    非尾递归的第一个解决方案,因为函数的第一次调用需要等待找到标签的函数调用(带有列表的尾部),或者在初始列表中缺少标签的情况下返回None。

    使用带有累加器的版本解决这个问题,而不会对堆栈产生不必要的开销,并添加@tailrec 注释

    当你尝试编写尾递归函数时,你总是可以添加一个@tailrec 注释,这样你就可以确定如果函数没有正确实现,你会收到一条错误消息。

    【讨论】:

    • 谢谢,但请您解释一下,如何更新 VAL tmp?是smth吗?与 scala 中的 match 方法有关,其中一些临时变量(例如示例中的“i”)将被更新然后由 VAL 评估?因为该功能将被传播,我确实了解它将如何更新 - 不幸的是,我不知道!这不是为什么我提供带有累加器(纯尾递归函数)的示例,其中非常清楚,将返回什么以及如何返回。
    • val 未更新。它在方法调用的每个实例上都是全新的。
    • 每个方法调用在堆栈上都有自己的空间,因此每个函数调用都有自己的每个变量的实例。
    【解决方案3】:

    我会让更精通 Scala 的人对每个执行阶段的堆栈内容给出更详细的答案,但我确实注意到 tmp 设置为函数的返回值。正向遍历列表时不会存储中间值,但每次调用 firstLangIntTag 都会将函数应用于返回它的调用结果。这是可能的,因为函数是 Scala 中的对象。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-03-17
      • 1970-01-01
      • 2018-03-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多