vlambda博客
学习文章列表

【函数式编程】函子(二)

证明

现在,证明我们JavaScript定义的有效性。

首先,从现在开始,你要认为Just(x)是一种数据结构了,它代表着一类数据(一种范畴)。

我们先把id函数补充进去

// Just为我们的一个“函子”
function Just(value{
    return {
        value, // 保存值
        // 值为基本数据,支持映射到范畴(Just)内其他元素
        // addOne = x => x + 1
        // 比如 Just(1).map(addOne)    -> Just(2)
        mapfunction map(fn{
            return Just(fn(value))
        },
        // 值为基本数据,支持映射到自身范畴
        // 比如 Just(1).flatMap(addOne)    -> 2
        flatMapfunction flatMap(fn{
            return fn(value);  
        },
        // 值为函数(映射),支持映射到范畴(Just)内的其他元素
        // 比如Just((x) => x + 1).fmap(Just(2))     -> Just(3)
        fmapfunction fmap(just// 函子 Just(compose(b, a)) = composeJust(Just(b), Just(a));
            return just.map(value);
        },
        idfunction 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类型看起来有这些功能

  1. 保存你的原始值(程序保存value),把你从JavaScript原始值范畴映射到Just范畴
  2. 态射值(程序实现为映射函数map),允许你在Just范畴上把Just(X)值态射为另一个Just(Y)
  3. 态射函数(程序实现为映射函数fmap),允许你在Just范畴上把Just(f)函数作为Just范畴上的值Just(X)的态射函数,并映射Just(X)为Just(Y)
  4. 提取你的原始值(程序实现为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也可以完成更多的事。

课后习题

  1. 实现 compose(...args)对任意函数列表都执行
  2. 实现 curry函数把一个函数 fn的参数列表编程一元化的