每天看一片代码系列(一):stream.js

简介

stream.js是一个小型的js库,用于处理stream相关的操作。这里的stream是指一种数据结构,它像数组一样,可以放置多个类型的数据,但是并不限制长度,甚至可以达到无限长。可以对该数据结构进行检索、修改、追加等种种操作。由于其长度不限这一特性,使得它与通常意义下的数据结构有明显的区别。

API

stream提供的API包含三种。

第一种是创建类。包括:

  1. new Stream(head, functionReturingTail) 第二个参数是一个放回除第一个元素之外剩下的元素的方法,所以它可能也返回一个Stream,即通过一个或多个现有的Stream来构造一个新的Stream。当然,这两个参数都不是必须的,如果都不填,那就是一个空的stresam。
  2. Stream.make() 相当于一个静态方法,比较简单,直接将数据填入里面就行了。
  3. Stream.range(from, to) 返回从from到to中间所有数字组成的Stream,如果不填,默认为自然数。

第二种是查询类,包括:

  1. head() 返回流中的第一个item
  2. item(index) 返回index位置上的item
  3. tail() 返回除了第一个位置上剩下的元素
  4. empty() 是否为空

第三种是遍历操作类型,包括:

  1. map() 类似于数组中的map,其实就是一个映射的过程
  2. walk() 同样是遍历,但是并不会对元素产生直接的影响
  3. filter() 过滤掉某些元素,这几个都接受参数为函数的情形
  4. take(n)  取前n个元素
  5. 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又可以进一步地划分,所以不可避免地通过递归来实现各种操作。对于应对无限长的数列来说是一个有效的方法,如果把它和数据结构中的链表关联起来,我们就明朗多了。

posted @ 2015-04-18 14:41  cubika  阅读(464)  评论(0编辑  收藏  举报