Pointy Functor Factory
在前面有关 Functor
的论述中使用了 of
函数,实际上这个函数并不是用来替换 new
这个操作符的:
Pointed Functor
:一种实现了 of
方法的 Functor
。
of
方法在很多地方以不同的名称出现,但是它们都是相同的意思,比如 pure
, point
, unit
或 return
。
JavaScript 语言中有许多类型功能的库:folktale
、ramda
或 fantasy-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
,我们:
- 可以并不增加成本地像用
map
处理纯函数那样,用 chain
处理有副作用的函数; - 甚至可以把
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
:
- 集合基本元素为签名为
T => Monad<T>
的所有映射 。态射即所有基本元素内的映射 - 考虑
chain
,定义二元运算:mcompose = (f, g) => compose(chain(f), g)
- 单位元:
id = (monad) => Monad.of(monad)
; - 组合律:
mcompose(mcompose(f, g), h) === mcompose(f, mcompose(g, h))
;
上面这个 category
在范畴论里来源于一个特殊的范畴 “Kleisli category”;