JS函数式编程【译】3.1Javascript的函数式库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.String());
我们已经见过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) {
igin ==
'spanish';
}));
// 查第⼀个回调函数返回真的元素
// 返回: {origin: 'spanish', value: 'hola'}
greetings = at(_.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);
_.chain()⽅法的返回值被包在了⼀个拥有Underscore全部函数的对象⾥。_.value⽅法⽤于把被包裹的对象提取出来。 包裹的对象对于把Underscore混合到⾯向对象编程中⾮常有⽤。
尽管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);
// ...
};
你可以在 查看Brian Lonsdorf讲话的视频。
在范畴论的形式⾥,map是⼀个同态函⼦接⼝(详见第五章《范畴轮》)。我们应该能够把map定义为函⼦, ⽆论我们是否需要这样。所以说underscore不是很函数式。
并且由于Javascript不具有内建的不可变数据,函数式库应该⼗分⼩⼼地避免辅助函数改变传⼊的对象。 下⾯展⽰了⼀个针对这个问题的例⼦。代码中你会了⼀个新的选线列表,其中有⼀个选择项被设为了默认项。 实际上原来的列表被修改了。
function getSelectedOptions(id, value) {
options = document.querySelectorAll('#' + id + ' option');
var newOptions = _.map(options, function(opt) {
if ( == value) {
opt.selected = true;
< += ' (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集合。
我插⼏句。这⼀段真是翻译得不太对劲。⾸先,我从哪翻译出来个“风⽔”?呃~原⽂就是“feng shui”啊。 ⼈家好不容易⽤了咱们的词我怎能不照字⾯翻译过来?我估计作者说的风⽔是指函数式那⼀整套系统的⼀个感觉吧。 反正是个说不太清的有机整体。原⽂是这样
的:“Underscore's map() function cheats to boost performance, but it is at the cost of functional feng shui”,我并没有严格按字⾯翻译是觉得这样说好理解⼀点, 不过可能不太准确。另外,作者说数组原⽣的map会建⽴拷贝,意思是没有副作⽤,但是你可以建个对象数组试试, 在map的回调中改变传⼊对象某属性的值,原数组也发⽣了变化。作者闹错了吧……
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
value函数什么意思Bilby是个啥?它可不是梦幻⼤陆(Fantasy Land)上的神话⽣物,⽽是地球上的介于⽼⿏和兔⼦之间的⼀种怪异⽽可爱的动物, 中⽂名是兔⽿袋狸。尽管如此,bilby.js库遵从Fantasy Land的规格。
特定多态(ad-hoc polymorphism)的不可变多元⽅法(multi-methods)
函数式数据结构
函数式语⾔的操作符重载
⾃动化规格测试(ScalaCheck, QuickCheck)
⽬前,Bilby.js这个已经很成熟的的库符合了Fantasy Land关于代数结构的规格。 要写完全函数式语⾔的代码,Bilby.js是⼀个优秀的资源。
我们来看个例⼦
// bilby的环境是多元⽅法的不可变结构
var shapes1 = vironment()
// 定义⽅法
.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 = ate(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: