在前面几次讨论中我们介绍了Free是个产生Monad的最基本结构。它的原理是把一段程序(AST)一连串的运算指令(ADT)转化成数据结构存放在内存里,这个过程是个独立的功能描述过程。然后另一个独立运算过程的Interpreter会遍历(traverse)AST结构,读取结构里的运算指令,实际运行指令。这里的重点是把一连串运算结构化(reify)延迟运行,具体实现方式是把Monad的连续运算方法flatMap转化成一串Suspend结构(case class),把运算过程转化成创建(construct)Suspend过程。flatMap的表现形式是这样的:flatMap(a => flatMap(b => flatMap(c => ....))),这是是明显的递归算法,很容易产生堆栈溢出异常(StackOverflow Exception),无法保证程序的安全运行,如果不能有效解决则FP编程不可行。Free正是解决这个问题的有效方法,因为它把Monad的递归算法flatMap转化成了一个创建数据结构实例的过程。每创建一个Suspend,立即完成一个运算。我们先用个例子来证明Monad flatMap的递归算法问题:
def zipIndex[A](xa: List[A]): List[(Int,A)] = xa.foldLeft(State.state[Int,List[(Int,A)]](List()))( (acc,a) => for { xn <- acc s <- get[Int] _ <- put[Int](s+1) } yield ((s,a) :: xn) ).eval(1).reverse //> zipIndex: [A](xa: List[A])List[(Int, A)] zipIndex(1 |-> 10) //> res6: List[(Int, Int)] = List((1,1), (2,2), (3,3), (4,4), (5,5), (6,6), (7,7), (8,8), (9,9), (10,10)) zipIndex(1 |-> 10000) //> java.lang.StackOverflowError
我们提到过用Trampoline可以heap换stack,以遍历数据结构代替递归运算来实现运行安全。那么什么是Trampoline呢?
sealed trait Trampoline[+A] case class Done[A](a: A) extends Trampoline[A] case class More[A](k: () => Trampoline[A]) extends Trampoline[A]
我们先试个递归算法例子:
def isEven(xa: List[Int]): Boolean = xa match { case Nil => true case h :: t => isOdd(t) } //> isEven: (xa: List[Int])Boolean def isOdd(xa: List[Int]): Boolean = xa match { case Nil => false case h :: t => isEven(t) } //> isOdd: (xa: List[Int])Boolean isOdd(0 |-> 100) //> res0: Boolean = true isEven(0 |-> 10000) //> java.lang.StackOverflowError
现在重新调整一下函数isEven和isOdd的返回结构类型:从Boolean换成Trampoline,意思是从返回一个结果值变成返回一个数据结构:
def even(xa: List[Int]): Trampoline[Boolean] = xa match { case Nil => Done(true) case h :: t => More(() => odd(t)) } //> even: (xa: List[Int])Exercises.trampoline.Trampoline[Boolean] def odd(xa: List[Int]): Trampoline[Boolean] = xa match { case Nil => Done(false) case h :: t => More(() => even(t)) } //> odd: (xa: List[Int])Exercises.trampoline.Trampoline[Boolean] even(1 |-> 123001) //> res0: Exercises.trampoline.Trampoline[Boolean] = More(<function0>)
sealed trait Trampoline[+A] { final def runT: A = this match { case Done(a) => a case More(k) => k().runT } } even(1 |-> 123001).runT //> res0: Boolean = false
实际上scalaz也提供了Trampoline类型:scalaz/Free.scala
/** A computation that can be stepped through, suspended, and paused */ type Trampoline[A] = Free[Function0, A] ... object Trampoline extends TrampolineInstances { def done[A](a: A): Trampoline[A] = Free.Return[Function0,A](a) def delay[A](a: => A): Trampoline[A] = suspend(done(a)) def suspend[A](a: => Trampoline[A]): Trampoline[A] = Free.Suspend[Function0, A](() => a) }
import scalaz.Free.Trampoline def even(xa: List[Int]): Trampoline[Boolean] = xa match { case Nil => Trampoline.done(true) case h :: t => Trampoline.suspend(odd(t)) } //> even: (xa: List[Int])scalaz.Free.Trampoline[Boolean] def odd(xa: List[Int]): Trampoline[Boolean] = xa match { case Nil => Trampoline.done(false) case h :: t => Trampoline.suspend(even(t)) } //> odd: (xa: List[Int])scalaz.Free.Trampoline[Boolean] even(1 |-> 123001).run //> res0: Boolean = false
def lift[M[_]: Applicative]: IndexedStateT[({type λ[α]=M[F[α]]})#λ, S1, S2, A] = new IndexedStateT[({type λ[α]=M[F[α]]})#λ, S1, S2, A] { def apply(initial: S1): M[F[(S2, A)]] = Applicative[M].point(self(initial)) }
def incr: State[Int, Int] = State {s => (s+1, s)}//> incr: => scalaz.State[Int,Int] incr.replicateM(10000).eval(0) take 10 //> java.lang.StackOverflowError import scalaz.Free.Trampoline incr.lift[Trampoline].replicateM(100000).eval(0).run.take(10) //> res0: List[Int] = List(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
上面这个例子也使用了State Monad:函数incr返回的是State,这时用replicateM(10000).eval(0)对重复对10000个State进行运算时产生了StackOverflowError。我们跟着用lift把incr返回类型变成StateT[Trampoline,S,A],这时replicateM(10000).eval(0)的作用就是进行结构转化了(State.apply:Trampoline[(S,A)]),再用Trampoline.run作为Interpreter遍历结构进行运算。用lift升格Trampoline后解决了StackOverflowError。
我们试着调整一下zipIndex函数:
def safeZipIndex[A](xa: List[A]): List[(Int,A)] = (xa.foldLeft(State.state[Int,List[(Int,A)]](List()))( (acc,a) => for { xn <- acc s <- get[Int] _ <- put(s + 1) } yield (s,a) :: xn ).lift[Trampoline]).eval(1).run.reverse //> safeZipIndex: [A](xa: List[A])List[(Int, A)] safeZipIndex(1 |-> 1000).take(10) //> res2: List[(Int, Int)] = List((1,1), (2,2), (3,3), (4,4), (5,5), (6,6), (7,7), (8,8), (9,9), (10,10))
safeZipIndex(1 |-> 10000).take(10) //> java.lang.StackOverflowError //| at scalaz.IndexedStateT$$anonfun$flatMap$1.apply(StateT.scala:62) //| at scalaz.IndexedStateT$$anon$10.apply(StateT.scala:95) //| at scalaz.IndexedStateT$$anonfun$flatMap$1.apply(StateT.scala:62) ...
以上我们证明了Trampoline可以把连续运算转化成创建数据结构,以heap内存换stack,能保证递归算法运行的安全。因为Trampoline是Free的一个特例,所以Free的Interpreter也就可以保证递归算法安全运行。现在可以得出这样的结论:FP就是Monadic Programming,就是用Monad来编程,我们应该尽量用Free来生成Monad,用Free进行编程以保证FP程序的可靠性。