混合比喻

http://www.organicchemistry.com/wp-content/uploads/BPOCchapter6-6htm-41.png

你看,除了太空墨西哥卷(如果你听说过这个传言的话)(译者注:此处的传言似乎是说一个叫 Chris Hadfield 的宇航员在国际空间站做墨西哥卷的事,视频链接),monad 还被喻为洋葱。让我以一个常见的场景来说明这点:

  1. // Support
  2. // ===========================
  3. var fs = require('fs');
  4. // readFile :: String -> IO String
  5. var readFile = function(filename) {
  6. return new IO(function() {
  7. return fs.readFileSync(filename, 'utf-8');
  8. });
  9. };
  10. // print :: String -> IO String
  11. var print = function(x) {
  12. return new IO(function() {
  13. console.log(x);
  14. return x;
  15. });
  16. }
  17. // Example
  18. // ===========================
  19. // cat :: IO (IO String)
  20. var cat = compose(map(print), readFile);
  21. cat(".git/config")
  22. // IO(IO("[core]\nrepositoryformatversion = 0\n"))

这里我们得到的是一个 IO,只不过它陷进了另一个 IO。要想使用它,我们必须这样调用: map(map(f));要想观察它的作用,必须这样: unsafePerformIO().unsafePerformIO()

  1. // cat :: String -> IO (IO String)
  2. var cat = compose(map(print), readFile);
  3. // catFirstChar :: String -> IO (IO String)
  4. var catFirstChar = compose(map(map(head)), cat);
  5. catFirstChar(".git/config")
  6. // IO(IO("["))

尽管在应用中把这两个作用打包在一起没什么不好的,但总感觉像是在穿着两套防护服工作,结果就形成一个稀奇古怪的 API。再来看另一种情况:

  1. // safeProp :: Key -> {Key: a} -> Maybe a
  2. var safeProp = curry(function(x, obj) {
  3. return new Maybe(obj[x]);
  4. });
  5. // safeHead :: [a] -> Maybe a
  6. var safeHead = safeProp(0);
  7. // firstAddressStreet :: User -> Maybe (Maybe (Maybe Street) )
  8. var firstAddressStreet = compose(
  9. map(map(safeProp('street'))), map(safeHead), safeProp('addresses')
  10. );
  11. firstAddressStreet(
  12. {addresses: [{street: {name: 'Mulburry', number: 8402}, postcode: "WC2N" }]}
  13. );
  14. // Maybe(Maybe(Maybe({name: 'Mulburry', number: 8402})))

这里的 functor 同样是嵌套的,函数中三个可能的失败都用了 Maybe 做预防也很干净整洁,但是要让最后的调用者调用三次 map 才能取到值未免也太无礼了点——我们和它才刚刚见面而已。这种嵌套 functor 的模式会时不时地出现,而且是 monad 的主要使用场景。

我说过 monad 像洋葱,那是因为当我们用 map 剥开嵌套的 functor 以获取它里面的值的时候,就像剥洋葱一样让人忍不住想哭。不过,我们可以擦干眼泪,做个深呼吸,然后使用一个叫作 join 的方法。

  1. var mmo = Maybe.of(Maybe.of("nunchucks"));
  2. // Maybe(Maybe("nunchucks"))
  3. mmo.join();
  4. // Maybe("nunchucks")
  5. var ioio = IO.of(IO.of("pizza"));
  6. // IO(IO("pizza"))
  7. ioio.join()
  8. // IO("pizza")
  9. var ttt = Task.of(Task.of(Task.of("sewers")));
  10. // Task(Task(Task("sewers")));
  11. ttt.join()
  12. // Task(Task("sewers"))

如果有两层相同类型的嵌套,那么就可以用 join 把它们压扁到一块去。这种结合的能力,functor 之间的联姻,就是 monad 之所以成为 monad 的原因。来看看它更精确的完整定义:

monad 是可以变扁(flatten)的 pointed functor。

一个 functor,只要它定义个了一个 join 方法和一个 of 方法,并遵守一些定律,那么它就是一个 monad。join 的实现并不太复杂,我们来为 Maybe 定义一个:

  1. Maybe.prototype.join = function() {
  2. return this.isNothing() ? Maybe.of(null) : this.__value;
  3. }

看,就像子宫里双胞胎中的一个吃掉另一个那么简单。如果有一个 Maybe(Maybe(x)),那么 .__value 将会移除多余的一层,然后我们就能安心地从那开始进行 map。要不然,我们就将会只有一个 Maybe,因为从一开始就没有任何东西被 map 调用。

既然已经有了 join 方法,我们把 monad 魔法作用到 firstAddressStreet 例子上,看看它的实际作用:

  1. // join :: Monad m => m (m a) -> m a
  2. var join = function(mma){ return mma.join(); }
  3. // firstAddressStreet :: User -> Maybe Street
  4. var firstAddressStreet = compose(
  5. join, map(safeProp('street')), join, map(safeHead), safeProp('addresses')
  6. );
  7. firstAddressStreet(
  8. {addresses: [{street: {name: 'Mulburry', number: 8402}, postcode: "WC2N" }]}
  9. );
  10. // Maybe({name: 'Mulburry', number: 8402})

只要遇到嵌套的 Maybe,就加一个 join,防止它们从手中溜走。我们对 IO 也这么做试试看,感受下这种感觉。

  1. IO.prototype.join = function() {
  2. return this.unsafePerformIO();
  3. }

同样是简单地移除了一层容器。注意,我们还没有提及纯粹性的问题,仅仅是移除过度紧缩的包裹中的一层而已。

  1. // log :: a -> IO a
  2. var log = function(x) {
  3. return new IO(function() { console.log(x); return x; });
  4. }
  5. // setStyle :: Selector -> CSSProps -> IO DOM
  6. var setStyle = curry(function(sel, props) {
  7. return new IO(function() { return jQuery(sel).css(props); });
  8. });
  9. // getItem :: String -> IO String
  10. var getItem = function(key) {
  11. return new IO(function() { return localStorage.getItem(key); });
  12. };
  13. // applyPreferences :: String -> IO DOM
  14. var applyPreferences = compose(
  15. join, map(setStyle('#main')), join, map(log), map(JSON.parse), getItem
  16. );
  17. applyPreferences('preferences').unsafePerformIO();
  18. // Object {backgroundColor: "green"}
  19. // <div style="background-color: 'green'"/>

getItem 返回了一个 IO String,所以可以直接用 map 来解析它。logsetStyle 返回的都是 IO,所以必须要使用 join 来保证这里边的嵌套处于控制之中。