用JavaScript理解Functor, Applicative 和 Monad

1,637

在之前的文章中,我们用图片的形式来解释了FunctorApplicativeMonad,但还是太抽象了,现在让我们用JavaScript来继续说明这些概念。

容器

任何值都可以被放入一个上下文中。这个值就好像被放入了盒子中,我们不能直接操作这个值。

image

如图,在上下文(content)中,封装着一个值2。实现这个盒子的代码:

const Just = function(x) {
  this.__value = x;
}

Just.of = function(x) {
  return new Just(x);
};

在上面的代码中,数据类型Just形成了一个上下文,在这个上下文中,有属性__value 用来保存被放入的值。在数据类型Just上有of方法,它作为Just的构造器。

of方法不仅用来避免使用new关键字的,而且还用来把值放到默认最小化上下文default minimal context)中的。

让我们看看这个盒子:

Just.of(3)
// Just { __value: 3 }

Just.of('hotdogs')
// Just { __value: 'hotdogs' }

Just.of(Just.of({ name: 'yoda' }))
// Just { __value: Just { __value: { name: 'yoda' } } }

上面的结果是用node打印出来的,下面的同样如此。

Functor

当一个值被封装在一个盒子中,我们不能直接操作这个值:

const Just = function (x) {
  this.__value = x;
}

Just.of = function (x) {
  return new Just(x);
};

function add(x) {
  return 3 + x;
}

add(Just.of(2))
// 3[object Object]

image

这时,我们就需要一个方法让别的函数能够操作这个值:

// (a -> b) -> Just a -> Just b
Just.prototype.map = function(f){
  return Just.of(f(this.__value))
}

上面的代码中,map函数接受两个参数,返回一个容器:

  • 第一个参数是函数(a-> b): 这个函数接受一个变量a,返回一个变量b,这个ab的类型可能相同,可能不同。

这里变量指没有放在上下文中的值。

  • 第二个参数是数据类型Just,这个Just中封装着类型和a相同的值,和(a -> b)中的 a相对应。
  • 返回值是数据类型Just,这个Just中封装着类型和b相同的值,和(a -> b)中的b相对应。

此时,我们就可以使用map函数来操作上下文里的值了:

Just.of(2).map((a) => a + 3)
// Just { __value: 5 }

过程如下:

我们使用map方法来操作数据,是为了在数据(比如2)不脱离数据类型(比如Just)的情况下,就可以操作数据,操作结束后,为了防止意外再把它放回它所属的容器(Just)。这样,我们能连续地调用map,运行任何我们想运行的函数。甚至还可以改变值的类型。

此时,Just就是一个Functor,它不仅是一种容器类型,也可以使用map 将一个函数运用到一个封装的值上。

所以,functor是实现了map函数并遵守一些特定规则的容器类型。

map方法应该是泛指实现了能操作容器里的值的方法, 下面MonadApplicative的定义同样如此。

Maybe

上下文中可以放入任意的值,当然也就可以放入falsy 值,我们叫这个容器为Maybe。那么运用其他函数来操作里面的值时,有可能会抛出错误,所以我们可以在Maybe里面进行容错处理。

const Maybe = function(x) {
  this.__value = x;
}

Maybe.of = function(x) {
  return new Maybe(x);
}

Maybe.prototype.isNothing = function() {
  return (this.__value === null || this.__value === undefined);
}

Maybe.prototype.map = function(f) {
  return this.isNothing() ? Maybe.of(null) : Maybe.of(f(this.__value));
}

Maybe看起来跟其他容器非常类似,但是有一点不同:Maybe 会先检查自己的值是否为空,然后才调用传进来的函数。

Maybe.of(null).map((a) => a + 3)
// Just { __value: null }

当传给map的值是null 时,代码并没有爆出错误。这样我们就能连续使用map,保证了一种线性的工作流,不必担心错误的数据造成代码抛出错误。

Monad

这是我们上面定义的容器:

const Just = function (x) {
  this.__value = x;
}

Just.of = function (x) {
  return new Just(x)
}

当我们想操作容器里的值时,我们可以这样做:

Just.prototype.map = function (f) {
  return Just.of(f(this.__value))
}

const half = function (x) {
  return x / 2
}

如果此时容器是这样的:

Just.of(3).map(half)

此时容器里面封装着值3,我们使用map操作它的值是没有问题的。这就是上面讲的Functor

但如果此时,容器是这样的:

const nestedContainer = Just.of(Just.of(3))
// Just { __value: Just { __value: 3 } }

此时,如果我们使用map来操作nestedContainer容器里的值是不可能的:

nestedContainer.map(half)

此时回调函数half参数为:

Just { __value: 3 }

这时,我们又将一个被封装过的值运用到一个普通函数上,这又回到了我们最开始的时候。

如果此时我们想操作nestedContainer容器里的值,那我们就需要Monad

monad是可以变扁(flatten)的pointed functor

pointed functor是实现了of方法的functor

我们来为Maybe定义一个join方法,让它成为称为一个Monad

// m a -> (a -> m b) -> m b
Maybe.prototype.join = function() {
  return this.isNothing() ? Maybe.of(null) : this.__value;
}

const mmo = Maybe.of(Maybe.of("nunchucks"));
// Maybe { __value: Maybe { __value: 'nunchucks' } }

mmo.join();
// Maybe { __value: 'nunchucks' }

而对于halfJust 3),Monad是这样处理的:

理论

下面是一个组合(compose)函数:

const compose = function(f,g) {
  return function(x) {
    return f(g(x));
  };
};

对于Monad有:

1. 结合律

 // 结合律
  compose(join, map(join)) == compose(join, join)

用图表示则是:

从左上角往下,先用join合并M(M(M a))最外层的两个 M,然后往右,再调用一次join,就得到了我们想要的M a。或者,从左上角往右,先打开最外层的M,用map(join)合并内层的两个 M,然后再向下调用一次join,也能得到M a。不管是先合并内层还是先合并外层的M,最后都会得到相同的M a,所以这就是结合律。

2. 同一律

 // 同一律 (M a)
  compose(join, of) == compose(join, map(of)) == id

用图表示则是:

如果从左上角开始往右,可以看到of的确把M a丢到另一个M 容器里去了。然后再往下join,就得到了M a,跟一开始就调用id的结果一样。从右上角往左,可以看到如果我们通过map进到了M 里面,然后对普通值a调用of,最后得到的还是M (M a);再调用一次join将会把我们带回原点,即M a

Applicative

Functor可以将封装到上下文里的值运用到普通函数上:

那如果(+3)函数也被封装在容器中:

那么此时,对容器Just里面的值进行加3操作,就变成了:

此时是两个Functor之间的交互,就需要用到Applicative了。

我们先定义一个ap方法,让它可以让两个functor进行交互:

function add(x) {
  return function (y) {
    return x + y;
  };
}

Just.prototype.ap = function (otherContainer) {
  return otherContainer.map(this.__value)
}

Just.of(add(2)).ap(Just.of(3));
// Just { __value: 5 }

其中map函数的参数this.__value是一个函数。

所以Applicative就可以定义为:

applicative functor是实现了ap方法的pointed functor

下面是一个特性:

M.of(a).map(f) = F.of(f).ap(M.of(a))

用图表示则是:

上面实际表示的是map一个f等价于ap一个值为ffunctor

总结:

  • Functor:你可以使用map将一个函数运用到一个封装的值上
  • Applicative:你可以使用ap 将一个封装过的函数运用到一个封装的值上
  • Monad:你可以使用join将一个返回封装值的函数运用到一个封装的值上

参考文献

JS函数式编程指南

Functors, Applicatives, And Monads In Pictures