应用 applicative functor

考虑到其函数式的出身,applicative functor 这个名称堪称简单明了。函数式程序员最为人诟病的一点就是,总喜欢搞一些稀奇古怪的命名,比如 mappend 或者 liftA4。诚然,此类名称出现在数学实验室是再自然不过的,但是放在其他任何语境下,这些概念就都像是扮作达斯维达去汽车餐馆搞怪的人。(译者注:此处需要做些解释,1. 汽车餐馆(drive-thru)指的是那种不需要顾客下车就能提供服务的地方,比如麦当劳、星巴克等就会有这种 drive-thru;2. 达斯维达(Darth Vader)是《星球大战》系列主要反派角色,在美国大众文化中的有着广泛的影响力,其造型是很多人致敬模仿的对象;3. 由于 2 的缘故,美国一些星战迷会扮作 Darth Vader 去 drive-thru 点单,YouTube 上有不少这种搞怪视频;4. 作者使用这个“典故”是为了说明函数式里很多概念的名称有些“故弄玄虚”,而 applicative functor 是少数比较“正常”的。)

无论如何,applicative 这个名字应该能够向我们表明一些事实,告诉我们作为一个接口,它能为我们带来什么:那就是让不同 functor 可以相互应用(apply)的能力。

然而,你可能会问了,为何一个正常的、理性的人,比如你自己,会做这种“让不同 functor 相互应用”的事?而且,“相互应用”到底是什么意思

要回答这些问题,我们可以从下面这个场景讲起,可能你已经碰到过这种场景了。假设有两个同类型的 functor,我们想把这两者作为一个函数的两个参数传递过去来调用这个函数。简单的例子比如让两个 Container 的值相加:

// 这样是行不通的,因为 2 和 3 都藏在瓶子里。
add(Container.of(2), Container.of(3));
//NaN

// 使用可靠的 map 函数试试
var container_of_add_2 = map(add, Container.of(2));
// Container(add(2))

这时候我们创建了一个 Container,它内部的值是一个局部调用的(partially applied)的函数。确切点讲就是,我们想让 Container(add(2)) 中的 add(2) 应用到 Container(3) 中的 3 上来完成调用。也就是说,我们想把一个 functor 应用到另一个上。

巧的是,完成这种任务的工具已经存在了,即 chain 函数。我们可以先 chain 然后再 map 那个局部调用的 add(2),就像这样:

Container.of(2).chain(function(two) {
  return Container.of(3).map(add(two));
});

只不过,这种方式有一个问题,那就是 monad 的顺序执行问题:所有的代码都只会在前一个 monad 执行完毕之后才执行。想想看,我们的这两个值足够强健且相互独立,如果仅仅为了满足 monad 的顺序要求而延迟 Container(3) 的创建,我觉得是非常没有必要的。

事实上,当遇到这种问题的时候,要是能够无需借助这些不必要的函数和变量,以一种简明扼要的方式把一个 functor 的值应用到另一个上去就好了。