Monad

Pointy Functor Factory

在前面有关 Functor 的论述中使用了 of 函数,实际上这个函数并不是用来替换 new 这个操作符的:

  • Pointed Functor:一种实现了 of 方法的 Functor

of 方法在很多地方以不同的名称出现,但是它们都是相同的意思,比如 pure, point, unitreturn

JavaScript 语言中有许多类型功能的库:folktaleramdafantasy-land

What’s Monad?

先看一个通过 Container 方法从 Json 中获取给定字段的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const safeProp: string => object => Mabey<any> = curry((x, obj) => Maybe.of(obj[x]));
const safeHead: object => Maybe<any> = safeProp(0);

const firstAddressStreet: object => Maybe<Maybe<Maybe<any>>> = compose(
  map(map(safeProp('street'))),
  map(safeHead),
  safeProp('addresses'),
);

firstAddressStreet({
  addresses: [{ street: { name: 'Mulburry', number: 8402 }, postcode: 'WC2N' }],
});

可以看到在处理真实场景时,map 会像上面的情况一样出现多层嵌套的情况。我们可以用 join 函数解决上面的问题:

  • join 是用于缩减相同类型返回值出现嵌套情况的函数。实现了 join 方法的 pointed functor 被称为 monad

以下是 Maybe 实现 join 方法的例子:

1
2
3
Maybe.prototype.join = function join() {
  return this.isNothing() ? Maybe.of(null) : this.$value;
};

我们可以用 join 优化上面的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const firstAddressStreet = compose(
  join,										// Maybe< Maybe<object> > => Maybe<object>
  map(safeProp('street')),// Maybe<object> => Maybe< Maybe<object> >
  join,										// Maybe< Maybe<object> > => Maybe<object>
  map(safeHead),					// Maybe<object> => Maybe< Maybe<object> >
  safeProp('addresses'), 	// object => Maybe<object>
);

firstAddressStreet({
  addresses: [{ street: { name: 'Mulburry', number: 8402 }, postcode: 'WC2N' }],
});

chain

在上面引入 join 对原来的程序进行了优化之后,虽然解决了嵌套调用和返回多层 Functor 的问题,但是调用链却似乎更长了。

我们发现 join 函数与 map 函数总是连续地出现,我们可以把这个抽象成一个新的函数 chain(或者在某些语境下的其他名称,但是它们都表示相同的意思,比如 >>=flatMap):

1
2
3
4
5
const chain = curry((f, m) => m.map(f).join());
// or
const chain = function <Farg, Fret>(f: Farg => Monad<Fret>): (Monad<Farg> => Monad<Fret>) {
  return compose(join, map(f));
}

同样的我们可以用 chain 来优化上面的例子:

1
2
3
4
5
const firstAddressStreet = compose(
  chain(safeProp('street')),// Maybe<object> => Maybe<object>
  chain(safeHead),					// Maybe<object> => Maybe<object>
  safeProp('addresses'),		// object => Maybe<object>
);

有了强大的 chain,我们:

  1. 可以并不增加成本地像用 map 处理纯函数那样,用 chain 处理有副作用的函数;
  2. 甚至可以把 join 定义为 chain(id),所有这些概念的定义都是相互联系的(Js 中有关这些概念的推导都列在了 fantacyland 中);

下面是一个用 chain 处理副作用函数的例子:

1
2
3
4
5
querySelector('input.username').chain(
  ({ value: uname }) => querySelector('input.email').map(
    ({ value: email }) => `Welcome ${uname} prepare for spam at ${email}`
  )
);

Power Trip

有了上面定义的这些方法我们就可以优化分支极多的错误处理函数了。比如最常见的读取文件后发送网络请求:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// upload :: Filename -> (String -> a) -> Void
const upload = (filename, callback) => {
  if (!filename) {
    throw new Error('You need a filename!');
  } else {
    readFile(filename, (errF, contents) => {
      if (errF) throw errF;
      httpPost('/uploads', contents, (errH, json) => {
        if (errH) throw errH;
        callback(json);
      });
    });
  }
};

可以用下面的一行函数优化:

1
const upload = compose(map(chain(httpPost('/uploads'))), readFile);

范畴学理论

由 Monad 构成的运算规则也可以被定义为 category

  1. 集合基本元素为签名为 T => Monad<T> 的所有映射 。态射即所有基本元素内的映射
  2. 考虑 chain,定义二元运算:mcompose = (f, g) => compose(chain(f), g)
    1. 单位元:id = (monad) => Monad.of(monad)
    2. 组合律:mcompose(mcompose(f, g), h) === mcompose(f, mcompose(g, h))

上面这个 category 在范畴论里来源于一个特殊的范畴 “Kleisli category”;