Coding just likes Art
This is the blog about the coding, the whole coding and nothing but the coding , so God would help me become a better developer.

Translate: Stackless Scala With Free Monads

1 Motivation

由于最近经常在生产中使用 Free Monad 范式, 所以就顺带看了看 cats.Free 的实现, 发现其实现方式和我想想中的 Free 不太一样, 除了 Pure (Return), Suspend 两种状态之外, 还有一个 FlatMapped 状态. 这样做看上去是为了将 flatMap 的 evaluation 延迟至 transform 期间完成, 但还是隐隐感觉和堆栈安全有关. 顺带想起关于 Scala FP 的堆栈安全一直是自己知识体系的一个空白, 因此就有了翻译 Stackless Scala With Free Monads 这篇论文的冲动, 期望这个工作能 drive 自己搞清楚 Trampolining.

2 Stackless Scala With Free Monads

这篇论文是 Rúnar Bjarnason 在 2012 年的 Scala Days Conf 上发表并演讲的主题, 通过引入 Trampolining 范式(堆换栈, 感觉和 CPS 的理念很相似) 来解决 Scala 编译器无法完全尾调用优化的问题. 会议演讲视频: http://skillsmatter.com/podcast/scala/stackless-scala-free-monads 论文原稿: http://days2012.scala-lang.org/sites/days2012/files/bjarnason_trampolines.pdf

3 Body 正文

3.1 Abstract 摘要

Scala 编译器的尾调用优化 (Tail Call Elimination, TCE) 仅限于自递归方法 (Self-Recursive Methods) 而不是所有的尾调用. 这很容易使得那些由小函数组合而成的函数造成堆栈溢出. 拥有一个通用的尾调用优化方法非常有益于 Scala 语言, 尤其是有益于 Scala 的函数式编程. 针对那些本不支持尾调用优化的语言, Trampolining 是一个可以用于解决尾调用优化问题的常用方法. 本文引入 Trampolining 技术, 以将其推广为一个对任何方法调用(甚至是非尾部调用)的优化技术. 该技术能够完全的消除 Scala 程序对调用堆栈的使用.

3.2 Introduction 介绍

调用堆栈是虚拟机的有限资源, 稍微有点经验的 Scala 程序员应该都遇见过一些看上去很正常的函数用尽了调用堆栈后使得程序崩溃并抛出 StackOverflowError 的错误. 考虑一个实用的例子, 在遍历一个 List 的同时维护一些状态. 我们采用 State 数据结构来表征一个简单的状态机的变换.

case class State[S, +A](runS: S => (A, S)) {
  def map[B](f: A => B) = State[S, B] { s => {
    val (a, s1) = runS(s)
    (f(a), s1)
  }}

  def flatMap[B](f: A => State[S, B]) = State[S, B] { s => {
    val (a, s1) = runS(s)
    f(a).runS(s1)
  }}
}

函数 runS 以一个类型为 S 的状态作为输入, 返回一个类型为 A 的值以及一个新的状态. 而 mapflatMap 方法允许通过 for-comprehension 以一种命令式的代码方式将状态变换组合在一起, 如:

def getState[S]: State[S, S] = State(s => (s, s))

def setState[S](s: S): State[S, Unit] = State(_ => ((), s))

def pureState[S, A](a: A): State[S, A] = State(s => (a, s))

注意到 pureStateflatMap 两个方法使得 State 成为一个 Monad.

作为一个简单的例子, 我们用 State 来实现对一个 List 中的元素编号. 这并不见得是 State 的一个常见用例, 仅仅因为这能够简单的诠释上文所说的堆栈溢出问题.

def zipIndex[A](as: List[A]): List[(Int, A)] = as.foldLeft(pureState[Int, List[(Int, A)]](Nil)) { (acc, a) =>
  for {
    xs <- acc
    n <- getState[Int]
    _ <- setState[Int](n + 1)
  } yield (n, a) :: xs
}.runS(0)._1.reverse

译者注: 对于 StateflatMap 方法, 每次调用都会在堆中产生一个新的 State.class 类型实例及其 runS 方法的函数类型 State.1.class 的实例. (A Look at How Scala Compiles to Java)

我们使用一个 foldLeft 来强调对 list 的遍历是一个尾递归的调用. 这个函数以一个空的 list 为起点, 不断在列表的头部添加元素, 所以与原 list 顺序相反. 而 state 是一个从零开始每步递增的整数, 所有的 state 的生成都发生在反转结果之前.

但是如果用一个元素数量超过虚拟机调用堆栈大小的 list 作为参数, State.flatMap 方法会抛出 StackOverflowError 的错误. 发生错误的原因是最终的 state 本质上是一个大函数, 由与 list 大小成正比的多个函数组合而成. 尽管它让我们觉得这是一个离散的一系列步骤, 但每个步骤使用了一种无法另编译器优化的方式去调用下一个步骤.

这种情况极大的限制了 Scala 的函数式编程特性. 而这篇论文就是来探讨解决方案的:

  • 第三部分将探讨熟知的 Trampolining 技术. 在一个 Trampolined 的程序中, 我们不再让每个步骤直接去调用另一个步骤, 而采用一种 Trampoline 控制循环来暂缓下一步骤的发生. 这使得我们可以用堆去代替调用栈.
  • 之后(在第四部分), 我们会将这种方法扩展为一个 Monad, 使得所有的调用都可以被转化为可依次消除的尾递归调用.
  • 在实现这种 monad 时, 如果实现不当, 依然会导致在一些场景堆栈溢出的错误. 在 4.3 节, 我们将探讨这些场景以及如何消除这些问题.
  • Trampolined 程序可以是多层嵌套的来提供 cooperative coroutines, 我们将在第五部分探讨.
  • 在第六部分, 我们将 Trampolines 推广为 Free Monad, 一种极为通用的递归数据结构. 通过分析操作这种结构的部分函数, 利用 Trampoline 的方法来解决 Free Monad 的堆栈问题.

3.3 背景: Scala 的尾调用优化

Scala 的编译器能够对一种叫做自调用尾递归 (Self-recursive Call) 的代码进行优化. 例如, 下面实例代码中对 list 的 左折 操作可以被编译器优化为一个使用固定栈空间的函数:

def foldl[A, B](as: List[A], b: B, f: (B, A) => B): B =
  as match {
    case Nil => b
    case x :: xs => foldl(xs, f(b, x), f)
  }

当编译器发现一个函数在尾部调用了自己, 并且 该函数不能被重载 (例如, 使用 privatefinal 关键字声明), 则该函数的自调用会被编译替换为一个简单的跳转 (Jump). 这就相当于将该递归写成一个这循环:

def foldl[A, B](as: List[A], b: B, f: (B, A) => B): B = {
  var z = b
  var az = as
  while (true) {
    az match {
      case Nil => return z
      case x :: xs =>
        z = f(b, x)
        az = xs
    }
  }
}

除了跳转比方法调用更高效这个优势外, 这种优化使得无需使用任何调用栈. 但是,用跳转替换一个普通的尾调用就没有自递归调用优化那么简单了. 目前, JVM 只允许本地跳转, 所以根本没法从一个方法尾部直接跳转至另一个方法.例如, 一个相互跳转的递归是不能被编译器优化的, 即便它的调用在方法的尾部.

def even[A](ns: List[A]): Boolean = ns match {
  case Nil => true
  case x :: xs => odd(xs)
}

def odd[A](ns: List[A]): Boolean = ns match {
  case Nil => false
  case x :: xs = even(xs)
}

这两个方法会在参数长度大于堆栈长度时引发堆栈溢出.

尽管 JVM 很可能会在未来改进这点, 但也并不是一件容易的事, 并且有可能没有想象中那么有用. 比如说, JVM 的调用模型需要把每个线程的执行状态都保存在线程的调用堆栈里. 另外, 异常处理也是通过将异常压栈后将堆栈暴露给程序进行检查的. 事实上, JVM 的安全模型也是基于检查每个调用栈帧 (Stack Frame) 的权限来实现的. 而这又与子类, 动态分配策略, 即时编译器偶合在一起, 使得 Scala 编译器的尾调用优化难以实现.

不过还好, 我们可以回避所有这些问题, 通过一种简单的数据结构来用堆空间来代替栈空间.

3.4 Trampolines: 以堆换栈

我们从一个非常简单的 Trampoline 数据类型来说明. 下例与 scala.util.control.TailCalls.TailRec 包中的 Trampoline 从原理上是完全相同的, 尽管实现的方式不同.

sealed trait Trampoline[+A] {
  final def runT: A = this match {
    case More(k) => k().runT
    case Done(v) => v
  }
}

case class More[+A](k: () => Trampoline[A])
    extends Trampoline[A]

case class Done[+A](v: A)
    extends Trampoline[A]

Trampoline 将计算分解为多个步骤, 每个步骤可以有两种形式. Done 包含一个用于返回的值 v, 表示后续再无其他步骤. More 则包含一个生成后续步骤的函数 k. 而 runT 方法则是一个简单的尾递归, 用于执行所有的步骤. 它被定义为 final 以便让 Scala 编译器进行优化. 这就可以解决上文提到的互相跳转的尾调用递归问题. 我们只需要机械的将所有类型为 T 的返回值替换为 Trampoline[T] 就行了. 具体如下:

def even[A](ns: List[A]): Trampoline[Boolean] = ns match {
  case Nil => Done(true)
  case x :: xs => More(() => odd(xs))
}

def odd[A](ns: List[A]): Trampoline[Boolean] = ns match {
  case Nil => Done(false)
  case x :: xs = More(() => even(xs))
}

我们从原来的直接递归调用, 变成使其返回一个表示下一个步骤的 Trampoline, 然后可以通过调用可以被优化的方法 runT 来递归执行. 这样, 无论输入的 List 有多长, 上面的代码也不会再引发堆栈溢出.

3.5 把所有调用都变为尾调用

让我们看看如何使用 Trampoline 技术来解决论文一开始提出的 State 遍历 list 的 堆栈溢出问题. 首先, 我们需要改写 State 的行为来返回可供尾递归的 Trampoline 类型.

case class State[S, +A](runS: S => Trampoline[(A, S)])

我们现在该如何实现组合 State 的 flatMap 呢? 先试试这样:

def flatMap[B](f: A => State[S, B]) = State[S, B] { s =>
  More[(S, B)] { () =>
    val (a, s1) = runS(s).runT
    More(() => f(a).runS(s1))
  }
}

但这样写会很低效. 而且例一中的 zipIndex 仍然可能会堆栈溢出, 甚至对于更小的 list. 问题出在 runT 的调用没有出现在尾部, 所以它并不能被封装在 Trampoline 内部, 也不能用 Trampoline 来优化.

3.5.1 用一个 Trampoline Monad ?

现在我们要用一个 Monadic 的 Trampoline 来解决该问题。它已经有了 unit 操作1,定义在构造函数中。因此,只需要定义 Monadic 的 bind 操作,也就是 flatMap 方法, 即可. 让我们直接给 Trampoline 添加一个 flatMap 方法,如此一来,就可以将 State.flatMap 改写为:

def flatMap[B](f: A => State[S, B]) =
  State[S, B](s => More(() => runS(s) flatMap {
    case (a, s1) => More(() => f(a) runS s1)
  }))

.1 对于任意类型 A, Monad Munit 函数定义为 A => M[A]. 之所以叫做 unit 是因为它是 flatMap 操作的 幺元.

这显然有所改进. 它把问题转移到 TrampolineflatMap 方法中, 因此我们可以这样试着实现:

def flatMap[B](f: A => Trampoline[B]) =
  More[B](() => f(runT))

但这不是我们想要的. runT 依然不在调用的尾部. 看来无论我们怎么实现 TrampolineflatMap, 都不可能不消耗额外的栈空间.

3.5.2 正确的构建 Monad

解决该问题的方法是为 Trampoline 数据类型增加一个类型构造器,并将 flatMap 方法的调用改为对构造器的调用:

case class FlatMap[A, +B](
  sub: Trampoline[A],
  k: A => Trampoline[B]) extends Trampoline[B]

Trampoline 的这种形式可以看作是对子程序 sub 的调用,其结果会被传入 continuation k. 这时候, Trampoline traitrunT 就需要考虑这个新的构造器. 为了简单说明, 让我们把执行下一步与执行所有步分开:

final def resume: Either[() => Trampoline[A], A] = this match {
  case Done(v) => Right(v)
  case More(k) => Left(k)
  case FlatMap(a, f) => a match {
    case Done(v) => f(v).resume
    case More(k) => Left(() => FlatMap(k(), f))
    case FlatMap(b, g) => (FlatMap(b, (x: Any) => FlatMap(g(x), f)): Trampoline[A]).resume
  }
}

final def runT: A = resume match {
  case Right(a) => a
  case Left(k) => k().runT
}

resume 方法利用模式匹配来处理 Trampoline, 或者返回一个结果(即 Right), 或者返回一个 Function0 来表明下一步的操作(即 Left). 这里处理 FlatMap 这种情况的方式微秒而重要. 如果子程序 aDone, 我们会直接执行 continuation. 如果其被包裹在 More 中, 我们向前执行一步(译者: 即执行 More 中的函数), 并对其执行 FlatMap 操作. 如果子程序包含另一个子程序, 也就是一个内嵌的左结合的 FlatMap 如下:

FlatMap(FlatMap(b, g), f)

这一步非常关键, 我们需要保证不引入新的栈帧. 技巧是利用结合律将其变换到右边: (译者: 注意 Trampoline Monad 满足结合律, 因此 (b FlatMap g) FlatMap f = b FlatMap (x => g(x) FlatMap f))

FlatMap(b, x => FlatMap(g(x), f))

这样一来, 在下一个迭代时就会对 b 进行模式匹配, 这样就能够在尾部继续调用 resume. 由于这次 resume 调用是在 FlatMap 上的, 我们必须显示的将当前的对象转换为 Trampoline, 这样编译器才能明白这其实是一次尾递归调用2. 不用担心, 我们会在 4.3 节去掉该转换.

同时需要注意的是, 内嵌的 FlatMap 构造器中丢失了一些类型信息. 在 FlatMap(FlatMap(b, g), f) 形式中, b 的类型无从得知, 所以只能在转换右结合时将其视为 Any. 这其实非常安全, 因为左结合时的嵌套结构的类型是正确的. p 通过结合律转换利用到了 Monad 的规则. Trampoline 是一个 Monad, 而 Monad 一定是满足结合律的. 因此, 右结合方式一定等价于左结合的.

译者注: Trampoline 的三个实例代表了不同的使用场景: Done 表明了下一步计算会取出其值, 用于递归中的返回. More 表明了下一步计算的递归特性, 一般用于递归调用. FlatMap 中包含两个值, 左侧的子程序表明了下一步计算的类型, 右侧的函数表明了在下一步计算后需要做的事, 因此可用于非尾递归的后续操作.

3.5.3 一个易错的问题

这里还有一个边缘情况需要考虑. resume 函数依然有可能造成栈溢出. 倘若 FlatMap 左侧的堆叠层次高出了调用栈的高度, f(v) 的调用会导致 g(x) 调用, 然后会导致里层的函数调用, 等等. 这可以通过在一开始就禁止左结合的内嵌的构造来避免. 我们将 FlatMap 构造器变为 private, 并通过暴露 TrampolineflatMap 方法来代替它, 这样就能总是用右结合的结构来改写:

def flatMap[B](f: A => Trampoline[B]): Trampoline[B] = this match {
  case FlatMap(a, g) => FlatMap(a, (x: A) => g(a) flatMap f)
  case x => FlatMap(x, f)
}

同时, 必须同时避免通过 resume 方法来构造这种结构, 我们可以将其中的 FlatMap 构造改为对 flatMap 的调用:

case FlatMap(a, f) => a match {
  case Done(v) => f(v).resume
  case More(k) => Left(() => k() flatMap f)
  case FlatMap(b, g) =>
    b.flatMap((x: Any) => g(x) flatMap f).resume
}

最终, 为了能够在 Scala 的 for-comprehension 表达式中使用 Trampoline monad, 只需要用 flatMap 定义一个 map 方法就行了:

def map[B](f: A => B): Trampoline[B] =
  flatMap(a => Done(f(a)))

3.6 无栈 Scala

通过使用 Trampoline 化的 State monad, 对任何大小的输入 list, 现在运行之前看到的 zipIndex 方法不会在引发堆栈溢出的异常了. 这里介绍的 Trampoline 是 Scala 中清除栈帧的一种通用手段. 我们可以用它来重写任何程序而不再需要栈空间了. 考虑一个具有如下形式的程序:

val x = f()
val y = g(x)
h(y)

可以非常轻松的这样重写:

for {
  x <- f()
  y <- g(x)
  z <- h(y)
} yield z

当然, 需要定义如下的隐式转换:

implicit def step[A](a: => A): Trampoline[A] =
  More(() => Done(a))

唯一不太适合用 step 转换的是(非偶然地)自递归调用. 这非常容易察觉, 当这种情况, 我们可以显示的调用 More 构造器, 例如下面的计算第 n 个斐波那契数的递归函数:

def fib(n: Int): Trampoline[Int] =
  if(n <= 1) Done(n) else for {
    x <- More(() => fib(n-1))
    y <- More(() => fib(n-2))
  } yield x + y

由于这种转换是完全机械化的, 可能有人会写一个编译器插件或者扩展 Scala 的编译期将所有程序转换成这样.

3.7 多任务

我们已经看到如何使用 flatMap 序列化的组合 Trampoline. 但其实可以通过交叉计算来并行的组合它们:

def zip[B](b: Trampoline[B]): Trampoline[(A, B)] =
  (this.resume, b.resume) match {
    case (Right(a), Right(B)) => Done((a, b))
    case (Left(a), Left(b)) =>
      More(() => a() zip b())
    case (Left(a), Right(b)) =>
      More(() => a() zip Done(b))
    case (Right(a), Left(b)) =>
      More(() => Done(a) zip b())
  }

为了验证, 我们引入控制台打印:

val hello: Trampoline[Unit] = for {
  _ <- print("Hello, ")
  _ <- println("World!")
} yield ()

将其与自身交错执行, 然后观察结果:

(hello zip hello).runT
// Hello, Hello, World!
// World!

尽管这是单线程的并行计算, 但很容易扩展到多线程的分布式计算. 对于一组待执行的 trampoline, 任何不是 Doneresume 任务都可以在任何线程执行.

可以将其推广到一个完全对称的协同程序. 我们会在下一节讨论.

3.8 Free Monads: Trampoline 的推广

我们可以将 Trampoline 看作事一个能通过 Function0 挂起并之后执行的协同程序. 那么它就不再是唯一一种有此性质的类型构造器了, 如果将该行为抽象, 我们就能获得如下的类型:

sealed trait Free[S[+_], +A] {
  private case class FlatMap[S[+_], A, +B](
    a: Free[S, A],
    f: A => Free[S, B]) extends Free[S, B]
}

case class Done[S[+_], +A](a: A) extends Free[S, A]

case class More[S[+_], +A](k: S[Free[S, A]]) extends Free[S, A]

译者注: S 是 Function0 的推广. S 具有能够挂起计算的能力.

那么, Trampoline 可以如下定义:

type Trampoline[+A] = Free[Function0, A]

就像 DoneFlatMap 数据构造器一样, Free[S, A] 是任意协变的 Functor S 的一种 Monad. 从范畴论角度来看, 它正是该 Functor 生成的 Free Monad [4]. S 必须是一个 Functor 即必须存在 Functor[S]3:

trait Functor[F[_]] {
  def map[A, B](m: F[A])(f: A => B): F[B]
}

Function0 来说是显而易见的:

implicit val f0Functor = new Functor[Function0] {
  def map[A, B](a: () => A)(f: A => B) = () => f(a())
}

3.8.1 Free Monad 的函数

为证实 Free Monad 的功效, 我们需要将之前为 Trampoline 定义的函数进行推广. 例如, 以下是 resume 的推广:

final def resume(implicit S: Functor[S]): Either[S[Free[S, A]], A] =
  this match {
    case Done(a) => Right(a)
    case More(k) => Left(k)
    case a FlatMap f => a match {
      case Done(a) => f(a).resume
      case More(k) => Left(S.map(k)(_ flatMap f))
      case b FlatMap g => b.flatMap((x: Any) => g(x) flatMap f).resume
    }
  }

注意到该定义与之前 Trampolineresume 如出一辙. 有所不同的只是类型签名, 隐式的 Functor 参数, 以及我们将显式的 Function0 构造改为了 Functormap 方法调用. 这也适用于其它函数, 如 zip, map, 和 flatMap [10]. 以下是 zip 的定义:

def zip[B](b: Free[S, B])(implicit S: Functor[S]): Free[S, (A, B)] =
  (this.resume, b.resume) match {
    case (Right(a), Right(B)) => Done((a, b))
    case (Left(a), Left(b)) => More(S.map(a)(x => More(S.map(b)(y => x zip y))))
    case (Left(a), Right(b)) => More(S.map(a)(x => x zip Done(b))
    case (Right(a), Left(b)) => More(S.map(b)(y => Done(a) zip y)
  }

3.8.2 普通数据类型到 Free Monad

粗略的来看, 我们可以将 Free[S, A] 看作事任何以某种 Functor S 作为分支节点并以某种数据类型 A 作为叶子节点的计算. 为证明这一点, 考虑一个普通的决策树. 它是一个 Free Monad, 其 Functor 将每个分支节点的计算分为两部分:

type Pair[+A] = (A, A)
type BinTree[+A] = Free[Pair, A]

BinTree[A]Done 的情况指的是包含一个类型为 A 的值的叶子节点. 其 More 则是一个包含两个类型为 BinTree[A] 的分支节点. 我们的 Free Monad (即 FlatMap 的情况) 就是对任意的节点, 给予一个树生成算法, 再将生成的树替换原有节点. 由于它是 Free 的实例, 因此 Trampline 的特性使得我们可以在恒定的时间和栈空间进行操作.

为了得到一棵能够给定任意分支数量的树, 我们可以使用 List Functor:

type Tree[+A] = Free[List, A]

事实上, List 本身就可以表示为 Free _4 的应用:

type List[A] = Free[({type λ[+α] = (A, α)}#λ), Unit]

在这种表示中, 一个 List[A] 是一个协同程序, 它每次 resume 可以产生一个类型为 A 的值, 而 Unit 则表示当前为空. 这里的 bind 并不是真正 ListflatMap (也就是由一个新生成 List 替代每个当前元素), 而是将两个 List 首尾相连5. 同样, 由于是 Free 的实例, 操作均在恒定的时间和栈空间中.

这里所给的 List 的类型参数是一个逆变的, 但可以改为协变. 这可以作为一个小练习.

3.8.3 一个 Free 的 State Monad

尽管本文所举的 Free Monad 的例子非常简单, 用来挂起的 Functor 可以是非常复杂的. 它可以以任意的组合方式来输出或等待输入. 我们可以把 State 作为一种小型语言, 这是状态的 setput 定义:

sealed trait StateF[S, +A]
case class Get[S, A](f: S => A) extends StateF[S, A]
case class Put[S, A](s: S, a: A) extends StateF[S, A]

Get 的构造器中, f 是一个接收当前状态值的函数. 而在 Put 构造器中, s 是一个新的状态, a 是计算的剩余部分 (可能用来对状态做一些事情).

我们要指明当前的数据类型是一个 Functor:

implicit def statefFun[S] = new Functor[({type λ[+α] = StateF[S, α)})#λ] {
  def map[A, B](m: StateF[S, A])(f: A => B) = m match {
    case Get(g) => Get((s: S) => f(g(s)))
    case Put(s, a) => Put(s, f(a))
  }
}

我们可以直接用 StateF Functor 来生成一个类 StateMonad:

type FreeState[S, +A] = Free[({type λ[+α] = StateF[S, α)})#λ, A]

第一部分的 pureState 组合子可以由 Free MonadDone 构造器生成:

def pureState[S, A](a: A): FreeState[S, A] = Done(a)

另外的获取和设置状态的方法也可以简单定义:

def getState[S]: FreeState[S, S] = More(Get(s => Done(s)))

def setState[S](s: S): FreeState[S, Unit] = More(Put(s, Done()))

执行一个 State 只需一个简单的循环:

def evalS[S, A](s: S, t: FreeState[S, A]): A = t.resume match {
  case Left(Get(f)) => evalS(s, f(s))
  case Left(Put(n, a)) => evalS(n, a)
  case Right(a) => a
}

现在我们可以在该 Monad 中写纯函数了. 比如, 一下是第一部分的 zipIndex, 这次我们用 FreeStateMonda:

def zipIndex[A](as: List[A]): List[(Int, A)] = evalS(0, as.foldLeft(
  pureState[Int, List[(Int, A)]](List())) {
  (acc, a) => for {
    rs <- acc
    n <- getState
    _ <- setState(n + 1)
  } yield (n,a)::xs }).reverse

实现的方式几乎是一致的, 同时在不使用 Trampoline 的情况下使用恒定的栈空间. 这里的结论是不需要总使用 trampoline 来封装数据类型. 可以使用新的 Free Monad 来实现同样的目的.

3.9 其它

后续部分是作者对当前工作的总结和展望.