泛函编程方式其中一个特点就是普遍地使用递归算法,而且有些地方还无法避免使用递归算法。比如说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. Java构造函数

    构造函数的定义: 构造函数 ,是一种特殊的方法.主要用来在创建对象时初始化对象, 即为对象成员变量赋初始值,总与new运算符一起使用在创建对象的语句中.特别的一个类可以有多个构造函数 ,可根据其参数个 ...

  2. 转 jsp中 session的简单用法

    Session对象:是用来分别保存每一个用户信息的对象,以便于跟踪用户的操作状态.Session的信息保存在服务端,Session的ID保存在客户机的Cookie中.事实上,在许多服务器上,如果浏览器 ...

  3. python学习 数据类型之序列

    一.序列(本文使用python3.5)############################################################# 列表.元组 字符窜都是序列#特点:#1 ...

  4. ECMAScript 6 入门

    ECMAScript 6 入门 东西真多哇 http://es6.ruanyifeng.com/#docs/module 目录 前言 ECMAScript 6简介 let和const命令 变量的解构赋 ...

  5. Django ORM - 001 - 外键表查询主表信息

    开始用Django做web开发,我想大家都会遇到同样的问题,那就是如何高效快速的查询需要的数据,MVC都很简单,但是ORM折腾起来就有些费时间,我准备好好研究下Django ORM,所以会有一个系列的 ...

  6. iOS越狱开发(一)

    做越狱开发也有一些时间了,有很多东西想总结一下,希望给他人一些借鉴,也是自己对过去开发经历的一些总结.个人不推荐使用盗版,这里主要以技术介绍为主. 这个系列里面主要介绍怎样进行越狱开发,涉及到以下几个 ...

  7. 使用SSIS进行数据清洗

    简介     OLTP系统的后端关系数据库用于存储不同种类的数据,理论上来讲,数据库中每一列的值都有其所代表的特定含义,数据也应该在存入数据库之前进行规范化处理,比如说"age"列 ...

  8. 找到SQL Server数据库历史增长信息

        很多时候,在我们规划SQL Server数据库的空间,或向存储方面要空间时,都需要估算所需申请数据库空间的大小,估计未来最简单的办法就是看过去的趋势,这通常也是最合理的方式.     通常来讲 ...

  9. CSS裁剪clip

    × 目录 [1]定义 [2]RECT [3]应用 前面的话 CSS裁剪clip这个属性平时用的不多,但其实它并不是CSS3的新属性,很早就开始出现了.本文将介绍关于clip属性的相关知识 定义 一个绝 ...

  10. jsp网站服务器配置

    Jsp网站部署环境配置 首先解释一下,.jsp网站与.html网站有着很大的不同,html是一种静态网站开发脚本语言,jsp则是在html的基础上专门为开发动态网站设计的语言.所以jsp网站没办法直接 ...