上节我们提到Stream和List的主要分别是在于Stream的“延后计算“(lazy evaluation)特性。我们还讨论过在处理大规模排列数据集时,Stream可以一个一个把数据元素搬进内存并且可以逐个元素地进行处理操作。这让我不禁联想到我们常用的数据搜索读取方式了:大量的数据存放在数据库里,就好像无穷的数据源头。我们把数据读取方式(那些数据库读写API函数)嵌入Stream的操作函数内,把数据搜索条件传入Stream构造器(constructor)中形成一个对数据搜索操作的描述。这个产生的Stream只有在我们调用符合搜索条件的数据库记录时才会逐个从数据库中读取出来。这可是一个非常熟悉的场景,但我们常常会思考它的原理。

很多时我们都需要无穷数据流(infinite stream),以直接或一些算法方式有规则地重复产生数据。无穷数据流被定义为“反递归”(corecursive)的:递归的特性是从复杂到简单并最后终止,而无穷数据流的特性却是从简单到复杂并永远不会终结。

我们先从简单的无穷数据流开始:数字产生器:

 def ones: Stream[Int] = cons(1,ones)              //> ones: => ch5.genstream.Stream[Int]
ones.take(5).toList //> res0: List[Int] = List(1, 1, 1, 1, 1)

ones函数可以产生一个无穷的数据流。每个元素都是常数1。从这个简单的例子我们可以稍微领略反递归的味道:cons(1,ones),通过重复不断运算cons来产生无穷数据。

我们可以把一些算法嵌入无穷数据流产生过程:

 def constant[A](a: A): Stream[A] = cons(a, constant(a))
//> constant: [A](a: A)ch5.genstream.Stream[A]
constant(5).take(5).toList //> res1: List[Int] = List(5, 5, 5, 5, 5) def from(n: Int): Stream[Int] = cons(n, from(n+1))//> from: (n: Int)ch5.genstream.Stream[Int]
from(5).take(5).toList //> res2: List[Int] = List(5, 6, 7, 8, 9) def fibs: Stream[Int] = {
def go (prev: Int, cur: Int): Stream[Int] = {
cons(prev,go(cur,prev + cur))
}
go(0,1)
} //> fibs: => ch5.genstream.Stream[Int]
fibs.take(5).toList //> res3: List[Int] = List(0, 1, 1, 2, 3)

从以上这些例子可以看出:我们不断重复的在cons。而cons的参数则是算法的实现结果。

以下的unfold是一个最通用的Stream构建函数(stream builder),我们需要做些重点介绍:

 def unfold[A,S](z: S)(f: S => Option[(A, S)]): Stream[A] = {
f(z) match {
case None => empty
case Some((a,s)) => cons(a, unfold(s)(f))
}
} //> unfold: [A, S](z: S)(f: S => Option[(A, S)])ch5.genstream.Stream[A]

unfold的工作原理模仿了一种状态流转过程:z是一个起始状态,代表的是一个类型的值。然后用户(caller)再提供一个操作函数f。f的款式是:S => Option[(A,S)],意思是接受一个状态,然后把它转换成一对新的A值和新的状态S,再把它们放入一个Option。如果Option是None的话,这给了用户一个机会去终止运算,让unfold停止递归。从unfold的源代码可以看到f(z) match {} 的两种情况。需要注意的是函数f是针对z这个类型S来操作的,A类型是Stream[A]的元素类型。f的重点作用在于把S转换成新的S。我们用一些例子来说明:

 def constantByUnfold[A](a: A): Stream[A] = unfold(a)(_ => Some((a,a)))
//> constantByUnfold: [A](a: A)ch5.genstream.Stream[A]
constantByUnfold(2).take(5).toList //> res4: List[Int] = List(2, 2, 2, 2, 2)

constantByUnfold产生一个无穷的常数:a同时代表了元素类型和状态。_ => Some((a,a))意思是无论输入任何状态,元素值和状态都不转变,所以unfold会产生同一个数字。另外f的结果永远不可能是None,所以这是一个无穷数据流(infinite stream)。

再来一个例子:

 def fromByUnfold(s: Int): Stream[Int] = unfold(s)(s => Some(s,s+1))
//> fromByUnfold: (s: Int)ch5.genstream.Stream[Int]
fromByUnfold(5).take(5).toList //> res5: List[Int] = List(5, 6, 7, 8, 9)

fromByUnfold产生一个从s开始的无穷整数序列:s 同时代表了元素类型和状态。_ => Some((s,s+1))表示新A值是s,新状态是s+1,所以新s = s + 1。状态转变原理可以从改变 s+1 到 s+2 运算后产生的结果得以了解:

 def fromByUnfold_2(s: Int): Stream[Int] = unfold(s)(s => Some(s,s+2))
//> fromByUnfold_2: (s: Int)ch5.genstream.Stream[Int]
fromByUnfold_2(5).take(5).toList //> res6: List[Int] = List(5, 7, 9, 11, 13)

再试一个不同类型的状态例子:

 def fibsByUnfold: Stream[Int] = unfold((0,1)){ case (a1,a2) => Some((a1, (a2, a1+a2))) }
//> fibsByUnfold: => ch5.genstream.Stream[Int]
fibsByUnfold.take(10).toList //> res8: List[Int] = List(0, 1, 1, 2, 3, 5, 8, 13, 21, 34)

在以上的例子里:S类型为tuple,起始值(0,1),元素类型A是Int。函数f: Int => Option[(Int, Int)]。f函数返回新A=a1, 新状态S (a2, a1+a2)。由于状态是个tuple类型,(a1,a2)是个模式匹配操作,所以必须加上case。S=(0,1) >>> (A,S)=(0,(1,0+1)) >>>(1,(1,1+1))>>>(1,(2,2+1))>>>(2,(3,2+3))>>>(3,(5,3+5))>>>(5,(8,5+8))>>>(8,(13,8+13))从以上推断我们可以得出A>>>0,1,1,2,3,5,8,13,而状态S>>>(0,1),(1,1),(1,2),(2,3),(3,5),(5,8)...不断变化。

在来个更体现状态转变的例子:用unfold实现的map函数

     def mapByUnfoldInfinite[B](f: A => B): Stream[B] = {
unfold(uncons) {
case Some((h,t)) => Some((f(h),Some((t.head, t.tail))))
case _ => None
}
}
(fromByUnfold(1).mapByUnfoldInfinite {_ + 10}).take(5).toList
//> res9: List[Int] = List(11, 12, 13, 14, 15)

S类型即uncons类型>>>Option[(A, Stream[A])], uncons的新状态是 Some((t.head, t.tail))。因为我们采用了数据结构嵌入式的设计,所以必须用uncons来代表Stream,它的下一个状态就是Some((t.head, t.tail))。如果使用子类方式Cons(h,t),那么下一个状态就可以直接用t来表示,简洁多了。

再看看还有什么函数可以用unfold来实现吧:

     def takeByUnfold(n: Int): Stream[A] = {
unfold((uncons,n)) {
case (Some((h,t)),k) if (k > 0) => Some(h, (Some((t.head, t.tail)), k-1))
case _ => None
}
}
def takeWhileByUnfold(f: A => Boolean): Stream[A] = {
unfold(uncons) {
case Some((h,t)) if (f(h)) => Some(h, Some((t.head, t.tail)))
case _ => None
}
}
def filterByUnfold(f: A => Boolean): Stream[A] = {
unfold(uncons) {
case Some((h,t)) if (f(h)) => Some(h, Some((t.head, t.tail)))
case _ => None
}
}
def zipWithByUnfold[B,C](b: Stream[B])(f: (A,B) => C): Stream[C] = {
unfold((uncons,b.uncons)) {
case (Some((ha,ta)),Some((hb,tb))) => Some(f(ha,hb),(Some((ta.head,ta.tail)),Some((tb.head,tb.tail))))
case _ => None
}
}
def zip[B](b: Stream[B]): Stream[(A,B)] = zipWithByUnfold(b){( _ , _)}
(fromByUnfold(1).mapByUnfoldInfinite {_ + 10}).take(5).toList
//> res9: List[Int] = List(11, 12, 13, 14, 15)
(fromByUnfold(1).mapByUnfoldInfinite {_ + 10}).takeByUnfold(5).toList
//> res10: List[Int] = List(11, 12, 13, 14, 15)
(fromByUnfold(1).mapByUnfoldInfinite {_ + 10}).takeWhileByUnfold(_ < 15).toList
//> res11: List[Int] = List(11, 12, 13, 14)
(fromByUnfold(1).mapByUnfoldInfinite {_ + 10}).filterByUnfold(_ < 15).toList
//> res12: List[Int] = List(11, 12, 13, 14)
(fromByUnfold(5) zip fromByUnfold(1).mapByUnfoldInfinite {_ + 10}).take(5).toList
//> res13: List[(Int, Int)] = List((5,11), (6,12), (7,13), (8,14), (9,15))

风格基本上是一致的。

写完这些例子才有时间静下来想了一下:费了这么大劲搞这个无穷数据流到底能干什么呢?作为数据库搜索的数据源吗,这个可以用普通的Stream来实现。由于无穷数据流是根据一些算法有规则的不停顿产生数据,那么用来搭建测试数据源或者什么数学统计模式环境道是是可以的。想到不断产生数据,那么用来画些动态的东西也行吧,那在游戏软件中使用也有可能了。

泛函编程(13)-无穷数据流-Infinite Stream的更多相关文章

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

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

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

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

  3. 泛函编程(35)-泛函Stream IO:IO处理过程-IO Process

    IO处理可以说是计算机技术的核心.不是吗?使用计算机的目的就是希望它对输入数据进行运算后向我们输出计算结果.所谓Stream IO简单来说就是对一串按序相同类型的输入数据进行处理后输出计算结果.输入数 ...

  4. 泛函编程(36)-泛函Stream IO:IO数据源-IO Source & Sink

    上期我们讨论了IO处理过程:Process[I,O].我们说Process就像电视信号盒子一样有输入端和输出端两头.Process之间可以用一个Process的输出端与另一个Process的输入端连接 ...

  5. 泛函编程(11)-延后计算-lazy evaluation

    延后计算(lazy evaluation)是指将一个表达式的值计算向后拖延直到这个表达式真正被使用的时候.在讨论lazy-evaluation之前,先对泛函编程中比较特别的一个语言属性”计算时机“(s ...

  6. 泛函编程(23)-泛函数据类型-Monad

    简单来说:Monad就是泛函编程中最概括通用的数据模型(高阶数据类型).它不但涵盖了所有基础类型(primitive types)的泛函行为及操作,而且任何高阶类或者自定义类一旦具备Monad特性就可 ...

  7. 泛函编程(6)-数据结构-List基础

    List是一种最普通的泛函数据结构,比较直观,有良好的示范基础.List就像一个管子,里面可以装载一长条任何类型的东西.如需要对管子里的东西进行处理,则必须在管子内按直线顺序一个一个的来,这符合泛函编 ...

  8. 泛函编程(20)-泛函库设计-Further Into Parallelism

    上两节我们建了一个并行运算组件库,实现了一些基本的并行运算功能.到现在这个阶段,编写并行运算函数已经可以和数学代数解题相近了:我们了解了问题需求,然后从类型匹配入手逐步产生题解.下面我们再多做几个练习 ...

  9. 泛函编程(14)-try to map them all

    虽然明白泛函编程风格中最重要的就是对一个管子里的元素进行操作.这个管子就是这么一个东西:F[A],我们说F是一个针对元素A的高阶类型,其实F就是一个装载A类型元素的管子,A类型是相对低阶,或者说是基础 ...

随机推荐

  1. 代码片段:基于 JDK 8 time包的时间工具类 TimeUtil

    摘要: 原创出处:www.bysocket.com 泥瓦匠BYSocket 希望转载,保留摘要,谢谢! “知识的工作者必须成为自己时间的首席执行官.” 前言 这次泥瓦匠带来的是一个好玩的基于 JDK ...

  2. JSON跨域请求

    原理:首先客户机会注册一个callback,在发送跨域请求之前,会在url后附带注册的callback参数(如:callback1982342322),随后服务器拿到了callback参数,获取数据后 ...

  3. ctex moderncv版本更新--用latex写一个漂亮的简历

    我的电脑是win7系统32位,ctex版本是v2.9.2.164 full(http://www.ctex.org/CTeXDownload) 一直不太清楚moderncv里面类似\cventry这种 ...

  4. 3D全景!这么牛!!

    如果你用过网页版的百度地图,你大概3D全景图浏览是一种怎样的酷炫体验:在一个点可以360度环顾周围的建筑.景色,当然也可以四周移动,就像身临其境. 全景图共分为三种: ①球面全景图 利用一张全景图围成 ...

  5. 进阶学习js中的执行上下文

    在js中的执行上下文,菜鸟入门基础 这篇文章中我们简单的讲解了js中的上下文,今天我们就更进一步的讲解js中的执行上下文. 1.当遇到变量名和函数名相同的问题. var a = 10; functio ...

  6. ruby -- 进阶学习(十二)fragment cache

    基于rails4.0环境 Rails 页面缓存的方法很多,最近弱弱地尝试了fragment cache,用法还算简单~@_@|| 首先,查看config/environment/production. ...

  7. Web程序员开发App系列 - 申请苹果开发者账号

    Web程序员开发App系列 Web程序员开发App系列 - 认识HBuilder Web程序员开发App系列 - 申请苹果开发者账号 Web程序员开发App系列 - 调试Android和iOS手机代码 ...

  8. maven pox.xml 设置主入口配置节点

    <?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven ...

  9. gulp-uglify《JS压缩》----gulp系列(四)

    本节实现JS压缩,在实现压缩前,先配置JS任务,设置源目录和输出目录. 在系列(三)代码的基础上,再进行扩展. 1.找到gulp->config.js,对JS进行源目录(src->img) ...

  10. Android Logcat 封装类

    简单日志封装类: public final class CLog { public static final boolean DEBUG = true; private CLog() { } publ ...