高阶函数——unfold
作用
unfold 可以看做与普通聚合( fold 或 reduce )反向的一种操作:fold 可以根据数据源和条件,由包含多个元素的序列产生一个结果;而 unfold 方向相反,它根据条件由源产生了更多的结果。
它有两个优点:
- 消除了while循环语句
- 消除了不必要的变量声明
实现
concat版本
1.0
从 homoiconic 看到的原始版本后,第一个用JavaScript实现的想法是用 concat ,因为它没有副作用,支持链式调用:
// v1.0
Object.prototype.unfold = function(incrementor) {
var r = incrementor(this);
return (r != null) && [r].concat( r.unfold(incrementor) ) || [];
};
// 错误的版本,错误的结果
console.log( 10..unfold(function(n) { if(n!=-2) return n - 1 }) );
// => [9, 8, 7, 6, 5, 4, 3, 2, 1, 0, -1, -2]
1.0版 concat 函数连接结果的思路是正确的,它产生了形如 [5].concat( [4].concat( [3].concat([2].concat(...)) ) ) 式的递归调用。
缺点在于需要预先计算结果,因此声明了临时变量 r;同样的原因,因为需要预先计算,它把第一个值丢弃了,这是一个错误。
下面看看解决了上述问题的2.0版。
2.0 自定义运算符
// v2.0
var actor = function(next,act) {
return next == null ? [] : act(next);
}
Object.prototype.unfold = function(incrementor) {
return [this].concat(actor(incrementor(this),function(next) {
return next.unfold(incrementor)
}))
};
// 示例1:生成字母表
var val = function(c){ return c.valueOf() };
console.log( 'a'.unfold(function(c) { if(c!='f') return String.fromCharCode(c.charCodeAt(0)+1) }).map(val) );
// => ["a", "b", "c", "d", "e", "f"]
// 示例2:列举原型链
document.querySelector('p').unfold(function(obj) { if(obj!==null) return Object.getPrototypeOf(obj) })
// => [<p>…</p>, HTMLParagraphElement, HTMLElement, Element, Node,Object]
// 示例3:金字塔
var pyramid = function(char,layer) {
return char.unfold(function(c) { if(c.length<layer) return c+char }).join('\n')
}
console.log( pyramid('*',5) );
// =>
// *
// **
// ***
// ****
// *****
2.0版本首先创建了一个函数 actor 作为运算符,它用于识别参数的模式:当参数为 null 或 undefined时,分叉处理。
这个版本也产生了问题:
首先,由于接受了第一值,unfold 被当作原型上的方法。此时,它输出的结果都是包装类型,所以上面例子中不得不用map循环来获取可打印的值。
其次,在JavaScript扩展 Object 的原型是个危险的举动。
解决办法之一是增加参数,来自定义每个输出项:
options || (options = {})
transformed = options.map && options.map(this) || this
或者放弃原型方法,见3.0版
2.1 漂亮的无变量声明
// v2.1
Object.prototype.unfold = function(incrementor) {
return [this].concat(
(function(next,act) {
return next == null ? [] : act(next);
})(incrementor(this),function(next) {
return next.unfold(incrementor)
})
)
};
2.1版与2.0相同,只是隐藏了运算符,是当前最漂亮的实现。
3.0 在函数间传递状态
// 3.0 强化纯净版
var unfold = function() {
var actor = function(next,act) {
return next == null ? [] : act(next);
}
var _unfold = function(obj,incrementor,idx) {
return [obj].concat(
actor(incrementor(obj,idx++),function(next) {
return _unfold(next,incrementor,idx)
})
)
}
return function(obj,incrementor) {
return _unfold(obj,incrementor,0);
}
}();
// 示例 repeat
var repeat = function(char,count){
return unfold(char,function(c,i) { if(i<count-1) return c }).join('')
}
var range = function(from,to){
return unfold(from,function(n){ if(n<to-1) return n+1; })
}
console.log( repeat('*',10) );
// => **********
console.log( range(0,10) );
// => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
3.0版 不再扩展Object对象的原型,并且通过函数来传递状态(索引)和输入输出,进行连续计算。
由于传递了索引,它能供2.0版不能完成的功能,比如 repeat 函数。
这个版本传递基本类型时,返回的结果也全部是基本类型。
unshift版本
Array原型上的unshift方法没有连缀能力,要先改造一个:
// chained unshift
Array.prototype.unshift2 = function(){
this.unshift.apply(this,arguments);
return this;
}
actor版
Object.prototype.unfold2 = function(incrementor) {
return actor(incrementor(this),function(next) {
return next.unfold2(incrementor)
}).unshift2(this)
};
伪nilclass版
这是最接近 ruby 原始实现的方式
// fake NilClass
var empty = { unfold3 : function() { return [] } }
var empty_if_nil = function(v) { return v == null ? empty : v }
Object.prototype.unfold3 = function(incrementor) {
return empty_if_nil( incrementor(this) ).unfold3(incrementor).unshift2(this)
}
性能
# Benchmark alphabet : 'a'~'z' iter : 188 * 800
## ruby
user system total real
unfold unshift 0.982000 0.000000 0.982000 ( 0.972056)
unfold concat 1.202000 0.000000 1.202000 ( 1.210069)
unfold while 0.717000 0.000000 0.717000 ( 0.709041)
## js
### proto 指原型中方法 plain 指普通函数
#chrome 13
object.proto.unfold concat: 1037ms
object.proto.unfold unshift: 988ms
object.proto.unfold fakenil: 431ms
plain.unfold recursive: 111ms
plain.unfold while: 23ms
#firefox 5
object.proto.unfold concat: 1235ms
object.proto.unfold unshift: 1050ms
object.proto.unfold fakenil: 700ms
plain.unfold recursive: 1261ms
plain.unfold while: 133ms
#ie 8
object.proto.unfold concat 6362ms
object.proto.unfold unshift 7572ms
object.proto.unfold fakenil 4648ms
plain.unfold recursive 6549ms
plain.unfold while 1008ms
不用多说, chrome v8在动态语言里面的表现真有王者风范。
亮点有三:原型查找很消耗时间;v8下递归函数效率很高;'fake NilClass'的版本表现也很抢眼,可以看出来,减少匿名函数创建极大地提高了效率。
总结
- 1.0 动态语言的函数是不同的。高阶函数更加威力强大,它是处理一组相似逻辑的函数,然后在运行时再接收定义了这种逻辑的函数作为参数来调用。
- 2.0 除了上面把函数做参数外,函数还可以看做运算符——把一个功能放置到一个单独函数里面,就是设计了自己的运算符。编写程序的过程就是不断设计这样的函数,直到得到结果。
- 3.0 连续计算:通过函数可以把状态和值传入下一个函数进行连续计算,不需要中间声明变量。
- Duck Typing:'fake NilClass'版本体现了这一概念,获取下一个值时,只要保证返回对象能够响应 unfold 方法即可,不管它是什么。
- 时间消耗:匿名函数创建和原型查找都有所影响。
update@2011-09-22
测试了一个调用栈(call stack)消耗的情况:https://gist.github.com/1234338,结果如下:
递归版本检测调用栈耗尽时间就需要217912ms,仅最后一次表达式展开就需要74ms。
尾递归版本计算过程是迭代的,不需要展开表达式,效率和while循环接近。
javascript没有 TCO (tail call optimization),同样存在调用栈空间耗尽的问题:
- 当只传递一个状态变量时, chrome v8下调用栈空间接近两万,firefox下小一半;
- 传递状态变量越多,栈空间消耗越快;
- 闭包版消耗了些许空间,提升了些许速度。
意义重大的是,这表明直接用函数能代替while起的控制结构作用,解决问题的思考过程也是相异的。
wikipedia 上还指出两点重要信息,不经意使用的2.0版其实有个正式的名称: CPS(continuation passing style) ;迭代时栈耗尽的问题可以用名为 trampoline 的技巧解决。