为什么 Haskell 不需要蹦床?

Maa*_*mon 20 recursion haskell scala tail-recursion trampolines

由于 Scala 开发人员正在学习 IO Monad,因此一般来说,在无法进行尾调用优化的情况下,对于递归来说是必需的 Trampoliing 技术细节,我想知道 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))
  }
}
Run Code Online (Sandbox Code Playgroud)

Lev*_*sey 23

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

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)
Run Code Online (Sandbox Code Playgroud)

evenand 中odd,每个分支要么是一个简单的表达式(truefalse在这种情况下),它不进行函数调用或尾调用:被调用函数的值被返回而不被操作。

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

Haskell 和基本上所有其他 post-Scheme 函数式语言运行时都实现了广义的尾调用消除:尾调用变成了无条件跳转(想想 GOTO)。著名的系列斯蒂尔和萨斯曼论文(PDF文件遗憾的是没有得到存档,但你可以搜索,如AIM-443mitsteelesussman可能需要))被称为“拉姆达:终极”(这激发了编程语言的名称论坛)探讨了尾调用消除的含义,以及这如何意味着函数式编程对于解决现实世界的计算问题实际上是可行的。

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

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

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

  • 我还应该指出,Scheme 并没有发明尾部调用消除:这是一种优化技巧,自从 20 世纪 50 年代子例程和递归子例程设计模式变得普遍以来,汇编语言编码人员就一直在使用这种优化技巧。在此之前的几年里,早期的 lisp 和其他编译器已经在某些情况下执行了尾部调用消除。斯蒂尔和萨斯曼证明它是普遍适用的。 (2认同)
  • http://dspace.mit.edu/bitstream/handle/1721.1/5753/AIM-443.pdf 可能是尾部调用消除最清晰的原始解释:“一般来说,过程调用可以被有效地视为 GOTO 语句,它也传递参数,并且可以统一编码为JUMP指令” (2认同)