【问题标题】:Why doesn't Haskell need Trampolining?为什么 Haskell 不需要蹦床?
【发布时间】:2021-07-16 05:00:57
【问题描述】:

作为 Scala 开发人员学习 IO Monad,因此一般来说 Trampolining 的技术对于无法进行尾调用优化的递归是必需的,我想知道 Haskell 似乎是如何在本机避免它的。

我知道 Haskell 是一种惰性语言,但我想知道是否有人可以进一步详细说明。

例如,为什么 ForeverM stackoverflow 不在 Scala 中?好吧,我可以回答蹦床,我可以在库和博客中找到执行此操作的实际代码。我实际上自己实现了一个基本的蹦床来学习。

它在 Haskell 中是如何发生的?有没有办法解开懒惰的包装,提供一些指示,也许还有有助于更好地理解它的文档?

sealed trait IO[A] {

.....


  def flatMap[B](f: A => IO[B]): IO[B] =
    FlatMap[A,B](this, f) // we do not interpret the `flatMap` here, just return it as a value
  def map[B](f: A => B): IO[B] =
    flatMap[B](f andThen (Return(_)))

}
case class Return[A](a: A) extends IO[A]
case class Suspend[A](resume: () => A) extends IO[A]
case class FlatMap[A,B](sub: IO[A], k: A => IO[B]) extends IO[B]

......

@annotation.tailrec
def run[A](io: IO[A]): A = io match {
  case Return(a) => a
  case Suspend(r) => r()
  case FlatMap(x, f) => x match {
    case Return(a) => run(f (a))
    case Suspend(r) => run(f( r()))
    case FlatMap(y, g) => run(y flatMap (a => g(a) flatMap f))
  }
}

【问题讨论】:

  • 我们不在这里解释flatMap,只是将它作为一个值返回”基本上是Haskell中发生的一直,没有明确说明 - 一切都只是一个懒惰的重击,它由一个执行整个程序的巨大蹦床评估。

标签: scala haskell recursion tail-recursion trampolines


【解决方案1】:

函数式编程通常需要消除尾调用(否则函数调用的深层链会溢出堆栈)。例如,考虑这个偶数/奇数分类器的实现(效率低得离谱):

def even(i: Int): Boolean =
  if (i == 0) true
  else if (i > 0) odd(i - 1)
  else odd(i + 1)

def odd(i: Int): Boolean =
  if (i == 0) false
  else if (i > 0) even(i - 1)
  else even(i + 1)

evenodd 中,每个分支要么是一个简单的表达式(在本例中为truefalse),它不进行函数调用或尾部调用:被调用的函数不被操作直接返回。

如果没有尾调用消除,(可能是无限循环长度的递归)调用必须使用消耗内存的堆栈来实现,因为调用者可能会对结果做一些事情。尾调用消除依赖于观察调用者对结果不做任何事情,因此被调用函数可以有效地替换堆栈上的调用者。

Haskell 和基本上所有其他后方案函数式语言运行时都实现了通用的尾调用消除:尾调用成为无条件跳转(想想 GOTO)。著名的series of Steele and Sussman papers(遗憾的是,PDF 没有存档,但您可以搜索,例如AIM-443(可能需要mitsteelesussman))被称为“Lambda:The Ultimate” (这启发了编程语言论坛的名称)探讨了尾调用消除的含义以及这意味着函数式编程实际上对于解决现实世界的计算问题是可行的。

然而,Scala 主要针对 Java 虚拟机,其规范(通过设计)有效地禁止通用尾调用消除,其指令集约束无条件跳转不跨越方法的边界。在某些有限的上下文中(基本上是一个方法的递归调用,其中编译器可以绝对确定正在调用什么实现),Scala 编译器在发出 Java 字节码之前执行尾部调用消除(理论上可以想象 Scala Native 可以执行泛化尾调用消除,但这将需要 JVM 和 JS Scala 的一些语义中断(一些 JavaScript 运行时执行通用的尾调用消除,尽管据我所知不是 V8))。 @tailrec 注释,您可能对它有些熟悉,它强制要求编译器能够执行尾调用消除。

Trampolining 是一种运行时的低级技术,用于模拟编译时尾调用消除,尤其是在 C 或 Scala 等语言中。由于 Haskell 在编译时执行了尾调用消除,因此不需要蹦床的复杂性(以及将高级代码编写为连续传递样式的要求)。

您可以将 Haskell 程序中的 CPU(或运行时本身,如果转译为,例如 JS)视为实现蹦床。

【讨论】:

  • 我还应该指出,Scheme 并没有发明尾调用消除:自从子例程和递归子例程设计模式在1950 年代。在此之前的几年里,早期的 lisp 和其他编译器在某些情况下已经执行了尾调用消除。 Steele 和 Sussman 证明它是普遍适用的。
  • dspace.mit.edu/bitstream/handle/1721.1/5753/AIM-443.pdf 可能是对尾调用消除最清晰的原始解释:“一般来说,过程调用可以被认为是传递参数的 GOTO 语句,并且可以统一编码为 JUMP 指令"
  • 感谢您对此的详细回答,包括参考。我需要阅读该参考资料并回复您。
  • 如前所述,Scheme 执行通用的尾调用消除并​​且它不是惰性的(您可以实现惰性,就像在 Scala 中一样)。
  • 不幸的是,至少有一种主流函数式编程语言实现不执行 TCO:GNU R。当然,这不是它唯一的缺点。
【解决方案2】:

蹦床并不是尾声的唯一解决方案。 Scala 需要蹦床正是因为它运行在 JVM 上,带有 Java 运行时。 Scala 语言开发人员无法准确选择其运行时的运行方式或二进制格式。因为他们使用 JVM,所以他们必须忍受 JVM 针对 Java 而不是针对 Scala 而优化的所有方式。

Haskell 没有这个限制,因为它有自己的运行时、自己的二进制格式等。它可以根据 Haskell 语言的语言级结构精确地选择如何在运行时设置堆栈 --- 不是, 属于 Java 的。

【讨论】:

    猜你喜欢
    • 2010-09-16
    • 1970-01-01
    • 2011-12-27
    • 1970-01-01
    • 2014-12-30
    • 1970-01-01
    • 2018-11-02
    • 2012-03-09
    • 2021-06-01
    相关资源
    最近更新 更多