Monad单词的意思是单细胞动物的意思,我们经常把他翻译成单子。

在学习Monad之前我们先来说下IO函子的一个问题,在linux系统中有个cat命令,是读取文件内容并且把他打印出来,我们写一个函数来模拟这个命令。

class IO {
    static of (x) {
        return new IO(function() {
            return x;
        })
    }
    consturctor (fn) {
        this._value = fn;
    }
    map (fn) {
        return new IO(fp.flowRight(fn, this._value));
    }
}

我们先来写一个读取文件的函数,再来写一个打印的函数,然后把他们组合成一个函数。

因为读取文件存在副作用,会让函数变得不纯,所以我们这里使用IO函子, 也就是我们把读取文件的过程延迟执行。

在打印函数中我们也返回IO函子,延迟执行

let readFile = function (filename) {
    return new IO(function () {
        return fs.readFileSync(filename, 'utf-8');
    })
}

let print = function (x) {
    return new IO(function () {
        console.log(x);
        return x;
    })
}

下面我们将这两个函数合并成cat。

let cat = fp.flowRight(print, readFile);

let r = cat('package.json');

console.log(r);

这里的调用之后,我们readFile返回一个IO函子,IO函子传入print函数之后,这个函数返回了一个函子,函子中的value就是readFile的函子,所以这里拿到的是嵌套函子。

下面我们去执行函子里面的函数,之前我们介绍过可以通过调用_value()执行。

console.log(r._value());

当我们执行_value的时候得到的是readFile函数返回的IO函子, 因为readFile返回值会传递给print函数。

我们现在想要拿到文件的结果,我们还需要再调用一次_value方法,这个方法才是readFile中的_value

console.log(r._value()._value());

至此我们就获取到了文件内容,但是问题是我们在调用嵌套函子的时候非常的不方便,我们需要._value()._value(),这看起来很怪异。

下面我们来介绍一下Monad来解决一下上面的问题。

Monad是可以变扁的Pointed函子,那什么是变扁呢,上面我们出现了一个问题,就是函子嵌套的话,我们调用起来会很不方便,变扁就是解决函子嵌套的问题。

之前学过,如果函数嵌套的话,可以使用函数组合来解决这个问题,如果函子嵌套就可以使用Monad。

如果一个函子同时具有join和of两个方法,并且遵守一些定律的话,就是一个Monad。

of我们很熟悉,join也不复杂,他直接就返回了我们对_value的调用。

我们将IO类改造成Monad,添加一个join方法,join方法不需要任何参数,这里只是返回_value的调用。

class IO {
    static of (x) {
        return new IO(function() {
            return x;
        })
    }
    consturctor (fn) {
        this._value = fn;
    }
    map (fn) {
        return new IO(fp.flowRight(fn, this._value));
    }
    join () {
        return this._value();
    }
}

接着我们再写一个flatMap方法,我们在使用Monad的时候经常会把map和join联合起来去使用,因为map的作用是把当前的函数和函子内部的value组合起来,返回一个新的函子,map在组合这个函数的时候,这个函数最终也会返回一个函子,所以我们需要调用join把他变扁,把他拍平。

flatMap的作用就是同时调用map和join,flatMap要调用map方法,map方法需要一个fn参数,所以flatMap也需要一个参数,在flapMap执行完成之后,我们要去调用join,并且把join执行的结果,也就是这个函子返回。

当我们调用map的时候,我们就把value和fn进行合并,合并之后返回一个新的函子,在这个函子包裹的函数最终也会返回一个函子,所以我们再去调用join()。

class IO {
    static of (x) {
        return new IO(function() {
            return x;
        })
    }
    consturctor (fn) {
        this._value = fn;
    }
    map (fn) {
        return new IO(fp.flowRight(fn, this._value));
    }
    join () {
        return this._value();
    }
    flatMap(fn) {
        return this.map(fn).join();
    }
}

至此Monad就写完了,下面我们看下如何去使用。

let r = readFile('package.json');

当我们调用readFile的时候他会生成一个函子,这个函子包裹了我们读文件的操作,然后我们将读文件的操作和打印的操作合并起来。

我们要调用map还是flatMap取决于我们要合并的函数返回的是值还是函子,如果是指就调用map,函子就调用flatMap。

let r = readFile('package.json').flatMap(print);

我们调用完readFile会返回一个IO函子,它里面封装了一个读取文件的函数,接下来调用flatMap我们传入print,我们看下flatMap执行,当我们调用flatMap的时候传入了print,在flatMap里面调用了this.map, 我们将print和当前函子内部的value进行合并,合并之后返回了一个新的函子。

当我们调用完map之后,我们得到一个函子,并且这个函子中报国的函数最终返回的还是一个函子,接着我们调用join,他就是调用返回这个函子的value。

所以flatMap返回的就是print的函子,最后我们想要获取print的文件内容,我们再调用一下join就可以了, 因为join就是在调用内部的value。

let r = readFile('package.json').flatMap(print).join();

这里可能看起来比较麻烦,不过在实际运用中是不需要关心函子内部实现的,只需要调用函子的api实现想要的功能就可以了。

假设我们读取完文件内容,我们想把文件的字符串全部转换成大写,我们直接在readFile后面调用map方法就可以了,因为map方法作用是处理函子内部value的值。