FP15:Monad-2

September 10, 2018 by Sylvenas

“A monad is just a monoid in the category of endofunctors. What’s the problem?”

Monad是非常简单的,但是它的概念却有点让人云里雾里,尤其是网上查询资料博客的时候,一般会从范畴论开始讲解,这是正确的道路,不过可惜的是大部分的JavaScript开发人员,并不懂范畴论,范畴不仅仅是一种数学语言,也是一种哲学观点,范畴论也绝不是一两篇文章就能讲清楚的,这里我们不过多的去说明范畴论,而仅仅是谈论一下在计算机编程中的Monad的概念

Monad是一种组合函数的方法,除了返回值之外,还需要上下文,比如计算,if/else分支,IO等等。Monad可以类型提升,并且扁平化的映射a -> M(b),使函数可组合,可以把类型a的数据映射成数据b的类型,并隐藏了实现的细节

这里说的上下文不同于函数执行上下文,这里的上下文,仅仅是只数据的外部环境,或者是某种Wrapper、Box之类的概念,例如数组的仅仅是对数据的一种包裹,但是却可以提供很多的便捷的方法

  • Function map:a=>b
  • Functor map with context:Functor(a) => Functor(b)
  • Monad flatten and map with context:Monad(Monad(a)) => Monad(b)

上面所说的:mapflatten,context又是什么意思呢?

  • Map的意思是说,使用参数a调用一个函数,计算之后,函数返回值为b. Given some input, return some output.
  • Context是组合Monad的计算细节,这里和Functor类似,我们可以直接调用fmap之类的方法,却无需关心实现的细节,这样我们就可以放心的在上下文环境中,完成从数据a,到数据b的映射,并返回处于同样上下文中的b,eg:Array(a) => Array(b),Observable(a) => Observable(b)
  • Type lift意味着将数据提升到上下文中,这样可以方便的使用上下文的方法,a => Functor(a),Monad只不过是更强大的Functor,eg:字符串'abc','xyz',把他们做个类型提升:['abc','xyz'],那么就可以方便快捷的,使用数据的map,filter等等方法了,借助Functor可以提升任何类型的数据
  • Flatten是从上下文中取出数据Functor(a) => a,去除包装,取出果实,有可能一个值是被层层包装的,那么就是层层的去除包装来扁平化,类似于拨洋葱一样一层一层的去除外衣

Example:

const x = 20;             // Some data of type `a`
const f = n => n * 2;     // A function from `a` to `b`
const arr = Array.of(x);  // The type lift.
// JS has type lift sugar for arrays: [x]
// .map() applies the function f to the value x
// in the context of the array.
const result = arr.map(f); // [40]

在这个例子中,Array就是context,x是被包裹的值。

这个例子中没有包含,数组中的数组,但是使用concat扁平化数组,绝不陌生:

[].concat.apply([], [[1], [2, 3], [4]]); // [1, 2, 3, 4]

You’re probably already using monads

函数组合创建数据流经的函数管道。您在管道的第一阶段输入了一些输入,并且一些数据从管道的最后一个阶段弹出,进行了转换。但要实现这一点,管道的每个阶段都必须期望前一阶段返回的数据类型。

编写简单的函数很容易,因为类型都很容易排列。只需将输出类型b的函数g与输入类型b的函数f匹配即可:

g: a => b
f: b => c
h = f(g(a)): a => c

如果是在Functor中进行组合或者连续调用,也非常简单,因为永远都是相同的Wrapper类型:

g: F(a) => F(b)
f: F(b) => F(c)
h = f(g(Fa)): F(a) => F(c)

但是如果你想组合的函数是a => F(b),b => F(c),这个时候就需要Monad了,使用MFunctor替换一下F,让问题更清晰一些:

g: a => M(b)
f: b => M(c)
h = composeM(f, g): a => M(c)

Oops.这个时候发现类型对应不上,f函数的输入我们想要的是类型b,但是我们得到的却是类型M(b),由于这中错位,在composeM中,我们需要从函数g的返回值M(b)中取出数据b。而这个过程正是flatenmap的过程

g: a => M(b) flattens to => b
f: b maps to => M(c)
h composeM(f, g):a flatten(M(b)) => b => map(b => M(c)) => M(c)

在上面中M(b) => b的展平,以及b => M(c)的映射,实在a => M(c)chain中完成的,在更高层级composeM中的完成的,稍后会讲解composeM如何实现。

现在我们只要知道我们借助Monad完成更高级的函数组合,有很多的函数不是简单的从a => b的映射,有些函数需要处理副作用,例如(promise)、分支处理(Either)、异常处理(Maybe)。。。

举一个更实际的例子,如果我们要从一个异步的API中获取某个用户的信息,然后把这个信息,传递给另一个异步的API,来查询别的数据,这个时候我们怎么办呢?

getUserById(id) => Promise(User)
hasPermision(User) => Promise(Boolean)

首先写一些工具函数,帮助我们完成任务:

const compose = (...fns) => x => fns.reduceRight((y, f) => f(y), x);
const trace = label => value => {
  console.log(`${ label }: ${ value }`);
  return value;
};

然后可以这样使用代码:

const label = 'API call composition';

// a => Promise(b)
const getUserById = id => id === 3 ?
    Promise.resolve({ name: 'Kurt', role: 'Author' }) :
    undefined

// b => Promise(c)
const hasPermission = ({ role }) => (
    Promise.resolve(role === 'Author')
);

// Try to compose them. Warning: this will fail.
const authUser = compose(hasPermission, getUserById);
// Oops! Always false!
authUser(3).then(trace(label));

当我们组合getUserByIdhasPermission函数的时候,我们发现了一个大问题,hasPermission函数期望得到一个User对象作为参数,而getUserById函数的返回值却是Promise(User),为了解决这个问题,我们需要使用then方法从Promise(User)中把User对象取出来,为此我们做一个定制版的composePromises函数:

const composeM = chainMethod => (...ms) => (
    ms.reduce((f, g) => x => g(x)[chainMethod](f))
);
const composePromises = composeM('then');
const label = 'API call composition';

// a => Promise(b)
const getUserById = id => id === 3 ?
    Promise.resolve({ name: 'Kurt', role: 'Author' }) :
    undefined

// b => Promise(c)
const hasPermission = ({ role }) => (
    Promise.resolve(role === 'Author')
);

// Compose the functions (this works!)
const authUser = composePromises(hasPermission, getUserById);
authUser(3).then(trace(label)); // true

Promise也是一种Monad。

What Monads are Made of

Monad遵循一个简单的对称,把一个值包装到context中,并且能够把值从context中取出来。

  • Lift/Unit:把一个值包装到Monad的context中,a => M(a)
  • Flatten/Join:把值从context中取出来,M(a) => a

Monad肯定也是一个Functor,那么很明显也有一个fmap方法:

  • Map:从一个Functor映射到另一个Functor,M(a) => M(b)

合并FlattenMap,这个就是Chain

  • FlatMap/Chain: Flatten + map: M(M(a)) => M(b)

在Promise中.then方法实际上就是Monad中的FlatMap/Chain方法

Monad是一个抽象接口(类似于Java中的Interface),定义了实现该接口必须定义的方法,而实现了Monad的具体类型被称为Monadic,Monadic才是根据方向,可以有不同的具体的实现,例如Promise,Array等等

扩展应用

看一个具体的例子:

// The algebraic definition of function composition:
// (f ∘ g)(x) = f(g(x))
const compose = (f, g) => x => f(g(x));
const x = 20;    // The value
const arr = [x]; // The container
// Some functions to compose
const g = n => n + 1;
const f = n => n * 2;
// Proof that .map() accomplishes function composition.
// Chaining calls to map is function composition.
trace('map composes')([
    arr.map(g).map(f),
    arr.map(compose(f, g))
]);
// => [42], [42]

不仅仅是数组具有map方法,我们可以把任何包含map方法的Functor,都可以组合

const composeMap = (...ms) => ms.reduce((f, g) => x => g(x).map(f))

Promise的组合:

const label = 'Promise composition';
const g = n => Promise.resolve(n + 1);
const f = n => Promise.resolve(n * 2);
const h = composePromises(f, g);
h(20)
.then(trace(label))
// Promise composition: 42

其实规律非常简单,只要是这种结构的数据都可以自由的定义组合:

const composeM = method => (...ms) => (
  ms.reduce((f, g) => x => g(x)[method](f))
);

const composePromises = composeM('then');
const composeMap = composeM('map');
const composeFlatMap = composeM('flatMap');