Scala中Stream的应用场景及事实上现原理
欢迎訪问我的独立博客:http://cuipengfei.me/blog/2014/10/23/scala-stream-application-scenario-and-how-its-implemented/
如果一个场景
须要在50个随机数中找到前两个能够被3整除的数字。
听起来非常easy,我们能够这样来写:
1 |
|
一个产生50个随机数的函数。
一个检查某数字能否被3整除的函数;
最后,对含有50个随机数的List做filter操作。找到当中全部可以被3整除的数字。取当中前两个。
把这段代码在Scala的console里面跑一下,结果是这种:
1 |
|
其终于结果固然是没有问题,找到了48和27这两个数字。可是很明显的能够看出,isDivisibleBy3被调用了50次。找到了远多于两个的能被3整除的数字。可是最后我们仅仅关心当中前两个结果。
这似乎有点浪费,做了非常多多余的运算。
对于这个样例来说。这还没什么,我们的List非常小。推断整除于否也不是什么耗时操作。
可是假设List非常大,filter时所做的运算非常复杂的话,那这样的做法就不可取了。
现有解法的优缺点
1 |
|
这行代码有一个长处:
用描写叙述性、声明性的语言描写叙述了我们要做的事是什么,而无需描写叙述怎么做。
我们仅仅需说先用filter过滤一下,然后拿前两个,整件事就完毕了。
可是它同一时候也有一个缺点:
做了多余的运算。浪费资源。并且这个缺点会随着数据量的增大以及计算复杂度的添加而更为凸显。
试着解决其缺点
解决多余运算的思路非常easy,不要过滤完整个List之后再取前两个。
而是在过滤的过程中假设发现已经找到两个了,那剩下的就忽略掉无论了。
顺着这个思路非常easy写出例如以下非常像Java的代码:
1 |
|
创建一个可变的List,開始遍历随机数。找到能被3整除的就把它塞进可变List里面去。找够了两个就返回。
运行的结果例如以下:
1 |
|
能够看到,运算量确实变少了,找够了两个就直接收工了。
可是这实在非常糟糕,显式使用了return同一时候还引入了可变量。
有什么东西像是一个foreach循环而又能够不引入可变量呢?fold
1 |
|
运行:
1 |
|
效果和上面一段代码类似,没有多余的运算。
可是因为须要early termination。所以还是摆脱不了return。
这两种解法在去除多余运算这个缺点的同一时候也把原来的长处给丢掉了,我们又退化回了描写叙述怎样做而不是做什么的程度了。
怎样保持代码的表意性而又不用做多余运算呢?
事实上类似的问题是有套路化的解决方式的:使用Stream。
1 |
|
这行代码运行的结果:
1 |
|
可见没有多余运算了,并且这行代码和最初代码极为相似。都是通过描写叙述先做filter再做take来完毕任务的。缺点没有了,长处也保留了下来。
这相同都是filter和take,代码跟代码的差距咋就这么大呢?
答案就是:由于Stream利用了惰性求值(lazy evaluation),或者也能够称之为延迟运行(deferred execution)。
接下来就看一下这两个晦涩的名词是怎样帮助Stream完毕工作的吧。
实现原理
在这里我借用一下Functional programming in Scala这本书里对Stream实现的代码。之所以不用Scala标准库的源代码是由于我们仅仅须要实现filter,take和toList这三个方法就能够展示Stream的原理。就不须要动用重型武器了。
先如果我们自己实现了一个MyStream,它的使用方法和Stream是类似的:
1 |
|
以这一行代码为引子。我们来開始解剖MyStream是怎样工作的。
类型签名
1 |
|
一个trait叫做MyStream,当中的内容我们临时忽略掉。
它有两个子类,一个Cons。一个Empty。Empty当然是代表空Stream了。
而Cons则是头尾结构的,头是Stream中的一个元素。尾是Stream中余下的元素。请注意头和尾这两个參数的类型并非A。头的类型是一个可以返回A的函数。尾的类型是一个可以返回MyStream[A]的函数。
初始化
有了以上的类型定义以及头尾结构,我们就能够把非常多个Cons加一个Empty(或者是无限多个Cons。没有Empty)连起来就构成一个Stream了。比方这样:
1 |
|
这样就能够构造一个含有1,2的Stream了。
只是,请注意,上面的说法并不严谨,实际上它是一个包括着两个分别会返回1和2的函数的Stream。
也就是说当上面的代码在构造Cons的时候,1和2还没有“出生”。它们被包在一个函数里,等着被释放出来。
假设说我们通常熟知的一些集合包括的是花朵的话,那Stream所包括的就是花苞,它本身不是花,可是有开出花来的能力。
Smart初始化
当然,假设直接暴露Cons的构造函数出去给别人用的话,那这API也未免太不友好了,所以Stream须要提供一个易用的初始化的方式:
1 |
|
这个没有太多好解释的,我们就是用apply和小写的cons这两个方法来把客户代码原本要写的一大堆匿名函数给代劳掉。
须要注意的一点是apply方法看似是递归的,好像是你调用它的时候假设给它n个元素的话,它会自己调用自己n-1次。其实它确实会调用自己n-1次,可是并非马上发生的。为什么呢?
由于小写的cons方法所接受的第二个參数不是eager evaluation的,这就会使得apply(elems.tail: _*)这个表达式不会马上被求值。
这就意味着,apply缺失会被调用n次,可是这n次并非一次接一次连续发生的。它仅仅会在我们对一个Cons的尾巴求值时才会发生一次。
假设说普通的集合中包括的是数据的话,那Stream中所包括的就是可以产生数据的算法。
怎样?是不是花朵花苞的感觉又回来了?
还记得我们開始剖析的时候那句代码是什么吗?
1 |
|
如今我们算是把MyStream(randomList: _*)这一小点说清了。
接下来看MyStream(randomList: _*).filter(isDivisibleBy3)是怎样work的。
filter
1 |
|
这种方法定义在基类里,又是一个看似递归的实现。
为什么说是看似呢?由于在
1 |
|
这行代码中我们又用到了小写的cons。它所接受的參数不会被马上求值。也就是说filter一旦找到一个合适的元素。它就不再继续跑了,剩下的计算被延迟了。
比較值得提一下的是:这里的h()是什么呢?h是构造Cons时的第一个參数,它是什么类型的?()=>A。它就是之前提到的可以生产数据的算法,就是那个可以开出花朵的花苞。在这里我们说h()。就是在调用这个函数来拿到它所生产的数据,就是让一个花苞开出花朵。
take
1 |
|
接下来就该说take是怎样work的了。在这里我们能够回想一下,MyStream(randomList: _*)返回一个类型为MyStream[Int]。当中包括非常多个能够返回Int的函数的容器。然后我们调用了这个容器的filter方法,filter又返回一个包括非常多个能够返回Int的函数的容器。
请注意。到这里为止,真正的计算还没有開始,真正的计算被包括到了一个又一个的函数(花苞)中。等待着被调用(绽放)。
那对filter的结果调用take又会如何呢?
1 |
|
看过了前面的apply和filter之后,take就显得顺眼了非常多。我们又见到了小写的cons,条件反射一般,我们就能够意识到。仅仅要看见cons,那就意味着作为它的參数的表达式不会被马上求值,那这就意味着计算被放到了函数里。稍后再运行。那稍后究竟是什么时候呢?
那就得看以下的toList了。
toList
1 |
|
又是一个递归实现,可是这次可不是看似递归了,这次是实打实的递归:仅仅要还没有遇到空节点。就继续向后遍历。这次没有使用cons。没有不论什么计算被延迟运行。我们通过不断地对h()求值,来把整个Stream中每个可以生产数据的函数都调用一遍以此来拿到我们终于想要的数据。
总结
要把以上的代码细节所有load进脑子跑一遍确实不太easy。我们人类的大脑栈空间太浅了。
所以我们试着从上面所罗列出的纷繁的事实中抽象出一些适合人脑理解的描写叙述性语句吧:
- List(1,2,3)会构造一个容器,容器中包括数据
- List(1,2,3).filter(n=>n>1)会构造出一个新的容器,当中包括2和3,这两块详细的数据
List(1,2,3).filter(n=>n>1).take(1)会把上一步中构造成的容器中的第一块数据取出,放入一个新容器
MyStream(1,2,3)也会构造一个容器,可是这个容器中不包括数据,它包括可以生产数据的算法
- MyStream(1,2,3).filter(n=>n>1)也会构造出一个新的容器,这个容器中所包括的仍然是算法,是基于上一步构造出的能生产1,2。3的算法之上的推断数字是否大于1的算法
- MyStream(1,2,3).filter(n=>n>1).take(1)会把上一步中构造成的算法容器中的第一个算法取出,放入一个新容器
- MyStream(1,2,3).filter(n=>n>1).take(1).toList最终把上面全部步骤构造出的算法运行了,从而得到了最终想要的结果
上面对List和Stream的应用的差别在哪儿呢?
就在于List是先把数据构造出来,然后在一堆数据中挑选我们心仪的数据。
而Stream是先把算法构造出来。挑选心仪的算法,最后仅仅运行一大堆算法中我们须要的那一部分。
这样,自然就不会运行多余的运算了。
Scala中Stream的应用场景及事实上现原理的更多相关文章
- Scala中Stream的应用场景及其实现原理
欢迎关注我的新博客地址:http://cuipengfei.me/blog/2014/10/23/scala-stream-application-scenario-and-how-its-imple ...
- scala中Stream理解
// Stream:Stream is lazy List; // Stream惰性求值指它只确定第一个值,后面的值用到再求值,这样可以防止数据过大全部加载导致内存溢出 // 将Range转化成Str ...
- scala中Either的一种使用场景
用scala有一年多了,对于scala中的Option和Try使用的较为频繁,对其应用场景相对熟悉一些.而对于Either,仔细回想一下却发现几乎(完全)没有使用过,其实并不是没有遇到过Either的 ...
- scala中“=>”的4种使用场景
一直以来都对scala中"=>"的使用比较迷茫,也不知道他表示什么意思.今天就它的使用场景列举如下,希望可以共同探讨. 表示函数的返回类型(Function Type) sc ...
- 深入理解Java8中Stream的实现原理
Stream Pipelines 前面我们已经学会如何使用Stream API,用起来真的很爽,但简洁的方法下面似乎隐藏着无尽的秘密,如此强大的API是如何实现的呢?比如Pipeline是怎么执行的, ...
- Scala中apply的用法
Scala中的 apply 方法有着不同的含义, 对于函数来说该方法意味着调用function本身, 以下说明摘自Programming in Scala, 3rd Edition Every fun ...
- Scala 深入浅出实战经典 第51讲:Scala中链式调用风格的实现代码实战及其在Spark中应用
王家林亲授<DT大数据梦工厂>大数据实战视频 Scala 深入浅出实战经典(1-64讲)完整视频.PPT.代码下载:百度云盘:http://pan.baidu.com/s/1c0noOt6 ...
- Scala中的构造器和高阶函数
构造器 在定义类时可以定义主构造器.主构造器可以同时声明字段. /** * 主构造器 * @author Administrator */ //在scala中,类和方法交织在一起 class Test ...
- Scala 中方法扩展实践
前言 这个名字不知道取得是否合适,简单来说要干的事情就是给某个类型添加一些扩展方法,此场景在各种语言中都会用到,比如 C# 语言,如果我们使用一个别人写好的类库,而又想给某个类库添加一些自己封装的方法 ...
随机推荐
- Qt+SQLite数据加密的一种思路(内存数据库)
了解Qt是比较早的,可是一直没有耐心去做一个练习.近期花了差不多两周时间做了次Qt开发练习,基本完成了Qt的入门,逃脱微软平台又迈出了几小步.Qt虽然是C++的,但开发应用是比较方便的,我觉得它在界面 ...
- 【JUnit4.10源码分析】5 Statement
假设要评选JUnit中最最重要的类型.或者说核心,无疑是org.junit.runners.model.Statement.Runner等类型看起来热闹而已. package org.junit.ru ...
- 二分图最大匹配 hdoj 1045
题目:hdoj1045 题意:给出一个图.当中有 . 和 X 两种,. 为通路,X表示墙,在当中放炸弹,然后炸弹不能穿过墙.问你最多在图中能够放多少个炸弹? 分析:这道题目是在上海邀请赛的题目的数据简 ...
- leetcode先刷_Search in Rotated Sorted Array II
上一页下一页,找到相同的旋转阵列的问题.假设数组元素一再怎么办呢?会发生什么? 我给大家举一个极端的例子.如果是这样的阵列中的元件.1,1,2,1,1,1,1,我们想看看这个数组2,刚开始A[midd ...
- java打印日历
打个日历,写了半天,感情水平真菜, 不过主要是不会数组,明天一定要把数组看了 package demo; import java.util.Scanner; public class Demo { / ...
- 如何设置gen_server在退出时执行相关操作
如果gen_server在监控树中不需要stop函数,gen_server会由其supervisor根据shutdown策略自动终止掉.如果要在进程终止之前执行清理,shutdown策略必须设定一个t ...
- 高质量程序设计指南C/C++语言——C++/C程序设计入门
1.在C++/C中,全局变量(extern或static)存放在程序的静态数据区中,在程序进入main()之前创建,在main()结束之后销毁,因此在我们的代码中根本没有机会初始化它们,于是语言及其实 ...
- 如何去掉IE控件的垂直滚动条(使用QAxWidget加载IE控件)
如果使用MFC的CHtmlView或Qt的QAxWidget加载IE控件,载入html文件后都会自动带一个垂直滚动条,我们不想要这个滚动条,改怎么办呢?搜索了一下“隐藏IE控件滚动条”,发现在 htt ...
- docker学习笔记7:发布镜像到docker hub上
镜像创建好后,很重要的一个操作就是共享和发布.可以将自己创建的镜像发布到docker hub上,也可以发布到自己的私有docker hub上. 要想发布镜像到dokcer hub上,首先要在dokce ...
- 基于visual Studio2013解决算法导论之051区间树
题目 区间树 解决代码及点评 #include <stdio.h> #include <string.h> #include <iostream> #def ...