你的位置:首页 > Java教程

[Java教程]JS函数式编程【译】5.3 单子 (Monad)


单子是帮助你组合函数的工具。

像原始类型一样,单子是一种数据结构,它可以被当做装载让函子取东西的容器使用。 函子取出了数据,进行处理,然后放到一个新的单子中并将其返回。

我们将要关注三种单子:

  • Maybes
  • Promises
  • Lenses

除了用于数组的map和函数的compose以外,我们还有三种函子(maybe、promise和lens)。 这仅仅是另一些函子和单子。

Maybe

Maybe可以让我们优雅地使用有可能为空并且有默认值的数据。maybe是一个可以有值也可以没有值的变量,并且这对于调用者来说无所谓。

就他自己来说,这看起来不是什么大问题。所有人都知道空值检查可以通过一个if-else语句很容易地实现。

if (getUsername() == null) { username = 'Anonymous'} else { username = getUsername();}

但是用函数式编程,我们要打破过程的、一行接一样的做事方式,而应该用函数和数据的管道方式。 如果我们不得不从链的中间断开来检查值是否存在,我们就得创建临时变量并写更多的代码。 maybe仅仅是帮助我们保持逻辑跟随管道的工具。

要实现maybe,我们首先要创建一些构造器。

// Maybe单子构造器,目前是空的var Maybe = function(){};// None实例, 对一个没有值的对象的包装var None = function(){};None.prototype = Object.create(Maybe.prototype);None.prototype.toString = function(){return 'None';};// 现在可以写`none`函数了// 这让我们不用总去写`new None()`var none = function(){return new None()};// Just实例, 对一个有一个值的对象的包装var Just = function(x){return this.x = x;};Just.prototype = Object.create(Maybe.prototype);Just.prototype.toString = function(){return "Just "+this.x;};var just = function(x) {return new Just(x)};

最后,我们可以写maybe函数了。它返回一个新的函数,这个函数返回“没东西”或者maybe。它是个函子。

var maybe = function(m) { if (m instanceof None) {  return m; } else if (m instanceof Just) {  return just(m.x); } else {  throw new TypeError("Error: Just or None expected, " +   m.toString() + " given."); }}

我们还可以生成一个函子的生成器,就像数组的那样。

var maybeOf = function(f) { return function(m) {  if (m instanceof None) {   return m;  } else if (m instanceof Just) {   return just(f(m.x));  } else {   throw new TypeError("Error: Just or None expected, " +    m.toString() + " given.");  } }}

那么Maybe是单子,maybe是函子,maybeOf返回一个已经分配了态射的函子。

在我们继续往下进行之前,我们还得做点事情。我们需要给Maybe这个单子对象添加一个方法让它用起来更直观。

Maybe.prototype.orElse = function(y) { if (this instanceof Just) {  return this.x; } else {  return y; }}

在现在这样的形式里,maybe可以直接被使用。

maybe(just(123)).x; // Returns 123maybeOf(plusplus)(just(123)).x; // Returns 123maybeOf(plusplus)(none()).orElse('none'); // returns 'none'

所有东西都返回一个方法然后再被调用,这太复杂了,简直是在自找麻烦。我们可以通过我们的curry()函数让它看起来稍微明白一点:

maybePlusPlus = maybeOf.curry()(plusplus);maybePlusPlus(just(123)).x; // returns 123maybePlusPlus(none()).orElse('none'); // returns none

不过当直接调用none()和just()这些脏业务被抽象起来的时候,maybe真正的力量就变得明显了。我们用一个User对象的例子来试一下,这里username将使用maybe。

var User = function() { this.username = none(); // 初始设置为`none`};User.prototype.setUsername = function(name) { this.username = just(str(name)); // 这里用一个just};User.prototype.getUsernameMaybe = function() { var usernameMaybe = maybeOf.curry()(str); return usernameMaybe(this.username).orElse('anonymous');};var user = new User();user.getUsernameMaybe(); // Returns 'anonymous'user.setUsername('Laura');user.getUsernameMaybe(); // Returns 'Laura'

现在我们有了强大且安全的方法来定义默认值。记住这个User对象,因为在后面的章节里要用到它。

Promise

承诺(Promise)的本质就是它不受形式变化的影响- Frank Underwood, 纸牌屋

在函数式编程中,我们经常使用管道和数据流:也就是函数链,每个函数产生的数据类型被下一个函数消费。然而,有很多这些函数是异步的:文件、事件、Ajax等等。如果不用持续传递的风格和深层嵌套的回调,我们该如何修改这些函数的返回类型来说明结果?把它们封装到promise里。

promise像是回调的函数式等价物。很明显,回调不是那么函数式,如果不止一个函数要改变同样的数据,就会出现竞争条件和bug。promise解决了这个问题。

不用promise的代码是这样:

fs.readFile("file.json", function(err, val) { if (err) {  console.error("unable to read file"); } else {  try {   val = JSON.parse(val);   console.log(val.success);  } catch (e) {   console.error("invalid json in file");  } }});

用promise应该把代码编程这样:

fs.readFileAsync("file.json").then(JSON.parse) .then(function(val) {  console.log(val.success); }) .catch(SyntaxError, function(e) {  console.error("invalid json in file"); }) .catch(function(e) {  console.error("unable to read file") });

前面的代码来自于bluebird的README,bluebird是一个对Promises/A+的全面实现,并有非常好的性能。Promises/A+是一个JavaScript中promise的实现规范。这里只给出JavaScript社区当前的成果,把实现留给Promises/A+团队吧,因为它比maybe复杂得多。

不过这里给出部分实现:

// Promise单子var Promise = require('bluebird');// promise函子var promise = function(fn, receiver) { return function() {  var slice = Array.prototype.slice,   args = slice.call(arguments, 0, fn.length - 1),   promise = new Promise();  args.push(function() {   var results = slice.call(arguments),    error = results.shift();   if (error) promise.reject(error);   else promise.resolve.apply(promise, results);  });  fn.apply(receiver, args);  return promise; };};

现在我们可以利用函子promise()把需要传入回调的函数改为返回promise的函数。

var files = ['a.json', 'b.json', 'c.json'];readFileAsync = promise(fs.readFile);var data = files .map(function(f) {  readFileAsync(f).then(JSON.parse) }) .reduce(function(a, b) {  return $.extend({}, a, b) });

lens

程序员真正喜欢单子的另一个原因是它们使编写库非常简单。为了一探究竟,我们来给User对象扩展出更多的函数,这些函数用于设置和获取值,不过我们不用getter和setter,而使用lens。

lens是头等getter和setter,它让我们不仅可以设置和获取值,还可以直接运行函数。不过它不会改变数据,而是克隆一份数据,函数对克隆出来的数据进行修改后返回。它强制数据不可变,这对很多库所需要的安全性和一致性都很有好处。它有益于写出优雅的代码,无论什么样的应用,只要拷贝数组带来的增加量造成的性能冲击不是什么大问题。

在我们写lens()函数前,先来看看它是如何工作的:

var first = lens( function(a) { return arr(a)[0]; }, // get function(a, b) { return [b].concat(arr(a).slice(1)); } // set);first([1, 2, 3]); // 输出 1first.set([1, 2, 3], 5); // 输出 [5, 2, 3]function tenTimes(x) { return x * 10 }first.modify(tenTimes, [1, 2, 3]); // 输出 [10,2,3]

下面展示了lens()是函数如何工作的。它返回了一个定义了set、get和mod的函数。lens()函数本身是个函子。

var lens = fuction(get, set) { var f = function (a) {return get(a)}; f.get = function (a) {return get(a)}; f.set = set; f.mod = function (f, a) {return set(a, f(get(a)))}; return f;};

来试个例子,我们要扩展前面例子中的User对象。

// userName :: User -> strvar userName = lens( function(u) {  return u.getUsernameMaybe() }, // get function(u, v) { // set  u.setUsername(v);  return u.getUsernameMaybe(); });var bob = new User();bob.setUsername('Bob');userName.get(bob); // 返回'Bob'userName.set(bob, 'Bobby'); //return 'Bobby'userName.get(bob); // 返回'Bobby'userName.mod(strToUpper, bob); // 返回'BOBBY'strToUpper.compose(userName.set)(bob, 'robert'); // 返回'ROBERT'userName.get(bob); // 返回'robert'

jQuery是一个单子

如果你觉得所有这些关于范畴、函子和单子的抽象的玩意儿并没有在真实世界的应用,那你再想想。jQuery,这个最流行的JavaScript库,它提供了操作HTML的增强接口,实际上,它是一个单子化的库。

jQuery对象是一个单子,它的方法是函子。实际上它们是一种特殊类型的函子,叫做endofunctor。endofunctor是返回与输入相同范畴的函子,也就是F :: X -> X。每个jQuery方法都取一个jQuery对象并返回一个jQuery对象,这就使得方法可以链式调用。它们的类型签名是jFunc :: jquery-obj -> jquery-obj。

$('li').add('p.me-too').css('color', 'red').attr({id:'foo'});

这也用于jQuery的插件框架。如果一个插件以jQuery对象为输入,并返回一个jQuery对象为输出,那它就可以被添加到方法链中。

来看下jQuery是如何实现这个的。

单子是函子“伸手进去”取数据的容器。通过这种方式,数据就可以被保护起来并由库来控制。jQuery通过一些方法提供了访问里面数据(一系列封装起来的HTML元素)的方式。

jQuery自身是写成一个匿名函数调用的结果。

var jQuery = (function() { var j = function(selector, context) {  var jq - obj = new j.fn.init(selector, context);  return jq - obj; }; j.fn = j.prototype = {  init: function(selector, context) {   if (!selector) {    return this;   }  } }; j.fn.init.prototype = j.fn; return j;})();

在这个高度简化的jQuery版本里,它返回了一个定义了j对象的函数。j函数实际上是一个增强了的init构造器。

var $ = jQuery(); // 从这个函数得到了返回值并赋值给`$`var x = $('#select-me'); // 返回了jQuery对象

与函子从容器中取用值的方式相同,jQuery把HTML元素封装了起来,并提供了访问他们的方法,而没有直接去修改HTML元素。

尽管jQuery不经常宣扬它,但jQuery有自己的map方法用于从封装中取出HTML元素对象。就像fmap()方法一样,这些元素被取出,对它们做一些事情,然后放回到容器中。这就是许多jQuery命令后期的使用方式。

$('li').map(function(index, element) { // do something to the element return element});

另一个用于操作HTML元素的库Prototype就不是这样工作的。它通过一些辅助方式直接修改HTML元素。因此它在JavaScript社区里就没有那样的地位。

下一节 实现范畴
🏠 Functional Programming in Javascript 主目录第五章 范畴论