泛函编程方式其中一个特点就是普遍地使用递归算法,而且有些地方还无法避免使用递归算法。比如说flatMap就是一种推进式的递归算法,没了它就无法使用for-comprehension,那么泛函编程也就无法被称为Monadic Programming了。虽然递归算法能使代码更简洁易明,但同时又以占用堆栈(stack)方式运作。堆栈是软件程序有限资源,所以在使用递归算法对大型数据源进行运算时系统往往会出现StackOverflow错误。如果不想办法解决递归算法带来的StackOverflow问题,泛函编程模式也就失去了实际应用的意义了。

针对StackOverflow问题,Scala compiler能够对某些特别的递归算法模式进行优化:把递归算法转换成while语句运算,但只限于尾递归模式(TCE, Tail Call Elimination),我们先用例子来了解一下TCE吧:

以下是一个右折叠算法例子:

 def foldR[A,B](as: List[A], b: B, f: (A,B) => B): B = as match {
case Nil => b
case h :: t => f(h,foldR(t,b,f))
} //> foldR: [A, B](as: List[A], b: B, f: (A, B) => B)B
def add(a: Int, b: Int) = a + b //> add: (a: Int, b: Int)Int foldR((1 to 100).toList, 0, add) //> res0: Int = 5050
foldR((1 to 10000).toList, 0, add) //> java.lang.StackOverflowError

以上的右折叠算法中自引用部分不在最尾部,Scala compiler无法进行TCE,所以处理一个10000元素的List就发生了StackOverflow。

再看看左折叠:

 def foldL[A,B](as: List[A], b: B, f: (B,A) => B): B = as match {
case Nil => b
case h :: t => foldL(t,f(b,h),f)
} //> foldL: [A, B](as: List[A], b: B, f: (B, A) => B)B
foldL((1 to 100000).toList, 0, add) //> res1: Int = 705082704

在这个左折叠例子里自引用foldL出现在尾部位置,Scala compiler可以用TCE来进行while转换:

   def foldl2[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(z, x)
az = xs
}
}
}
z
}

经过转换后递归变成Jump,程序不再使用堆栈,所以不会出现StackOverflow。

但在实际编程中,统统把递归算法编写成尾递归是不现实的。有些复杂些的算法是无法用尾递归方式来实现的,加上JVM实现TCE的能力有局限性,只能对本地(Local)尾递归进行优化。

我们先看个稍微复杂点的例子:

 def even[A](as: List[A]): Boolean = as match {
case Nil => true
case h :: t => odd(t)
} //> even: [A](as: List[A])Boolean
def odd[A](as: List[A]): Boolean = as match {
case Nil => false
case h :: t => even(t)
} //> odd: [A](as: List[A])Boolean

在上面的例子里even和odd分别为跨函数的各自的尾递归,但Scala compiler无法进行TCE处理,因为JVM不支持跨函数Jump:

 even((1 to 100).toList)                           //> res2: Boolean = true
even((1 to 101).toList) //> res3: Boolean = false
odd((1 to 100).toList) //> res4: Boolean = false
odd((1 to 101).toList) //> res5: Boolean = true
even((1 to 10000).toList) //> java.lang.StackOverflowError

处理10000个元素的List还是出现了StackOverflowError

我们可以通过设计一种数据结构实现以heap交换stack。Trampoline正是专门为解决StackOverflow问题而设计的数据结构:

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

Trampoline代表一个可以一步步进行的运算。每步运算都有两种可能:Done(a),直接完成运算并返回结果a,或者More(k)运算k后进入下一步运算;下一步又有可能存在Done和More两种情况。注意Trampoline的runT方法是明显的尾递归,而且runT有final标示,表示Scala可以进行TCE。

有了Trampoline我们可以把even,odd的函数类型换成Trampoline:

 def even[A](as: List[A]): Trampoline[Boolean] = as match {
case Nil => Done(true)
case h :: t => More(() => odd(t))
} //> even: [A](as: List[A])ch13.ex1.Trampoline[Boolean]
def odd[A](as: List[A]): Trampoline[Boolean] = as match {
case Nil => Done(false)
case h :: t => More(() => even(t))
} //> odd: [A](as: List[A])ch13.ex1.Trampoline[Boolean]

我们可以用Trampoline的runT来运算结果:

 even((1 to 10000).toList).runT                    //> res6: Boolean = true
even((1 to 10001).toList).runT //> res7: Boolean = false
odd((1 to 10000).toList).runT //> res8: Boolean = false
odd((1 to 10001).toList).runT //> res9: Boolean = true

这次我们不但得到了正确结果而且也没有发生StackOverflow错误。就这么简单?

我们再从一个比较实际复杂一点的例子分析。在这个例子中我们遍历一个List并维持一个状态。我们首先需要State类型:

 case class State[S,+A](runS: S => (A,S)) {
import State._
def flatMap[B](f: A => State[S,B]): State[S,B] = State[S,B] {
s => {
val (a1,s1) = runS(s)
f(a1) runS s1
}
}
def map[B](f: A => B): State[S,B] = flatMap( a => unit(f(a)))
}
object State {
def unit[S,A](a: A) = State[S,A] { s => (a,s) }
def getState[S]: State[S,S] = State[S,S] { s => (s,s) }
def setState[S](s: S): State[S,Unit] = State[S,Unit] { _ => ((),s)}
}

再用State类型来写一个对List元素进行序号标注的函数:

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

运行一下这个zip函数:

 zip((1 to 10).toList)                             //> res0: List[(Int, Int)] = List((1,0), (2,1), (3,2), (4,3), (5,4), (6,5), (7,6
//| ), (8,7), (9,8), (10,9))

结果正确。如果针对大型的List呢?

 zip((1 to 10000).toList)                          //> java.lang.StackOverflowError

按理来说foldLeft是尾递归的,怎么StackOverflow出现了。这是因为State组件flatMap是一种递归算法,也会导致StackOverflow。那么我们该如何改善呢?我们是不是像上面那样把State转换动作的结果类型改成Trampoline就行了呢?

 case class State[S,A](runS: S => Trampoline[(A,S)]) {
def flatMap[B](f: A => State[S,B]): State[S,B] = State[S,B] {
s => More(() => {
val (a1,s1) = runS(s).runT
More(() => f(a1) runS s1)
})
}
def map[B](f: A => B): State[S,B] = flatMap( a => unit(f(a)))
}
object State {
def unit[S,A](a: A) = State[S,A] { s => Done((a,s)) }
def getState[S]: State[S,S] = State[S,S] { s => Done((s,s)) }
def setState[S](s: S): State[S,Unit] = State[S,Unit] { _ => Done(((),s))}
}
trait Trampoline[+A] {
final def runT: A = this match {
case Done(a) => a
case More(k) => k().runT
}
}
case class Done[+A](a: A) extends Trampoline[A]
case class More[+A](k: () => Trampoline[A]) extends Trampoline[A] def zip[A](as: List[A]): List[(A,Int)] = {
as.foldLeft(
unit[Int,List[(A,Int)]](List()))(
(acc,a) => for {
xs <- acc
n <- getState[Int]
_ <- setState[Int](n + 1)
} yield (a,n) :: xs
).runS(0).runT._1.reverse
} //> zip: [A](as: List[A])List[(A, Int)]
zip((1 to 10).toList) //> res0: List[(Int, Int)] = List((1,0), (2,1), (3,2), (4,3), (5,4), (6,5), (7,
//| 6), (8,7), (9,8), (10,9))

在这个例子里我们把状态转换函数 S => (A,S) 变成 S => Trampoline[(A,S)]。然后把其它相关函数类型做了相应调整。运行zip再检查结果:结果正确。那么再试试大型List:

 zip((1 to 10000).toList)                          //> java.lang.StackOverflowError

还是会出现StackOverflow。这次是因为flatMap中的runT不在尾递归位置。那我们把Trampoline变成Monad看看如何?那我们就得为Trampoline增加一个flatMap函数:

 trait Trampoline[+A] {
final def runT: A = this match {
case Done(a) => a
case More(k) => k().runT
}
def flatMap[B](f: A => Trampoline[B]): Trampoline[B] = {
this match {
case Done(a) => f(a)
case More(k) => f(runT)
}
}
}
case class Done[+A](a: A) extends Trampoline[A]
case class More[+A](k: () => Trampoline[A]) extends Trampoline[A]

这样我们可以把State.flatMap调整成以下这样:

 case class State[S,A](runS: S => Trampoline[(A,S)]) {
def flatMap[B](f: A => State[S,B]): State[S,B] = State[S,B] {
s => More(() => {
// val (a1,s1) = runS(s).runT
// More(() => f(a1) runS s1)
runS(s) flatMap { // runS(s) >>> Trampoline
case (a1,s1) => More(() => f(a1) runS s1)
}
})
}
def map[B](f: A => B): State[S,B] = flatMap( a => unit(f(a)))
}

现在我们把递归算法都推到了Trampoline.flatMap这儿了。不过Trampoline.flatMap的runT引用f(runT)不在尾递归位置,所以这样调整还不足够。看来核心还是要解决flatMap尾递归问题。我们可以再为Trampoline增加一个状态结构FlatMap然后把flatMap函数引用变成类型实例构建(type construction):

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

case class FlatMap这种Trampoline状态意思是先引用sub然后把结果传递到下一步k再运行k:基本上是沿袭flatMap功能。再调整Trampoline.resume, Trampoline.flatMap把FlatMap这种状态考虑进去:

 trait Trampoline[+A] {
final def runT: A = resume match {
case Right(a) => a
case Left(k) => k().runT
}
def flatMap[B](f: A => Trampoline[B]): Trampoline[B] = {
this match {
// case Done(a) => f(a)
// case More(k) => f(runT)
case FlatMap(a,g) => FlatMap(a, (x: Any) => g(x) flatMap f)
case x => FlatMap(x, f)
}
}
def map[B](f: A => B) = flatMap(a => Done(f(a)))
def resume: Either[() => Trampoline[A], A] = this match {
case Done(a) => Right(a)
case More(k) => Left(k)
case FlatMap(a,f) => a match {
case Done(v) => f(v).resume
case More(k) => Left(() => k() flatMap f)
case FlatMap(b,g) => FlatMap(b, (x: Any) => g(x) flatMap f).resume
}
}
}
case class Done[+A](a: A) extends Trampoline[A]
case class More[+A](k: () => Trampoline[A]) extends Trampoline[A]
case class FlatMap[A,B](sub: Trampoline[A], k: A => Trampoline[B]) extends Trampoline[B]

在以上对Trampoline的调整里我们引用了Monad的结合特性(associativity):

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

重新右结合后我们可以用FlatMap正确表达复数步骤的运算了。

现在再试着运行zip:

 def zip[A](as: List[A]): List[(A,Int)] = {
as.foldLeft(
unit[Int,List[(A,Int)]](List()))(
(acc,a) => for {
xs <- acc
n <- getState[Int]
_ <- setState[Int](n + 1)
} yield (a,n) :: xs
).runS(0).runT._1.reverse
} //> zip: [A](as: List[A])List[(A, Int)]
zip((1 to 10000).toList) //> res0: List[(Int, Int)] = List((1,0), (2,1), (3,2), (4,3), (5,4), (6,5), (7,

这次运行正常,再不出现StackOverflowError了。

实际上我们可以考虑把Trampoline当作一种通用的堆栈溢出解决方案。

我们首先可以利用Trampoline的Monad特性来调控函数引用,如下:

 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: [A](a: => A)ch13.ex1.Trampoline[A]
def getNum: Double = 3 //> getNum: => Double
def addOne(x: Double) = x + 1 //> addOne: (x: Double)Double
def timesTwo(x: Double) = x * 2 //> timesTwo: (x: Double)Double
(for {
x <- getNum
y <- addOne(x)
z <- timesTwo(y)
} yield z).runT //> res6: Double = 8.0

又或者:

 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
} //> fib: (n: Int)ch13.ex1.Trampoline[Int]
(fib(10)).runT //> res7: Int = 55

从上面得出我们可以用flatMap来对Trampoline运算进行流程控制。另外我们还可以通过把多个Trampoline运算交叉组合来实现并行运算:

 trait Trampoline[+A] {
final def runT: A = resume match {
case Right(a) => a
case Left(k) => k().runT
}
def flatMap[B](f: A => Trampoline[B]): Trampoline[B] = {
this match {
// case Done(a) => f(a)
// case More(k) => f(runT)
case FlatMap(a,g) => FlatMap(a, (x: Any) => g(x) flatMap f)
case x => FlatMap(x, f)
}
}
def map[B](f: A => B) = flatMap(a => Done(f(a)))
def resume: Either[() => Trampoline[A], A] = this match {
case Done(a) => Right(a)
case More(k) => Left(k)
case FlatMap(a,f) => a match {
case Done(v) => f(v).resume
case More(k) => Left(() => k() flatMap f)
case FlatMap(b,g) => FlatMap(b, (x: Any) => g(x) flatMap f).resume
}
}
def zip[B](tb: Trampoline[B]): Trampoline[(A,B)] = {
(this.resume, tb.resume) match {
case (Right(a),Right(b)) => Done((a,b))
case (Left(f),Left(g)) => More(() => f() zip g())
case (Right(a),Left(k)) => More(() => Done(a) zip k())
case (Left(k),Right(a)) => More(() => k() zip Done(a))
}
}
}
case class Done[+A](a: A) extends Trampoline[A]
case class More[+A](k: () => Trampoline[A]) extends Trampoline[A]
case class FlatMap[A,B](sub: Trampoline[A], k: A => Trampoline[B]) extends Trampoline[B]

我们可以用这个zip函数把几个Trampoline运算交叉组合起来实现并行运算:

 def hello: Trampoline[Unit] = for {
_ <- print("Hello ")
_ <- println("World!")
} yield () //> hello: => ch13.ex1.Trampoline[Unit] (hello zip hello zip hello).runT //> Hello Hello Hello World!
//| World!
//| World!
//| res8: ((Unit, Unit), Unit) = (((),()),())

用Trampoline可以解决StackOverflow这个大问题。现在我们可以放心地进行泛函编程了。

泛函编程(29)-泛函实用结构:Trampoline-不再怕StackOverflow的更多相关文章

  1. 泛函编程(5)-数据结构(Functional Data Structures)

    编程即是编制对数据进行运算的过程.特殊的运算必须用特定的数据结构来支持有效运算.如果没有数据结构的支持,我们就只能为每条数据申明一个内存地址了,然后使用这些地址来操作这些数据,也就是我们熟悉的申明变量 ...

  2. 实用的Scala泛函编程

    既然谈到实用编程,就应该不单止了解试试一个新的编程语言那么简单了,最好通过实际的开发项目实例来演示如何编程.心目中已经有了一些设想:想用Scala泛函编程搞一个开源的数据平台应用系统,也就是在云平台P ...

  3. 泛函编程(32)-泛函IO:IO Monad

    由于泛函编程非常重视函数组合(function composition),任何带有副作用(side effect)的函数都无法实现函数组合,所以必须把包含外界影响(effectful)副作用不纯代码( ...

  4. 泛函编程(30)-泛函IO:Free Monad-Monad生产线

    在上节我们介绍了Trampoline.它主要是为了解决堆栈溢出(StackOverflow)错误而设计的.Trampoline类型是一种数据结构,它的设计思路是以heap换stack:对应传统递归算法 ...

  5. 泛函编程(38)-泛函Stream IO:IO Process in action

    在前面的几节讨论里我们终于得出了一个概括又通用的IO Process类型Process[F[_],O].这个类型同时可以代表数据源(Source)和数据终端(Sink).在这节讨论里我们将针对Proc ...

  6. 泛函编程(34)-泛函变量:处理状态转变-ST Monad

    泛函编程的核心模式就是函数组合(compositionality).实现函数组合的必要条件之一就是参与组合的各方程序都必须是纯代码的(pure code).所谓纯代码就是程序中的所有表达式都必须是Re ...

  7. 泛函编程(28)-粗俗浅解:Functor, Applicative, Monad

    经过了一段时间的泛函编程讨论,始终没能实实在在的明确到底泛函编程有什么区别和特点:我是指在现实编程的情况下所谓的泛函编程到底如何特别.我们已经习惯了传统的行令式编程(imperative progra ...

  8. 泛函编程(27)-泛函编程模式-Monad Transformer

    经过了一段时间的学习,我们了解了一系列泛函数据类型.我们知道,在所有编程语言中,数据类型是支持软件编程的基础.同样,泛函数据类型Foldable,Monoid,Functor,Applicative, ...

  9. 泛函编程(25)-泛函数据类型-Monad-Applicative

    上两期我们讨论了Monad.我们说Monad是个最有概括性(抽象性)的泛函数据类型,它可以覆盖绝大多数数据类型.任何数据类型只要能实现flatMap+unit这组Monad最基本组件函数就可以变成Mo ...

随机推荐

  1. Atitit 项目的主体设计与结构文档 v3

    Atitit 项目的主体设计与结构文档 v3 1. 实现的目标2 1.1. cross device跨设备(pc 手机 平板)作为规划2 1.2. 企业级Java体系与开发语言2 1.3. 高扩展性, ...

  2. Atitit.java图片图像处理attilax总结

    Atitit.java图片图像处理attilax总结 BufferedImage extends java.awt.Image 获取图像像素点 image.getRGB(i, lineIndex); ...

  3. VS2012 MVC4 学习笔记-概览

    1. 访问请求过程 访问收到后路由(Router)根据路径由分配给对应的控制器(Control),然后由控制器返回页面视图(View) 路由设置一个默认的控制器,类似 主页的样子吧 <未完待续& ...

  4. gradle.properties

    gradle.properties # If this is set, then multiple APK files will be generated: One per native platfo ...

  5. java 中获取2个时间段中所包含的周数(股票的周数->从周六到周五)

    Calendar 类中是以周日为开始的第一天的,所以Calendar.DAY_OF_WEEK为1的时候是周日. 在股票中有日K 周K和月K的数据.  在此之中的周K是指交易日中一周的数据,周六到周五为 ...

  6. http的500,502,504错误

    500 500的错误通常是由于服务器上代码出错或者是抛出了异常 解决方法:查看一下对应的代码是不是有问题. 502 502即 Bad Gateway网关(这里的网关是指CGI,即通用网关接口,从名字就 ...

  7. c++ ofstream & ifstream文件流操作

    ofstream是从内存到硬盘,ifstream是从硬盘到内存,其实所谓的流缓冲就是内存空间; //ofstream & ifstream inherit from istream class ...

  8. H5游戏开发之Stick Hero

    自从上次发布一个小恐龙游戏以后,到现在10天了,前后又写了3个游戏,挑了一个感觉比较有挑战的游戏和大家分享一下. 效果演示 这是我模拟一个苹果游戏<stick hero>游戏写的一个小游戏 ...

  9. dom4j的读写xml文件,读写xml字符串

    百度了一些博客,大同小异,在选取jar包工具的时候大概看了下,大抵是jdom原始,dom4j优秀.于是做了些练习. 参考:http://www.cnblogs.com/mengdd/archive/2 ...

  10. js基础篇——原型与原型链的详细理解

    js中的对象分为两种:普通对象object和函数对象function. function fn1(){}; var fn2 = function(){}; var fn3 = new Function ...