混合比喻
你看,除了太空墨西哥卷(如果你听说过这个传言的话)(译者注:此处的传言似乎是说一个叫 Chris Hadfield 的宇航员在国际空间站做墨西哥卷的事,视频链接),monad 还被喻为洋葱。让我以一个常见的场景来说明这点:
// Support
// ===========================
var fs = require('fs');
// readFile :: String -> IO String
var readFile = function(filename) {
return new IO(function() {
return fs.readFileSync(filename, 'utf-8');
});
};
// print :: String -> IO String
var print = function(x) {
return new IO(function() {
console.log(x);
return x;
});
}
// Example
// ===========================
// cat :: IO (IO String)
var cat = compose(map(print), readFile);
cat(".git/config")
// IO(IO("[core]\nrepositoryformatversion = 0\n"))
这里我们得到的是一个 IO
,只不过它陷进了另一个 IO
。要想使用它,我们必须这样调用: map(map(f))
;要想观察它的作用,必须这样: unsafePerformIO().unsafePerformIO()
。
// cat :: String -> IO (IO String)
var cat = compose(map(print), readFile);
// catFirstChar :: String -> IO (IO String)
var catFirstChar = compose(map(map(head)), cat);
catFirstChar(".git/config")
// IO(IO("["))
尽管在应用中把这两个作用打包在一起没什么不好的,但总感觉像是在穿着两套防护服工作,结果就形成一个稀奇古怪的 API。再来看另一种情况:
// safeProp :: Key -> {Key: a} -> Maybe a
var safeProp = curry(function(x, obj) {
return new Maybe(obj[x]);
});
// safeHead :: [a] -> Maybe a
var safeHead = safeProp(0);
// firstAddressStreet :: User -> Maybe (Maybe (Maybe Street) )
var firstAddressStreet = compose(
map(map(safeProp('street'))), map(safeHead), safeProp('addresses')
);
firstAddressStreet(
{addresses: [{street: {name: 'Mulburry', number: 8402}, postcode: "WC2N" }]}
);
// Maybe(Maybe(Maybe({name: 'Mulburry', number: 8402})))
这里的 functor 同样是嵌套的,函数中三个可能的失败都用了 Maybe
做预防也很干净整洁,但是要让最后的调用者调用三次 map
才能取到值未免也太无礼了点——我们和它才刚刚见面而已。这种嵌套 functor 的模式会时不时地出现,而且是 monad 的主要使用场景。
我说过 monad 像洋葱,那是因为当我们用 map
剥开嵌套的 functor 以获取它里面的值的时候,就像剥洋葱一样让人忍不住想哭。不过,我们可以擦干眼泪,做个深呼吸,然后使用一个叫作 join
的方法。
var mmo = Maybe.of(Maybe.of("nunchucks"));
// Maybe(Maybe("nunchucks"))
mmo.join();
// Maybe("nunchucks")
var ioio = IO.of(IO.of("pizza"));
// IO(IO("pizza"))
ioio.join()
// IO("pizza")
var ttt = Task.of(Task.of(Task.of("sewers")));
// Task(Task(Task("sewers")));
ttt.join()
// Task(Task("sewers"))
如果有两层相同类型的嵌套,那么就可以用 join
把它们压扁到一块去。这种结合的能力,functor 之间的联姻,就是 monad 之所以成为 monad 的原因。来看看它更精确的完整定义:
monad 是可以变扁(flatten)的 pointed functor。
一个 functor,只要它定义个了一个 join
方法和一个 of
方法,并遵守一些定律,那么它就是一个 monad。join
的实现并不太复杂,我们来为 Maybe
定义一个:
Maybe.prototype.join = function() {
return this.isNothing() ? Maybe.of(null) : this.__value;
}
看,就像子宫里双胞胎中的一个吃掉另一个那么简单。如果有一个 Maybe(Maybe(x))
,那么 .__value
将会移除多余的一层,然后我们就能安心地从那开始进行 map
。要不然,我们就将会只有一个 Maybe
,因为从一开始就没有任何东西被 map
调用。
既然已经有了 join
方法,我们把 monad 魔法作用到 firstAddressStreet
例子上,看看它的实际作用:
// join :: Monad m => m (m a) -> m a
var join = function(mma){ return mma.join(); }
// firstAddressStreet :: User -> Maybe Street
var firstAddressStreet = compose(
join, map(safeProp('street')), join, map(safeHead), safeProp('addresses')
);
firstAddressStreet(
{addresses: [{street: {name: 'Mulburry', number: 8402}, postcode: "WC2N" }]}
);
// Maybe({name: 'Mulburry', number: 8402})
只要遇到嵌套的 Maybe
,就加一个 join
,防止它们从手中溜走。我们对 IO
也这么做试试看,感受下这种感觉。
IO.prototype.join = function() {
return this.unsafePerformIO();
}
同样是简单地移除了一层容器。注意,我们还没有提及纯粹性的问题,仅仅是移除过度紧缩的包裹中的一层而已。
// log :: a -> IO a
var log = function(x) {
return new IO(function() { console.log(x); return x; });
}
// setStyle :: Selector -> CSSProps -> IO DOM
var setStyle = curry(function(sel, props) {
return new IO(function() { return jQuery(sel).css(props); });
});
// getItem :: String -> IO String
var getItem = function(key) {
return new IO(function() { return localStorage.getItem(key); });
};
// applyPreferences :: String -> IO DOM
var applyPreferences = compose(
join, map(setStyle('#main')), join, map(log), map(JSON.parse), getItem
);
applyPreferences('preferences').unsafePerformIO();
// Object {backgroundColor: "green"}
// <div style="background-color: 'green'"/>
getItem
返回了一个 IO String
,所以可以直接用 map
来解析它。log
和 setStyle
返回的都是 IO
,所以必须要使用 join
来保证这里边的嵌套处于控制之中。