每天看一片代码系列(一):stream.js
简介
stream.js是一个小型的js库,用于处理stream相关的操作。这里的stream是指一种数据结构,它像数组一样,可以放置多个类型的数据,但是并不限制长度,甚至可以达到无限长。可以对该数据结构进行检索、修改、追加等种种操作。由于其长度不限这一特性,使得它与通常意义下的数据结构有明显的区别。
API
stream提供的API包含三种。
第一种是创建类。包括:
- new Stream(head, functionReturingTail) 第二个参数是一个放回除第一个元素之外剩下的元素的方法,所以它可能也返回一个Stream,即通过一个或多个现有的Stream来构造一个新的Stream。当然,这两个参数都不是必须的,如果都不填,那就是一个空的stresam。
- Stream.make() 相当于一个静态方法,比较简单,直接将数据填入里面就行了。
- Stream.range(from, to) 返回从from到to中间所有数字组成的Stream,如果不填,默认为自然数。
第二种是查询类,包括:
- head() 返回流中的第一个item
- item(index) 返回index位置上的item
- tail() 返回除了第一个位置上剩下的元素
- empty() 是否为空
第三种是遍历操作类型,包括:
- map() 类似于数组中的map,其实就是一个映射的过程
- walk() 同样是遍历,但是并不会对元素产生直接的影响
- filter() 过滤掉某些元素,这几个都接受参数为函数的情形
- take(n) 取前n个元素
- scale(n) 将每一个元素都乘以n
栗子
递归调用构造Stream
如前所述,构造函数的第二个参数可以是一个返回Stream的方法,那如果该方法中调用了自身呢?也就构成了无限长的具有相同数字的流。
function ones() { return new Stream( // the first element of the stream of ones is 1... 1, // and the rest of the elements of this stream are given by calling the function ones() (this same function!) ones ); }
Stream的相加
一个无限长的Stream和另一些无限长的Stream相加会出现什么情况?如下:
function ones() { return new Stream( 1, ones ); } function naturalNumbers() { return new Stream( // the natural numbers are the stream whose first element is 1... 1, function () { // and the rest are the natural numbers all incremented by one // which is obtained by adding the stream of natural numbers... // 1, 2, 3, 4, 5, ... // to the infinite stream of ones... // 1, 1, 1, 1, 1, ... // yielding... // 2, 3, 4, 5, 6, ... // which indeed are the REST of the natural numbers after one return ones().add( naturalNumbers() ); } ); }
虽然从名字上可以看出这是自然数,但初次看上去确实有些confusing。不妨列一下式子:
设 result = (1, n1, n2, n3, ....)
又有(n1, n2, n3, ...) = (1, 1, 1, ...) + (1, n1, n2, n3, ...)
那么 n1 = 2, n2 = 1 + n1 = 3, n3 = 1 + n2 = 4, ... 所以下一个数总是比上一个数多1, 那么就是自然数了!
代码解析
初看代码时我都惊了,虽然API有那么多,但整个代码仅仅有两百来行,还真是挺小。
首先定义了一个类Stream, 设置了一下它的head和获取剩余部分用到的函数。
function Stream( head, tailPromise ) { if ( typeof head != 'undefined' ) { this.headValue = head; } if ( typeof tailPromise == 'undefined' ) { tailPromise = function () { return new Stream(); }; } this.tailPromise = tailPromise; }
接下来就直接在prototype上开放API了,注意由于它无限长的特性,因此不能像遍历数组一样去一个一个索引,而是像链表一样,通过next指针去获取,这里就是通过tail()方法去获取。
var s = this; while ( n != 0 ) { --n; try { s = s.tail(); } catch ( e ) { throw new Error('Item index does not exist in stream.'); } }
当然,如果你对一个无限长的Stream做map,它当然不会真正的一个一个去无穷尽地map完,而是重新构造了一个Stream,定义了新的规则,即head = f(head), tail = tail().map(f),所以只有在取数据的时候才会执行这里面的函数。
map: function( f ) { if ( this.empty() ) { return this; } var self = this; return new Stream( f( this.head() ), function () { return self.tail().map( f ); } ); }
其他的API方法也是一样,我认为实现上最大的特点就是:递归调用!一个流无论再长,我都把它划分为两部分:一个元素为头部,剩余的为尾部。然后要做的就是对头部操作一下,对尾部操作一下即可。比如构造rarnge为low - high的流时只需制定头部为low,尾部为low+1到high的流就行了。
return new Stream( low, function () { return Stream.range( low + 1, high ); } );
Stream 就是一种 headElement + tailStream的结构,由于tailStream又可以进一步地划分,所以不可避免地通过递归来实现各种操作。对于应对无限长的数列来说是一个有效的方法,如果把它和数据结构中的链表关联起来,我们就明朗多了。