高阶函数——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 的技巧解决。

posted @ 2011-09-14 14:49  ambar  阅读(1013)  评论(2编辑  收藏  举报