【函数式编程】函子(二)
证明
现在,证明我们JavaScript
定义的有效性。
首先,从现在开始,你要认为Just(x)是一种数据结构了,它代表着一类数据(一种范畴)。
我们先把id函数补充进去
// Just为我们的一个“函子”
function Just(value) {
return {
value, // 保存值
// 值为基本数据,支持映射到范畴(Just)内其他元素
// addOne = x => x + 1
// 比如 Just(1).map(addOne) -> Just(2)
map: function map(fn) {
return Just(fn(value))
},
// 值为基本数据,支持映射到自身范畴
// 比如 Just(1).flatMap(addOne) -> 2
flatMap: function flatMap(fn) {
return fn(value);
},
// 值为函数(映射),支持映射到范畴(Just)内的其他元素
// 比如Just((x) => x + 1).fmap(Just(2)) -> Just(3)
fmap: function fmap(just) { // 函子 Just(compose(b, a)) = composeJust(Just(b), Just(a));
return just.map(value);
},
id: function id(anything) {
return Just(value);
}
}
}
然后我们看一下haskell
的实现方式。
fmap id = id
fmap (f . g) = fmap f . fmap g
这是haskell
常见的写法,递归定义。第一行表明使用fmap操作id函数,得到id函数自身。我们在JavaScript中实现id函数并证明Just满足自等定律。
var id = (x) => x;
// 在Number范畴中,id函数自等,即x = id(x)
var a = 1;
console.log(a === id(a)); // true
// 在Just范畴中
var ja = Just(1);
function eq(justa, justb) { // 实现一个Just范畴的判等函数
return justa.flatMap(id) === justb.flatMap(id);
}
console.log(eq(ja, ja.id(ja))); // true 等于实现Just范畴中的 === 操作
Just(id).fmap(Just(999)); // Just(999) 等于实现Just范畴中的 fmap id = id
fmap
已经实现了,我们再用lambda
函数(箭头函数)来看一下具体区别。
// 需要实现操作: fmap (g . f) = (fmap g) . (fmap f)
var a = 3;
var square = x => x * x;
var addOne = x => x + 1;
// Number范畴中
var compose = (g, f) => x => g(f(x))
var compound = compose(square, addOne);
console.log(compound(a)); // (3 + 1) ^2 = 16
Just(compound)
.fmap(Just(a)) // Just(16) 等于实现了 fmap compound =
Just(square)
.fmap(
Just(addOne).fmap(Just(a))
) // Just(16) 等于实现了= fmap f . fmap g
风格
现在,你必须接受Just就是一种通用类型了,就像你能接受Number类型、String类型一样。Just类型看起来有这些功能
-
保存你的原始值(程序保存value),把你从JavaScript原始值范畴映射到Just范畴 -
态射值(程序实现为映射函数map),允许你在Just范畴上把Just(X)值态射为另一个Just(Y) -
态射函数(程序实现为映射函数fmap),允许你在Just范畴上把Just(f)函数作为Just范畴上的值Just(X)的态射函数,并映射Just(X)为Just(Y) -
提取你的原始值(程序实现为flatMap),把你从Just范畴拉回到JavaScript原始值范畴
这种写法风格,等价为给数据值封装了一系列操作,以后你使用值和进行运算,都必须在Just上进行。这就是范畴论,一种研究数据「对象」和「态射」的方法。你可以理解类比为三角形有函数表示、几何表示、代数表示、复数表示等方式,范畴态射把同一个事物映射到不同的研究空间里面去,本质上,你还是在做类似的事情。
以上的几个方法只是monad世界的冰山一角,后面我们要理解为什么要做这种抽象,也会讲更多的操作,用以构建我们的程序世界。
compose函数的意义
组合函数令我们的程序,能够以更方便的方式进行组合,并且让我们的基础函数可以得到复用。比如加减乘除四个操作只需要对应四个函数,程序中没有必要创建这种函数
function addThree(a, b, c) {
return a + b + c;
}
而可以通过组合的方式实现
var add = a => b => a + b;
var addThree = compose(add(c), add(b), add(a))(0)
数组中的Array.prototype.reduce
方法就可以完成这件事,不过reduce
也可以完成更多的事。
课后习题
-
实现 compose(...args)
对任意函数列表都执行 -
实现 curry
函数把一个函数fn
的参数列表编程一元化的