JS函数式编程【译】3.1 Javascript的函数式库
Javascript的函数式库
据说所有的函数式程序员都会写自己的函数库,函数式Javascript程序员也不例外。 随着如今开源代码分享平台如GitHab、Bower和NPM的涌现,对这些函数库进行分享、变得及补充变得越来越容易。 现在已经有很多Javascript的函数式变成苦,从小巧的工具集到庞大的模块库都有。
每一个库都宣扬着自己的函数式编程风格。从一本正经的数学风格到灵活松散的非正式风格,每一个库都不尽相同, 然而他们他们有一个共同的特点:都是通过抽象的Javascript函数式能力来增进代码的重用行、可读性和健壮性。
然而直到写这本书的时候,还没有一个函数库成为事实上的标准。有人可能会说underscore.js是, 不过在后面的章节你会看到,可能避免使用underscore.js是明智的。
Underscore.js
Underscore在很多人眼里已经成为函数式Javascript库的标准。它成熟稳定, 其创建者Jeremy Ashkenas也是Backbone.js和Coffeescript的创建者。 Underscore实际上是对Ruby的Enumerable模块的重新实现, 这也解释了为什么Coffeescript也是受Ruby影响。
与jQuery相似,Underscore并不改变Javascript原生对象,而是用一个符号来定义自己的对象, 就是下划线(underscore)字符“_”。所以使用Underscore会是这个样子:
var x = _.map([1,2,3], Math.sqrt); // Underscore的map函数
console.log(x.toString());
我们已经见过Javascript数组原生的map()方法,它是这样用的:
var x = [1,2,3].map(Math.sqrt);
不同的是,用underscore时,数组对象和回调函数都是作为参数传入给underscore的map()方法(_.map)的, 而不是像数组原生的map()方法(Array.prototype.map)那样只需传递回调。
不过underscore除了map()还有很多内建函数,他们都是非常好用的函数, 比如find()、invoke()、pluck()、sortBy()、groupBy()等等。
var greetings = [{
origin: 'spanish',
value: 'hola'
}, {
origin: 'english',
value: 'hello'
}];
console.log(_.pluck(greetings, 'value'));
// 获取一个对象的属性.
// 返回: ['hola', 'hello']
console.log(_.find(greetings, function(s) {
return s.origin ==
'spanish';
}));
// 查找第一个回调函数返回真的元素
// 返回: {origin: 'spanish', value: 'hola'}
greetings = greetings.concat(_.object(['origin', 'value'], ['french', 'bonjour']));
console.log(greetings);
// _.object通过合并两个数组来建立一个对象
// 返回: [{origin: 'spanish', value: 'hola'},
//{origin: 'english', value: 'hello'},
//{origin: 'french', value: 'bonjour'}]
并且它还提供了链式调用方法
var g = _.chain(greetings)
.sortBy(function(x) {
return x.value.length
})
.pluck('origin')
.map(function(x) {
return x.charAt(0).toUpperCase() + x.slice(1)
})
.reduce(function(x, y) {
return x + ' ' + y
}, '')
.value(); // 应用这些函数
// 返回: 'Spanish English French'
console.log(g);
尽管underscore易于使用并且被社区改进,他还是在遭受批评。underscore强迫你编写过于冗长的代码, 并鼓励你使用错误的模式。underscore的结构并不完美,甚至不够函数式!
就在Brian Lonsdorf在YouTube上发表名为“嘿,underscore,你做错了”的讲话不久之后, underscore在发行的1.7.0版本中明确地阻止了我们扩展函数,比如map()、reduce()和filter()等等。
_.prototype.map = function(obj, iterate, [context]) {
if (Array.prototype.map && obj.map === Array.prototype.map)
return obj.map(iterate, context);
// ...
};
在范畴论的形式里,map是一个同态函子接口(详见第五章《范畴轮》)。我们应该能够把map定义为函子, 无论我们是否需要这样。所以说underscore不是很函数式。
并且由于Javascript不具有内建的不可变数据,函数式库应该十分小心地避免辅助函数改变传入的对象。 下面展示了一个针对这个问题的例子。代码中你会了一个新的选线列表,其中有一个选择项被设为了默认项。 实际上原来的列表被修改了。
function getSelectedOptions(id, value) {
options = document.querySelectorAll('#' + id + ' option');
var newOptions = _.map(options, function(opt) {
if (opt.text == value) {
opt.selected = true;
opt.text += ' (this is the default)';
} else {
opt.selected = false;
}
return opt;
});
return newOptions;
}
var optionsHelp = getSelectedOptions('timezones', 'Chicago');
我们应当插入一行“opt = opt.cloneNode()”,让回调函数对传入的列表中每一个元素建立一份拷贝。 underscore的map()函数为了得到性能而破坏了函数式的风水。原生的Array.prototype.map()不要求这些, 因为它会建立一个拷贝,然而它无法作用于nodelist集合。
Underscore也许并没有要追求函数式编程数学上的正确性,不过它也从来没有想要把Javascript扩展或者转变为一个纯函数语言。 它把自己定义为一个提供一大堆有用的函数式编程辅助函数的Javascript库。 也许它比那些伪造得看起来像函数式辅助函数的玩意儿要好些,不过它也不是一个严肃的函数式库。
那么有没有更好的库呢?一个建立在数学之上的库?
Fantasy Land
有时,真实世界比小说更离奇。
Fantasy Land是一个函数式基础库的集合,也是一份关于如何在Javascript中实现“代数结构”的规格。 更确切地说,Fantasy Land阐述了一般代数结构(简称代数)的互操作性:monads、monoids、setoids、 函子(functors)、链(chains)等等。这些名字可能听起来很吓人,不过他们只是一系列值、 一系列操作以及一些必须要遵守的规定。换句话说,他们只不过是对象。
下图展示了他们是如何工作的。每一个代数是一个单独的Fantasy Land规格, 它可能依赖于另一个需要实现的代数。
这里列出一些代数的规格:
- Setoids:
- 实现自反性(reflexivity)、对称性(reflexivity)和传递性(transitivity)
- 定义equals()方法
- Semigroups
- 实现结合律
- 定义concat()方法
- Monoid
- 实现右单位元(right identity)和左单位元(left identity)
- 定义empty()方法
- 函子(functor)
- 实现单位元和组合定律
- 定义map()方法
这个列表还有很多内容
我们不需要知道么一个代数的确切含义是什么,但是它的确很有帮助,尤其是当你编写符合这些这些规则的自己的库的时候。 这不只是抽象的玩意儿,它对一个叫做范畴论的高度抽象的东西的含义进行了概括。第五章将对范畴论进行全面的解释。
Fantasy Land不只告诉了我们如何实现函数式编程,它还提供了一个Javascript的函数式模块集。 然而里面有很多不完整的东西,并且文档也很不完善。不过Fantasy Land不是对这个开源规格的唯一实现。 还有一个实现的库叫做Bilby.js。
Bilby.js
Bilby是个啥?它可不是梦幻大陆(Fantasy Land)上的神话生物,而是地球上的介于老鼠和兔子之间的一种怪异而可爱的动物, 中文名是兔耳袋狸。尽管如此,bilby.js库遵从Fantasy Land的规格。
实际上,bilby是一个严肃的函数库。如它的文档中所描述:严肃,意味着它应用范畴论来实现高度抽象代码; 函数式,意味着它可以使程序引用透明。Wow,它还真够严肃的。文档的地址是 http://bilby.brianmckenna.org/。 它提供了以下内容:
- 特定多态(ad-hoc polymorphism)的不可变多元方法(multi-methods)
- 函数式数据结构
- 函数式语言的操作符重载
- 自动化规格测试(ScalaCheck, QuickCheck)
目前,Bilby.js这个已经很成熟的的库符合了Fantasy Land关于代数结构的规格。 要写完全函数式语言的代码,Bilby.js是一个优秀的资源。
我们来看个例子
// bilby的环境是多元方法的不可变结构
var shapes1 = bilby.environment()
// 定义方法
.method(
'area', // 方法的名称
function(a){return typeof(a) == 'rect'}, // 断言
function(a){return a.x * a.y} // 实现
)
// 定义属性,类似于定义一个方法,里面只有总返回true的断言
.property(
'name', // 名称
'shape'); // 函数
// 现在我们可以把它重载
var shapes2 = shapes1
.method(
'area', function(a){return typeof(a) == 'circle'},
function(a){return a.r * a.r * Math.PI} );
var shapes3 = shapes2
.method(
'area', function(a){return typeof(a) == 'triangle'},
function(a){return a.height * a.base / 2} );
// 现在我们可以像这样做点什么
var objs = [{type:'circle', r:5}, {type:'rect', x:2, y:3}];
var areas = objs.map(shapes3.area);
// 或者这样
var totalArea = objs.map(shapes3.area).reduce(add);
这就是范畴论和特定多态的实践。再啰嗦一次:范畴论将会在第五章全面讲解。
事实上,Bilby和Fantasy Land真的让Javascript之上的函数式编程成为了可能。 尽管可以看到计算机科学发生着日新月异的变化,但是这个世界仍未准备好迎接Bilby和Fantasy Land 所推动的顽固的函数式编程风格。
也许在函数式Javascript的恐慌地带的如此壮丽的一个库并不是我们想要的。 毕竟我们的出发点是寻找用于补充Javascript的函数式技术,而不是建立函数式编程信条。 现在让我们把注意力转向另一个新库:Lazy.js。
Lazy.js
Lazy是一个实用的库,它更大程度上是沿着Underscore的路线,不过它有惰性求值策略。正因如此, Lazy让即刻解释的语言本不可能完成的函数式计算变成了可能。它还会显著提升性能。
Lazy库还很年轻,但是在它背后有旺盛的社区热度和强劲的动力。
Lazy的主意是,我们能够迭代的所有东西都是一个序列。由于这个库用方法执行的先后来控制顺序, 很多很酷的事情就可以实现了:异步循环(并行编程)、无限序列、函数式响应式编程等等。
下面的例子展示了一下各种情形的代码:
// 获得一首歌歌词的前三行
var lyrics = "我徘徊在海之滨山之巅\n越此城镇越彼乡园\n ...
// 如果没有惰性,整个歌词会先根据换行来分割
console.log(lyrics.split('\n').slice(0, 3));
// 有了惰性,可以只文本分割出来前三行
// 歌词甚至可以无限的长!
console.log(Lazy(lyrics).split('\n').take(3));
// 前十个能被3整除的平方数
var oneTo1000 = Lazy.range(1, 1000).toArray();
var sequence = Lazy(oneTo1000)
.map(function(x) { return x * x; })
.filter(function(x) { return x % 3 === 0; })
.take(10)
.each(function(x) { console.log(x); });
// 对无限序列的异步循环
var asyncSequence = Lazy.generate(function(x) {
return x++
})
.async(100) // 每两个元素间隔0.100秒
.take(20) // 只计算前20项
.each(function(e) { // 开始对序列进行循环
console.log(new Date().getMilliseconds() + ": " + e);
});
更多例子参见第四章。
不过Lazy库的这个主意并不能保证它完全的正确性。它还有一个前辈,Bacon.js,他们的工作方式差不多。
Bacon.js
这是Bacon.js的logo:
这个大胡子牛仔是一个函数式响应式编程的库。函数式响应式编程的意思是: 函数式设计模式用于展示响应式的经常变化的值,比如鼠标在屏幕上的位置,或者公司股票的价格。 跟Lazy通过需要时才计算值而避免创建无限循环的序列的方法相同, Bacon可以避免实时计算随时在发生变化的值,直到需要这个值的最后一秒。
在Lazy中被称为序列的东西在Bacon中是事件流和属性,因为这样更适合于使用事件(onmouseover, onkeydown等等)以及响应属性(滚动位置、鼠标位置、toggle等等)。
Bacon.fromEventTarget(document.body, "click")
.onValue(function() { alert("Bacon!") });
Bacon比Lazy稍老一些,但是它的功能集差不多是Lazy的一半,社区热度差不多。
其他的一些库
Javascript函数式编程的库实在太多了,无法在本书中一一展示。我们再来简单看几个吧。
- Functional
- 这也许是Javascript的第一个函数式编程库,它包括了全面的高阶函数支持和string lambdas。
- wu.js
- 因其curryable()函数而饱受赞誉的wu.js库是一个很优秀的函数式编程库。它是第一个(据我所知) 实现了惰性求值的库,这影响了Bacon.js、Lazy.js等库。
- 是的,它的名字来源于臭名昭著的摇滚组合“Wu-Tang Clan”
"Wu-Tang Clan"实际就是武当派。是一个黑人说唱摇滚组合,有很多功夫文化的内容。
- sloth.js
- 和Lazy.js很像,但是更小
- stream.js
- 支持无限流,其它没什么
- 特别小
- Lo-Dash.js
- 就像名字所暗示的那样,它是受underscore.js的启发
- 高度优化
- Sugar
- Sugar是Javascript函数式编程技术的支持库,和Underscore相像,但是在实现上有一些关键的不同。
- underscore中的 _.pluck(myObjs, 'value')在Suger中仅仅是myObjs.map('value')。 意思是他修改了Javascript原生的对象,所以它在跟其它库混用的时候会有些风险,比如Prototype。
- from.js
- 一个新的函数式库,Javascript的LINQ(语言集成查询)引擎,支持.net所提供的大多数LINQ函数。
- 100%惰性求值,并支持lambda表达式
- 很年轻,但是文档很出色
- JSLINQ
- 另一个Javascript的LINQ引擎
- 比from.js更老也更成熟
- Boiler.js
- 另一个让Javascript扩展的函数式方法更加原生的工具库,包括:字符串、数字、对象、集合和数组
- Folktale
- 像Bilby.js那样,Folktable是一个对Fantasy Land实现的新库。并且像他的祖先那样, Folktable也是一个Javascript函数式编程库的集合。它还很年轻,但有光明的前景。
- jQuery
- 在这里看到jQuery很吃惊吗?尽管jQuery不是一个用于函数式编程的工具,但它自己是函数式的。 jQuery应该是根植于函数式编程的使用最广泛的库。
- jQuery对象实际是一个monad。jQuery使用了monad的规则来实现方法链式调用:
关于这个的详细解释可以在第七章《Javascript的函数式和面向对象编程》中找到$('#mydiv').fadeIn().css('left': 50).alert('hi!');
- 它的一些函数是高阶的
$('li').css('left': function(index){return index*50});
- jQuery1.8以上的deferred.then实现了函数式概念Promise
- jQuery是一个抽象层,主要是面向DOM。它不是一个框架或工具集, 只是一个使用抽象来提高代码复用和减少丑陋代码的方式。而函数式编程不全都是关于这些的吗?