作为Scala开发人员学习IO Monad,以及Trampolling的一般技术细节,这些技术细节对于尾调用优化不可能的递归是必要的,我想知道Haskell是如何天生避免它的。
我知道Haskell是一种懒惰的语言,但我想知道是否有人能进一步阐述一下。
例如,为什么ForeverM不在scala中堆叠?好吧,我可以回答蹦床,我可以在图书馆和博客中找到真正的代码。实际上,我自己做了一个基本的蹦床来学习。
哈斯克尔是怎么发生的?有没有一种方法可以稍微解开懒惰的面纱,给出一些建议,也许还有文档可以帮助更好地理解它?
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))
}
}
函数式编程通常需要尾部调用消除(否则函数调用的深层链会溢出堆栈(。例如,考虑一下偶数/奇数分类器的这种(效率低得离谱(实现:
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)
在even
和odd
中,每个分支都是一个不进行函数调用或尾部调用的简单表达式(在这种情况下为true
或false
(:被调用函数的值在不进行操作的情况下返回
如果没有尾部调用消除,则必须使用消耗内存的堆栈来实现调用(可能是循环长度不确定的递归调用(,因为调用者可能会对结果做一些事情。尾部调用消除依赖于观察调用方对结果不做任何操作,因此被调用的函数可以有效地替换堆栈上的调用方。
Haskell和其他所有后Scheme函数语言运行时都实现了广义的尾部调用消除:尾部调用变成了无条件的跳转(想想GOTO(。Steele和Sussman的著名系列论文(PDF不幸没有存档,但你可以搜索,例如AIM-443
(可能需要mit
或steele
或sussman
((被称为";Lambda:终极版;(启发了编程语言论坛的名字(深入探讨了尾调用消除的含义,以及这意味着函数编程在解决现实世界的计算问题方面实际上是可行的。
然而,Scala主要针对Java虚拟机,其规范有效地(通过设计(禁止了广义的尾调用消除,并且其指令集限制无条件跳转,使其不跨越方法的边界。在某些有限的上下文中(基本上是方法的递归调用,其中编译器可以绝对确定正在调用什么实现(,Scala编译器在发出Java字节码之前执行尾部调用消除(理论上可以想象Scala Native可以执行广义尾部调用消除,但这将导致JVM和JS-Scala的一些语义中断(一些JavaScript运行时执行广义尾部呼叫消除,但据我所知不是V8(。您可能对@tailrec
注释有所熟悉,它强制要求编译器能够执行尾部调用消除。
Trampolling是一种在运行时模拟编译时尾部调用消除的低级技术,尤其是在C或Scala等语言中。由于Haskell已经在编译时执行了尾部调用消除,因此不需要蹦床的复杂性(也不需要将高级代码写入延续传递样式(。
可以说,您可以将Haskell程序中的CPU(或者运行时本身,如果转换为JS(视为实现蹦床。
蹦床并不是尾叫的唯一解决方案。Scala之所以需要蹦床,正是因为它运行在JVM上,带有Java运行时。Scala语言的开发人员没有准确地选择运行时的操作方式,也没有选择二进制格式。因为他们使用JVM,所以他们必须忍受JVM为Java而不是Scala优化的每一种方式。
Haskell没有这个限制,因为它有自己的运行时,有自己的二进制格式等。它可以根据Haskell语言的语言级别构造来精确地选择如何在运行时设置堆栈,而不是Java语言。