单子是帮助你组合函数的工具。
像原始类型一样,单子是一种数据结构,它可以被当做装载让函子取东西的容器使用。 函子取出了数据,进行处理,然后放到一个新的单子中并将其返回。
我们将要关注三种单子:
除了用于数组的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 主目录第五章 范畴论
原标题:JS函数式编程【译】5.3 单子 (Monad)
关键词:JS