由于最近经常在生产中使用 Free Monad
范式, 所以就顺带看了看 cats.Free
的实现, 发现其实现方式和我想想中的 Free
不太一样, 除了 Pure
(Return
), Suspend
两种状态之外, 还有一个 FlatMapped
状态. 这样做看上去是为了将 flatMap
的 evaluation 延迟至 transform 期间完成, 但还是隐隐感觉和堆栈安全有关. 顺带想起关于 Scala FP 的堆栈安全一直是自己知识体系的一个空白, 因此就有了翻译 Stackless Scala With Free Monads
这篇论文的冲动, 期望这个工作能 drive 自己搞清楚 Trampolining.
这篇论文是 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
Scala 编译器的尾调用优化 (Tail Call Elimination, TCE) 仅限于自递归方法 (Self-Recursive Methods) 而不是所有的尾调用. 这很容易使得那些由小函数组合而成的函数造成堆栈溢出. 拥有一个通用的尾调用优化方法非常有益于 Scala 语言, 尤其是有益于 Scala 的函数式编程. 针对那些本不支持尾调用优化的语言, Trampolining 是一个可以用于解决尾调用优化问题的常用方法. 本文引入 Trampolining 技术, 以将其推广为一个对任何方法调用(甚至是非尾部调用)的优化技术. 该技术能够完全的消除 Scala 程序对调用堆栈的使用.
调用堆栈是虚拟机的有限资源, 稍微有点经验的 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
的值以及一个新的状态. 而 map
和 flatMap
方法允许通过 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))
注意到 pureState
和 flatMap
两个方法使得 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
译者注: 对于
State
的flatMap
方法, 每次调用都会在堆中产生一个新的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 的函数式编程特性. 而这篇论文就是来探讨解决方案的:
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) }
当编译器发现一个函数在尾部调用了自己, 并且 该函数不能被重载 (例如, 使用 private
或 final
关键字声明), 则该函数的自调用会被编译替换为一个简单的跳转 (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 编译器的尾调用优化难以实现.
不过还好, 我们可以回避所有这些问题, 通过一种简单的数据结构来用堆空间来代替栈空间.
我们从一个非常简单的 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
有多长, 上面的代码也不会再引发堆栈溢出.
让我们看看如何使用 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 来优化.
现在我们要用一个 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
M
的unit
函数定义为A => M[A]
. 之所以叫做unit
是因为它是flatMap
操作的幺元
.
这显然有所改进. 它把问题转移到 Trampoline
的 flatMap
方法中, 因此我们可以这样试着实现:
def flatMap[B](f: A => Trampoline[B]) = More[B](() => f(runT))
但这不是我们想要的. runT
依然不在调用的尾部. 看来无论我们怎么实现 Trampoline
的 flatMap
, 都不可能不消耗额外的栈空间.
解决该问题的方法是为 Trampoline
数据类型增加一个类型构造器,并将 flatMap
方法的调用改为对构造器的调用:
case class FlatMap[A, +B]( sub: Trampoline[A], k: A => Trampoline[B]) extends Trampoline[B]
Trampoline
的这种形式可以看作是对子程序 sub
的调用,其结果会被传入 continuation
k
.
这时候, Trampoline
trait
的 runT
就需要考虑这个新的构造器. 为了简单说明, 让我们把执行下一步与执行所有步分开:
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
这种情况的方式微秒而重要. 如果子程序 a
是 Done
, 我们会直接执行 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
中包含两个值, 左侧的子程序表明了下一步计算的类型, 右侧的函数表明了在下一步计算后需要做的事, 因此可用于非尾递归的后续操作.
这里还有一个边缘情况需要考虑. resume
函数依然有可能造成栈溢出. 倘若 FlatMap
左侧的堆叠层次高出了调用栈的高度, f(v)
的调用会导致 g(x)
调用, 然后会导致里层的函数调用, 等等. 这可以通过在一开始就禁止左结合的内嵌的构造来避免. 我们将 FlatMap
构造器变为 private
, 并通过暴露 Trampoline
的 flatMap
方法来代替它, 这样就能总是用右结合的结构来改写:
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)))
通过使用 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 的编译期将所有程序转换成这样.
我们已经看到如何使用 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
, 任何不是 Done
的 resume
任务都可以在任何线程执行.
可以将其推广到一个完全对称的协同程序. 我们会在下一节讨论.
我们可以将 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]
就像 Done
和 FlatMap
数据构造器一样, 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()) }
为证实 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 } }
注意到该定义与之前 Trampoline
的 resume
如出一辙. 有所不同的只是类型签名, 隐式的 Functor
参数, 以及我们将显式的 Function0
构造改为了 Functor
的 map
方法调用. 这也适用于其它函数, 如 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) }
粗略的来看, 我们可以将 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
并不是真正 List
的 flatMap
(也就是由一个新生成 List
替代每个当前元素), 而是将两个 List
首尾相连5. 同样, 由于是 Free
的实例, 操作均在恒定的时间和栈空间中.
这里所给的 List
的类型参数是一个逆变的, 但可以改为协变. 这可以作为一个小练习.
尽管本文所举的 Free Monad
的例子非常简单, 用来挂起的 Functor
可以是非常复杂的. 它可以以任意的组合方式来输出或等待输入. 我们可以把 State
作为一种小型语言, 这是状态的 set
和 put
定义:
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
来生成一个类 State
的 Monad
:
type FreeState[S, +A] = Free[({type λ[+α] = StateF[S, α)})#λ, A]
第一部分的 pureState
组合子可以由 Free Monad
的 Done
构造器生成:
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
, 这次我们用 FreeState
的 Monda
:
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
来实现同样的目的.
后续部分是作者对当前工作的总结和展望.