混合比喻

你看,除了太空墨西哥卷(如果你听说过这个传言的话)(译者注:此处的传言似乎是说一个叫 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 来解析它。logsetStyle 返回的都是 IO,所以必须要使用 join 来保证这里边的嵌套处于控制之中。