协调与激励
假设我们要创建一个旅游网站,既需要获取游客目的地的列表,还需要获取地方事件的列表。这两个请求就是相互独立的 api 调用。
// Http.get :: String -> Task Error HTML
var renderPage = curry(function(destinations, events) { /* render page */ });
Task.of(renderPage).ap(Http.get('/destinations')).ap(Http.get('/events'))
// Task("<div>some page with dest and events</div>")
两个请求将会同时立即执行,当两者的响应都返回之后,renderPage
就会被调用。这与 monad 版本的那种必须等待前一个任务完成才能继续执行后面的操作完全不同。本来我们就无需根据目的地来获取事件,因此也就不需要依赖顺序执行。
再次强调,因为我们是使用局部调用的函数来达成上述结果的,所以必须要保证 renderpage
是 curry 函数,否则它就不会一直等到两个 Task
都完成。而且如果你碰巧自己做过类似的事,那你一定会感激 applicative functor
这个异常简洁的接口的。这就是那种能够让我们离“奇点”(singularity)更近一步的优美代码。
再来看另外一个例子。
// 帮助函数:
// ==============
// $ :: String -> IO DOM
var $ = function(selector) {
return new IO(function(){ return document.querySelector(selector) });
}
// getVal :: String -> IO String
var getVal = compose(map(_.prop('value')), $);
// Example:
// ===============
// signIn :: String -> String -> Bool -> User
var signIn = curry(function(username, password, remember_me){ /* signing in */ })
IO.of(signIn).ap(getVal('#email')).ap(getVal('#password')).ap(IO.of(false));
// IO({id: 3, email: "gg@allin.com"})
signIn
是一个接收 3 个参数的 curry 函数,因此我们需要调用 ap
3 次。在每一次的 ap
调用中,signIn
就收到一个参数然后运行,直到所有的参数都传进来,它也就执行完毕了。我们可以继续扩展这种模式,处理任意多的参数。另外,左边两个参数在使用 getVal
调用后自然而然地成为了一个 IO
,但是最右边的那个却需要手动 lift
,然后变成一个 IO
,这是因为 ap
需要调用者及其参数都属于同一类型。
lift
(译者注:此处原标题是“Bro, do you even lift?”,是一流行语,发源于健身圈,指质疑别人的健身方式和效果并显示优越感,后扩散至其他领域。再注:作者书中用了不少此类俚语或俗语,有时并非在使用俚语的本意,就像这句,完全就是为了好玩。另,关于 lift 的概念可参看第 8 章。)
我们来试试以一种 pointfree 的方式调用 applicative functor。因为 map
等价于 of/ap
,那么我们就可以定义无数个能够 ap
通用函数。
var liftA2 = curry(function(f, functor1, functor2) {
return functor1.map(f).ap(functor2);
});
var liftA3 = curry(function(f, functor1, functor2, functor3) {
return functor1.map(f).ap(functor2).ap(functor3);
});
//liftA4, etc
liftA2
是个奇怪的名字,听起来像是破败工厂里挑剔的货运电梯,或者伪豪华汽车公司的个性车牌。不过你要是真正理解了,那么它的含义也就不证自明了:让那些小代码块发生 lift,成为 applicative functor 中的一员。
刚开始我也觉得这种 2-3-4 的写法没什么意义,看起来又丑又没有必要,毕竟我们可以在 JavaScript 中检查函数的参数数量然后再动态地构造这样的函数。不过,局部调用(partially apply)liftA(N)
本身,有时也能发挥它的用处,这样的话,参数数量就固定了。
来看看实际用例:
// checkEmail :: User -> Either String Email
// checkName :: User -> Either String String
// createUser :: Email -> String -> IO User
var createUser = curry(function(email, name) { /* creating... */ });
Either.of(createUser).ap(checkEmail(user)).ap(checkName(user));
// Left("invalid email")
liftA2(createUser, checkEmail(user), checkName(user));
// Left("invalid email")
createUser
接收两个参数,因此我们使用的是 liftA2
。上述两个语句是等价的,但是使用了 liftA2
的版本没有提到 Either
,这就使得它更加通用灵活,因为不必与特定的数据类型耦合在一起。
我们试试以这种方式重写前一个例子:
liftA2(add, Maybe.of(2), Maybe.of(3));
// Maybe(5)
liftA2(renderPage, Http.get('/destinations'), Http.get('/events'))
// Task("<div>some page with dest and events</div>")
liftA3(signIn, getVal('#email'), getVal('#password'), IO.of(false));
// IO({id: 3, email: "gg@allin.com"})
操作符
在 haskell、scala、PureScript 以及 swift 等语言中,开发者可以创建自定义的中缀操作符(infix operators),所以你能看到到这样的语法:
-- haskell
add <$> Right 2 <*> Right 3
// JavaScript
map(add, Right(2)).ap(Right(3))
<$>
就是 map
(亦即 fmap
),<*>
不过就是 ap
。这样的语法使得开发者可以以一种更自然的风格来书写函数式应用,而且也能减少一些括号。
免费开瓶器
我们尚未对衍生函数(derived function)着墨过多。不过看到本书介绍的所有这些接口都互相依赖并遵守一些定律,那么我们就可以根据一些强接口来定义一些弱接口了。
比如,我们知道一个 applicative 首先是一个 functor,所以如果已经有一个 applicative 实例的话,毫无疑问可以依此定义一个 functor。
这种完美的计算上的大和谐(computational harmony)之所以存在,是因为我们在跟一个数学“框架”打交道。哪怕是莫扎特在小时候就下载了 ableton(译者注:一款专业的音乐制作软件),他的钢琴也不可能弹得更好。
前面提到过,of/ap
等价于 map
,那么我们就可以利用这点来定义 map
:
// 从 of/ap 衍生出的 map
X.prototype.map = function(f) {
return this.constructor.of(f).ap(this);
}
monad 可以说是处在食物链的顶端,因此如果已经有了一个 chain
函数,那么就可以免费得到 functor 和 applicative:
// 从 chain 衍生出的 map
X.prototype.map = function(f) {
var m = this;
return m.chain(function(a) {
return m.constructor.of(f(a));
});
}
// 从 chain/map 衍生出的 ap
X.prototype.ap = function(other) {
return this.chain(function(f) {
return other.map(f);
});
};
定义一个 monad,就既能得到 applicative 也能得到 functor。这一点非常强大,相当于这些“开瓶器”全都是免费的!我们甚至可以审查一个数据类型,然后自动化这个过程。
应该要指出来的一点是,ap
的魅力有一部分就来自于并行的能力,所以通过 chain
来定义它就失去了这种优化。即便如此,开发者在设计出最佳实现的过程中就能有一个立即可用的接口,也是很好的。
为啥不直接使用 monad?因为最好用合适的力量来解决合适的问题,一分不多,一分不少。这样就能通过排除可能的功能性来做到最小化认知负荷。因为这个原因,相比 monad,我们更倾向于使用 applicative。
向下的嵌套结构使得 monad 拥有串行计算、变量赋值和暂缓后续执行等独特的能力。不过见识到 applicative 的实际用例之后,你就不必再考虑上面这些问题了。
下面,来看看理论知识。
定律
就像我们探索过的其他数学结构一样,我们在日常编码中也依赖 applicative functor 一些有用的特性。首先,你应该知道 applicative functor 是“组合关闭”(closed under composition)的,意味着 ap
永远不会改变容器类型(另一个胜过 monad 的原因)。这并不是说我们无法拥有多种不同的作用——我们还是可以把不同的类型压栈的,只不过我们知道它们将会在整个应用的过程中保持不变。
下面的例子可以说明这一点:
var tOfM = compose(Task.of, Maybe.of);
liftA2(_.concat, tOfM('Rainy Days and Mondays'), tOfM(' always get me down'));
// Task(Maybe(Rainy Days and Mondays always get me down))
你看,不必担心不同的类型会混合在一起。
该去看看我们最喜欢的范畴学定律了:同一律(identity)。
同一律(identity)
// 同一律
A.of(id).ap(v) == v
是的,对一个 functor 应用 id
函数不会改变 v
里的值。比如:
var v = Identity.of("Pillow Pets");
Identity.of(id).ap(v) == v
Identity.of(id)
的“无用性”让我不禁莞尔。这里有意思的一点是,就像我们之前证明了的,of/ap
等价于 map
,因此这个同一律遵循的是 functor 的同一律:map(id) == id
。
使用这些定律的优美之处在于,就像一个富有激情的幼儿园健身教练让所有的小朋友都能愉快地一块玩耍一样,它们能够强迫所有的接口都能完美结合。
同态(homomorphism)
// 同态
A.of(f).ap(A.of(x)) == A.of(f(x))
同态就是一个能够保持结构的映射(structure preserving map)。实际上,functor 就是一个在不同范畴间的同态,因为 functor 在经过映射之后保持了原始范畴的结构。
事实上,我们不过是把普通的函数和值放进了一个容器,然后在里面进行各种计算。所以,不管是把所有的计算都放在容器里(等式左边),还是先在外面进行计算然后再放到容器里(等式右边),其结果都是一样的。
一个简单例子:
Either.of(_.toUpper).ap(Either.of("oreos")) == Either.of(_.toUpper("oreos"))
互换(interchange)
互换(interchange)表明的是选择让函数在 ap
的左边还是右边发生 lift 是无关紧要的。
// 互换
v.ap(A.of(x)) == A.of(function(f) { return f(x) }).ap(v)
这里有个例子:
var v = Task.of(_.reverse);
var x = 'Sparklehorse';
v.ap(Task.of(x)) == Task.of(function(f) { return f(x) }).ap(v)
组合(composition)
最后是组合。组合不过是在检查标准的函数组合是否适用于容器内部的函数调用。
// 组合
A.of(compose).ap(u).ap(v).ap(w) == u.ap(v.ap(w));
var u = IO.of(_.toUpper);
var v = IO.of(_.concat("& beyond"));
var w = IO.of("blood bath ");
IO.of(_.compose).ap(u).ap(v).ap(w) == u.ap(v.ap(w))
总结
处理多个 functor 作为参数的情况,是 applicative functor 一个非常好的应用场景。借助 applicative functor,我们能够在 functor 的世界里调用函数。尽管已经可以通过 monad 达到这个目的,但在不需要 monad 的特定功能的时候,我们还是更倾向于使用 applicative functor。
至此我们已经基本介绍完容器的 api 了,我们学会了如何对函数调用 map
、chain
和 ap
。下一章,我们将学习如何更好地处理多个 functor,以及如何以一种原则性的方式拆解它们。
Chapter 11: Traversable/Foldable Functors
练习
require('./support');
var Task = require('data.task');
var _ = require('ramda');
// 模拟浏览器的 localStorage 对象
var localStorage = {};
// 练习 1
// ==========
// 写一个函数,使用 Maybe 和 ap() 实现让两个可能是 null 的数值相加。
// ex1 :: Number -> Number -> Maybe Number
var ex1 = function(x, y) {
};
// 练习 2
// ==========
// 写一个函数,接收两个 Maybe 为参数,让它们相加。使用 liftA2 代替 ap()。
// ex2 :: Maybe Number -> Maybe Number -> Maybe Number
var ex2 = undefined;
// 练习 3
// ==========
// 运行 getPost(n) 和 getComments(n),两者都运行完毕后执行渲染页面的操作。(参数 n 可以是任意值)。
var makeComments = _.reduce(function(acc, c){ return acc+"<li>"+c+"</li>" }, "");
var render = _.curry(function(p, cs) { return "<div>"+p.title+"</div>"+makeComments(cs); });
// ex3 :: Task Error HTML
var ex3 = undefined;
// 练习 4
// ==========
// 写一个 IO,从缓存中读取 player1 和 player2,然后开始游戏。
localStorage.player1 = "toby";
localStorage.player2 = "sally";
var getCache = function(x) {
return new IO(function() { return localStorage[x]; });
}
var game = _.curry(function(p1, p2) { return p1 + ' vs ' + p2; });
// ex4 :: IO String
var ex4 = undefined;
// 帮助函数
// =====================
function getPost(i) {
return new Task(function (rej, res) {
setTimeout(function () { res({ id: i, title: 'Love them futures' }); }, 300);
});
}
function getComments(i) {
return new Task(function (rej, res) {
setTimeout(function () {
res(["This book should be illegal", "Monads are like space burritos"]);
}, 300);
});
}