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);
_.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);
  // ...
};
你可以在www.youtube.com/watch?v=m3svKOdZij查看Brian Lonsdorf讲话的视频。

在范畴论的形式里,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集合。

我插几句。这一段真是翻译得不太对劲。首先,我从哪翻译出来个“风水”?呃~原文就是“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

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的规则来实现方法链式调用:
      $('#mydiv').fadeIn().css('left': 50).alert('hi!');
      关于这个的详细解释可以在第七章《Javascript的函数式和面向对象编程》中找到
    • 它的一些函数是高阶的
      $('li').css('left': function(index){return index*50});
    • jQuery1.8以上的deferred.then实现了函数式概念Promise
    • jQuery是一个抽象层,主要是面向DOM。它不是一个框架或工具集, 只是一个使用抽象来提高代码复用和减少丑陋代码的方式。而函数式编程不全都是关于这些的吗?
posted @ 2015-08-10 10:23  tolg  阅读(3030)  评论(2编辑  收藏  举报