在前面几次讨论中我们介绍了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. position格式布局

    布局大体分为: 位置--position 绝对坐标 absolute 绝对定位的元素 不受其他位置影响 可通过z-index进行层次分级 body来定位自己 相对坐标 设置  top和left之后 r ...

  2. iOS-数据持久化-偏好设置

    一.简单介绍 很多iOS应用都支持偏好设置,比如保存用户名.密码.字体大小等设置,iOS提供了一套标准的解决方案来为应用加入偏好设置功能 每个应用都有个NSUserDefaults实例,通过它来存取偏 ...

  3. 事件异步(EAP)使用事件异步处理一些耗时操作

    比如需要下载一些比较大的文件,如果使用会UI卡顿,使用异步可以节省一些时间 下面是一些例子: using System; using System.Collections.Generic; using ...

  4. 每天一个linux命令(24):Linux文件类型与扩展名

    Linux文件类型和Linux文件的文件名所代表的意义是两个不同的概念.我们通过一般应用程序而创建的比如file.txt.file.tar.gz ,这些文件虽然要用不同的程序来打开,但放在Linux文 ...

  5. Java编程思想(Chapter2、4、6)

    一切皆对象 用引用操纵对象 Java中操纵的标识符实际上是对象的“引用”.例如想要操纵一个字符串,则可以创建一个String引用. String s; 此处s只是一个引用. 存储位置 基本类型/对象的 ...

  6. php基础教程-变量

    变量来源于数学,是计算机语言中能储存计算结果或能表示值抽象概念.变量可以通过变量名访问.在指令式语言中,变量通常是可变的:但在纯函数式语言(如Haskell)中,变量可能是不可变(immutable) ...

  7. Spark入门实战系列--6.SparkSQL(上)--SparkSQL简介

    [注]该系列文章以及使用到安装包/测试数据 可以在<倾情大奉送--Spark入门实战系列>获取 .SparkSQL的发展历程 1.1 Hive and Shark SparkSQL的前身是 ...

  8. Python编码问题整理

    认识常见编码 GB2312是中国规定的汉字编码,也可以说是简体中文的字符集编码 GBK 是 GB2312的扩展 ,除了兼容GB2312外,它还能显示繁体中文,还有日文的假名 cp936:中文本地系统是 ...

  9. 【环境配置】Linux环境下下载、配置java环境、安装eclipse、建立eclipse快捷方式详解

    一.首先是下载Java JDK 到目前为止的最新版本为(jdk1.8.0_60),有两种方式进行下载: 1.使用shell来进行下载,可使用如下命令直接进行下载: wget --no-check-ce ...

  10. Windows Azure Web Site (6) 使用FTP发布Azure Web Site

    <Windows Azure Platform 系列文章目录> 笔者在之前的文章中介绍的都是使用IDE,也就是Visual Studio,将本地的aspx发布到Azure Web Site ...