泛函编程方式其中一个特点就是普遍地使用递归算法,而且有些地方还无法避免使用递归算法。比如说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 游戏的通常流程 attilax 总结 基于cocos2d api

    Atitit 游戏的通常流程 attilax 总结 基于cocos2d api 加载音效1 加载页面1 添加精灵1 设置随机位置2 移动2 垃圾gc2 点击evt2 爆炸效果3 定时生成精灵3 加载音 ...

  2. Atiti 重定向标准输出到字符串转接口adapter stream流体系 以及 重定向到字符串

    Atiti 重定向标准输出到字符串转接口adapter stream流体系 以及 重定向到字符串 原理::syso  向ByteArrayOutputStream这个流理想write字节..然后可以使 ...

  3. Atitit 表达式原理 语法分析 原理与实践 解析java的dsl  递归下降是现阶段主流的语法分析方法

    Atitit 表达式原理 语法分析 原理与实践 解析java的dsl  递归下降是现阶段主流的语法分析方法 于是我们可以把上面的语法改写成如下形式:1 合并前缀1 语法分析有自上而下和自下而上两种分析 ...

  4. paip.mysql 性能测试 报告 home right

    paip.mysql  性能测试 报告 home right 作者Attilax  艾龙,  EMAIL:1466519819@qq.com  来源:attilax的专栏 地址:http://blog ...

  5. salesforce 零基础学习(三十三)通过REST方式访问外部数据以及JAVA通过rest方式访问salesforce

    本篇参考Trail教程: https://developer.salesforce.com/trailhead/force_com_dev_intermediate/apex_integration_ ...

  6. 安装指定版本的cordova

    安装指定版本的cordova 刚接触cordova看到教程肯定是直接 npm install -g cordova 然后下载个集成的adt 以为万事大吉,开始hello world 玩玩没有想到最新的 ...

  7. 按要求编写Java应用程序。 (1)创建一个叫做People的类: 属性:姓名、年龄、性别、身高 行为:说话、计算加法、改名 编写能为所有属性赋值的构造方法; (2)创建主类: 创建一个对象:名叫“张三”,性别“男”,年龄18岁,身高1.80; 让该对象调用成员方法: 说出“你好!” 计算23+45的值 将名字改为“李四”

    package java1; public class People { public String name; public int age; public String sex; public S ...

  8. CSS层模型

    参考:慕课网 点此可进 如何让html元素在网页中精确定位,就像图像软件PhotoShop中的图层一样可以对每个图层能够精确定位操作.CSS定义了一组定位(positioning)属性来支持层布局模型 ...

  9. Warning: Null value is eliminated by an aggregate or other SET operation.

    Null 值会被聚合函数忽略,默认情况下,Sql Server会给出Warning: Warning: Null value is eliminated by an aggregate or othe ...

  10. hibernate(十)双向关联关系的CRUD

    本文链接:http://www.orlion.ml/28/ 一.保存 1. 假设一个group有多个user,一个user只属于一个group,当保存user对象到数据库中时可以 User u = n ...