在前面几次讨论中我们介绍了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的递归算法问题:

 ef 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+)
} yield ((s,a) :: xn)
).eval().reverse //> zipIndex: [A](xa: List[A])List[(Int, A)] zipIndex( |-> ) //> 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( |-> ) //> java.lang.StackOverflowError

在这个例子里我们使用了State Monad。我们知道for-comprehension就是flatMap链条,是一种递归算法,所以当zipIndex针对大List时就产生了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]

Trampoline就是一种数据结构。它有两种状态:Done(a: A)代表结构内存放了一个A值,More(k: ()=>Trampoline[A])代表结构内存放的是一个Function0函数,就是一个运算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( |-> ) //> res0: Boolean = true
isEven( |-> ) //> java.lang.StackOverflowError

可以看到isEven和isOdd这两个函数相互递归调用,最终用大点的List就产生了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( |-> ) //> res0: Exercises.trampoline.Trampoline[Boolean] = More(<function0>)

现在我们获得了一个在heap上存放了123001个元素的数据结构More(<function0>)。这是一个在内存heap上存放的过程,并没有任何实质运算。
现在我们需要一个方法来遍历这个返回的结构,逐个运行结构中的function0:

 sealed trait Trampoline[+A] {
final def runT: A =
this match {
case Done(a) => a
case More(k) => k().runT
}
} even( |-> ).runT //> res0: Boolean = false

由于这个runT是个尾递归(Tail Call Elimination TCE)算法,所以没有出现StackOverflowError。

实际上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)
}

Trampoline就是Free[S,A]的一种特例。S == Function0,或者说Trampoline就是Free针对Function0生成的Monad,因为我们可以用Free.Return和Free.Suspend来实现Done和More。我们可以把scalaz的Trampoline用在even,odd函数里:

 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( |-> ).run //> res0: Boolean = false

语法同我们自定义的Trampoline差不多。

那么我们能不能把Trampoline用在上面的哪个zipIndex函数里来解决StackOverflowError问题呢?zipIndex里造成问题的Monad是个State Monad,我们可以用State.lift把State[S,A升格成StateT[Trampoline,S,A]。先看看这个lift函数:scalaz/StateT.scala

 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))
}

这个函数的功能等于是:State.lift[Trampoline] >>> StateT[Tarmpoline,S,A]。先看另一个简单例子:

 def incr: State[Int, Int] =  State {s => (s+, s)}//> incr: => scalaz.State[Int,Int]
incr.replicateM().eval() take //> java.lang.StackOverflowError import scalaz.Free.Trampoline
incr.lift[Trampoline].replicateM().eval().run.take()
//> 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 + )
} yield (s,a) :: xn
).lift[Trampoline]).eval().run.reverse //> safeZipIndex: [A](xa: List[A])List[(Int, A)] safeZipIndex( |-> ).take() //> 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))

表面上来看结果好像是正确的。试试大一点的List:

  safeZipIndex( |-> ).take()   //> 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)
...

还是StackOverflowError,看错误提示是State.flatMap造成的。看来迟点还是按照incr的原理把foldLeft运算阶段结果拆分开来分析才行。

以上我们证明了Trampoline可以把连续运算转化成创建数据结构,以heap内存换stack,能保证递归算法运行的安全。因为Trampoline是Free的一个特例,所以Free的Interpreter也就可以保证递归算法安全运行。现在可以得出这样的结论:FP就是Monadic Programming,就是用Monad来编程,我们应该尽量用Free来生成Monad,用Free进行编程以保证FP程序的可靠性。

Scalaz(35)- Free :运算-Trampoline,say NO to StackOverflowError的更多相关文章

  1. Scalaz(53)- scalaz-stream: 程序运算器-application scenario

    从上面多篇的讨论中我们了解到scalaz-stream代表一串连续无穷的数据或者程序.对这个数据流的处理过程就是一个状态机器(state machine)的状态转变过程.这种模式与我们通常遇到的程序流 ...

  2. Scalaz(47)- scalaz-stream: 深入了解-Source

    scalaz-stream库的主要设计目标是实现函数式的I/O编程(functional I/O).这样用户就能使用功能单一的基础I/O函数组合成为功能完整的I/O程序.还有一个目标就是保证资源的安全 ...

  3. Scalaz(44)- concurrency :scalaz Future,尚不完整的多线程类型

    scala已经配备了自身的Future类.我们先举个例子来了解scala Future的具体操作: import scala.concurrent._ import ExecutionContext. ...

  4. Smarty数学运算

    数学运算可以直接应用到变量 Example 3-5. math examples 例 3-5.数学运算的例子   {$foo+1} {$foo*$bar} {* some more complicat ...

  5. C/C+小记

    1.struct与typedef struct struct Student{int a;int b}stu1; //定义名为Student的结构体,及一个Student变量stu1 struct { ...

  6. java实现随机四则运算

    使用JAVA编程语言,独立完成一个包含3到5个数字的四则运算练习,软件基本功能要求如下: 程序可接收一个输入参数n,然后随机产生n道加减乘除练习题,每个数字在 0 和 100 之间,运算符在3个到5个 ...

  7. kotlin递归&尾递归优化

    递归: 对于递归最经典的应用当然就是阶乘的计算啦,所以下面用kotlin来用递归实现阶乘的计算: 编译运行: 那如果想看100的阶乘是多少呢? 应该是结果数超出了Int的表述范围,那改成Long型再试 ...

  8. Scalaz(50)- scalaz-stream: 安全的无穷运算-running infinite stream freely

    scalaz-stream支持无穷数据流(infinite stream),这本身是它强大的功能之一,试想有多少系统需要通过无穷运算才能得以实现.这是因为外界的输入是不可预料的,对于系统本身就是无穷的 ...

  9. Scalaz(56)- scalaz-stream: fs2-安全运算,fs2 resource safety

    fs2在处理异常及资源使用安全方面也有比较大的改善.fs2 Stream可以有几种方式自行引发异常:直接以函数式方式用fail来引发异常.在纯代码里隐式引发异常或者在运算中引发异常,举例如下: /函数 ...

随机推荐

  1. atitit.js浏览器环境下的全局异常捕获

    atitit.js浏览器环境下的全局异常捕获 window.onerror = function(errorMessage, scriptURI, lineNumber) { var s= JSON. ...

  2. 异步委托(APM)使用Func异步操作,处理耗时操作

    使用委托进行异步操作,处理一些耗时操作,防止主线程阻塞 使用例子: using System; using System.Collections.Generic; using System.Linq; ...

  3. 学习ASP.NET MVC(六)——我的第一个ASP.NET MVC 编辑页面

    在上一文章中由Entity Framework(实体框架)去实现了对数据库的CURD操作.在本篇文章中,主要是调试修改自动生成的动作方法和视图,以及调试编辑功能与编辑功能的Book控制器. 首先,在V ...

  4. 为什么说基于TCP的移动端IM仍然需要心跳保活?

    1.前言 很多人认为,TCP协议自身先天就有KeepAlive机制,为何基于它的通讯链接,仍然需要在应用层实现额外的心跳保活?本文将从移动端IM实践的角度告诉你,即使使用的是TCP协议,应用层的心跳保 ...

  5. Java environment variables and their functionality

    Explanations of Functionalities: 1. PATH env variable It is used to search the command directory whe ...

  6. 中小型ERP系统开发与实施

    1. 能反映企业管理的各个方面和解决企业实际的问题,不限于一般问题的解决,而是深入企业的业务过程 2. 在此软件的背后有真正的管理思想(不是泛泛而言)和对管理的精髓理解 和归纳,有一个整体的较详细的规 ...

  7. struts深入原理之RequestProcessor与xml

    和配置文件相对应的代码(struts1) public void process(HttpServletRequest request, HttpServletResponse response)   ...

  8. HTML5移动Web开发(八)——避免文本字体大小重置

    适用设备:iOS.Windows Mobile在一些移动设备上,比方说iPhone,Windows Mobile,当用户把手机切换到横屏时,浏览器会自动地重置文本字体大小.这可能会对我们造成困扰,因为 ...

  9. [转载]基于TFS实践敏捷-修复Bug和执行代码评审

    本主题阐释了这些功能,以继续这一关注虚拟敏捷团队成员的一天的教程. Peter 忙于编写一些代码以完成积压工作 (backlog) 项任务.但是,他的同事发现了一个阻碍他们工作的 Bug,他想立即修复 ...

  10. layout_weight详解

    注:LinearLayout中的TextView按比例显示的时候,layout_width="0dp"或者layout_height="0dp" 在androi ...