轉評:你造promise就是monad嗎

可健康了發表於2016-07-01

  看到一遍好文章,與我的想法如出一轍,先轉為敬。首先說說我對Monad和promise的理解:

  Monad的這種抽象方式是為了簡化程式中不確定狀態的判斷而提出的,能夠讓程式設計師從更高的層次順序描述程式邏輯的每一個動作,而不必關注每一個動作是否會出現異常,也不必關注第一個動作內是否需要邏輯判斷,是否要跳轉。haskell趣學指南我翻了好幾遍,終於對Monad這個概念有了一點認識,這個抽象是偉大的,它極大提高了程式的可讀性,同時降低了開發難度。這裡還要推薦一下haskell這門語言,Monad的概念就是haskell提出的,haskell的思想完全不同於傳統的oo語言,學習過之後,你的程式設計世界觀會有一個轉變,原來程式還能這麼寫呀,然後回頭再寫oo語言時,效率會比原來有質的飛躍。

  再說promise,經常寫前端的朋友應該對這個東西不陌生,前端語言如js,flash等經常要向後端傳送ajax請求,當多個ajax巢狀在一起時,回撥函式大坑簡直讓人崩潰,promise就是為了解決這個問題而發明的。用了promise,從此可以優雅的傳送ajax,序列發,並行發,多層巢狀發,想怎麼發就怎麼發。用了這麼長時間的promise,我終於發現,原來promise就是Monad啊。

 

以下是原文,作者把Haskell趣學指南中的程式碼翻譯成Js,完美闡釋了Monad的概念,贊!

 

Monad 這個概念好難解釋, 你可以理解為一個 Lazy 或者是狀態未知的盒子. 聽起來像是薛定諤貓(估計點進去你會更暈了). 其實就是的, 在你開啟這個盒子之前, 你是不知道里面的貓處在那種狀態.

Monad 這個黑盒子, 裡面到底賣的神馬藥,我們要開啟喝了才知道.

等等, 不是說好要解釋 Either 的嗎, 嗯嗯, 這裡就是在解釋 Either. 上節說 Either 是一個 Functor, 可以被 fmap over. 怎麼這裡又說道黑盒子了? 好吧, Monad 其實也是 Functor. 還記得我說的 Functor 其實是一個帶 context 的盒子嗎. 而 fmap 使得往盒子裡應用函式變換成為了可能.

Either

先來看看 Either 這種型別會幹什麼事情. Either 表示要不是左邊就是右邊的值, 因此我們可以用它來表示薛定諤貓, 要不是活著, 要不死了. Either 還有個方法:
either

(a -> c) -> (b -> c) -> Either a b -> c

想必你已經對箭頭->非常熟了吧.如果前面幾章你都跳過了,我再翻譯下好了. 這裡表示接收函式a->c和函式b->c, 再接收一個 Either, 如果 Either 的值在左邊,則使用函式對映a->c, 若值在右邊,則應用第二個函式對映b->c.

作為 Monad, 它還必須具備一個方法 '>>='(這個符號好眼熟的說, 看看 haskell 的 logo, 你就知道 Monad 是有多重要), 也就是 bind 方法.

 

bind 方法的意思很簡單, 就是給這個盒子加一個操作, 比如往盒子在加放射性原子,如果貓活著,就是綠巨貓, 如果貓是死的,那就是綠巨死貓.

Left("cat").bind(cat => 'hulk'+cat)
// => Left "hulkcat"
Right("deadcat").bind(cat => 'hulk' + cat)
// => Right "hulkdeadcat"

這有個毛用啊. 表急... 來看個經典例子

走鋼索

皮爾斯決定要辭掉他的工作改行試著走鋼索。他對走鋼索蠻在行的,不過仍有個小問題。就是鳥會停在他拿的平衡竿上。他們會飛過來停一小會兒,然後再飛走。這樣的情況在兩邊的鳥的數量一樣時並不是個太大的問題。但有時候,所有的鳥都會想要停在同一邊,皮爾斯就失去了平衡,就會讓他從鋼索上掉下去。

我們這邊假設兩邊的鳥差異在三個之內的時候,皮爾斯仍能保持平衡。

一般解法

首先看看不用 Monad 怎麼解

eweda.installTo(this);
var landLeft = eweda.curry(function(n, pole){
    return [pole[0]+n, pole[1]];
});
var landRight = eweda.curry(function(n, pole){
    return landLeft(n, eweda.reverse(pole));
});
var result = eweda.pipe(landLeft(1), landRight(1), landLeft(2))([0,0]);
console.log(result);
// => [3, 1]

還差一個判斷皮爾斯是否掉下來的操作.

var landLeft = eweda.curry(function(n, pole){
    if(pole==='dead') return pole;
    if(Math.abs(pole[0]-pole[1]) > 3)
      return 'dead';
    return [pole[0]+n, pole[1]];
});
var landRight = eweda.curry(function(n, pole){
    if(pole==='dead') return pole;
    return landLeft(n, eweda.reverse(pole));
});
var result = eweda.pipe(landLeft(10), landRight(1), landRight(8))([0,0]);
console.log(result);
// => dead

完整程式碼


現在來試試用 Either

我們先把皮爾斯放進 Either 盒子裡, 這樣皮爾斯的狀態只有開啟 Either 才能看見. 假設 Either Right 是活著, Left 的話皮爾斯掛了.

var land = eweda.curry(function(lr, n, pole){
    pole[lr] = pole[lr] + n;
    if(Math.abs(pole[0]-pole[1]) > 3) {
      return new Left("dead when land " + n + " became " + pole);
    }
    return new Right(pole);
});

var landLeft = land(0)
var landRight = land(1);

現在落鳥後會返回一個 Either, 要不活著, 要不掛了. 開啟盒子的函式可以是這樣的

var stillAlive = function(x){
    console.log(x)
}
var dead = function(x){
    console.log('皮爾斯' + x);
}
either(dead, stillAlive, landLeft(2, [0,0]))

好吧, 好像有一點點像了, 但是這隻落了一次鳥, 如果我要落好幾次呢. 這就需要實現 Either 的 >>= bind 方法了, 如果你還記得前面實現的 Functor, 這裡非常像 :

var Monad = function(type, defs) {
  for (name in defs){
    type.prototype[name] = defs[name];
  }
  return type;
};
function Left(value){
  this.value = value
}
function Right(value){
  this.value=value;
}

Monad(Right, {
  bind:function(fn){
    return fn(this.value)
  }
})

Monad(Left, {
  bind: function(fn){
    return this;
  }
})

哦, 對了, either:

either = function(left, right, either){
    if(either.constructor.name === 'Right')
        return right(either.value)
    else
        return left(either.value)
}

我們來試試工作不工作.

var walkInLine = new Right([0,0]);
eitherDeadOrNot = walkInLine.bind(landLeft(2))
    .bind(landRight(5))
either(dead, stillAlive, eitherDeadOrNot)
// => [2,5]
eitherDeadOrNot = walkInLine.bind(landLeft(2))
  .bind(landRight(5))
  .bind(landLeft(3))
  .bind(landLeft(10)
  .bind(landRight(10)))

either(dead, stillAlive, eitherDeadOrNot)
// => "皮爾斯dead when land 10 became 15,5"

完整程式碼

到底有什麼用呢, Monad

我們來總結下兩種做法有什麼區別:
1. 一般做法每次都會檢查查爾斯掛了沒掛, 也就是重複獲得之前操作的 context
2. Monad 不對異常做處理, 只是不停地往盒子裡加操作. 你可以看到對錯誤的處理推到了最後取值的 either.
2. Monad 互相傳遞的只是盒子, 而一般寫法會把異常往下傳如"dead", 這樣導致後面的操作都得先判斷這個異常.

comment 由於是用 JavaScript, pole 不限定型別, 所以這裡單純的用字串代表 pole 的異常狀態. 但如果換成強型別的 Java, 可能實現就沒這麼簡單了.

看來已經優勢已經逐步明顯了呢, Monad 裡面保留了值的 context, 也就是我們對這個 Monad 可以集中在單獨的本次如何操作value, 而不用關心 context.

還有一個 Monad 叫做 Maybe, 實際上皮爾斯的

Monad 在 JavaScript 中的應用

你知道 ES6有個新的 型別 Promise 嗎, 如果不知道, 想必也聽過 jQuery 的 $.ajax吧, 但如果你沒聽過 promise, 說明你沒有認真看過他的返回值:

var aPromise = $.ajax({
    url: "https://api.github.com/users/jcouyang/gists"
    dataType: 'jsonp'
    })
aPromise /***
=> Object { state: .Deferred/r.state(),
    always: .Deferred/r.always(),
    then: .Deferred/r.then(),
    promise: .Deferred/r.promise(),
    pipe: .Deferred/r.then(),
    done: b.Callbacks/p.add(),
    fail: b.Callbacks/p.add(),
    progress: b.Callbacks/p.add() }
***/

我們看到返回了好多Deferred型別的玩意, 我們來試試這玩意有什麼用

anotherPromise = aPromise.then(_ => _.data.forEach(y=> console.log(y.description)))
/* =>
Object { state: .Deferred/r.state(),
    always: .Deferred/r.always(),
    then: .Deferred/r.then(),
    promise: .Deferred/r.promise(),
    pipe: .Deferred/r.then(),
    done: b.Callbacks/p.add(),
    fail: b.Callbacks/p.add(),
    progress: b.Callbacks/p.add() }

"connect cisco anyconnect in terminal"
"為什麼要柯里化(curry)"
"批量獲取人人影視下載連結"
......
*/

看見沒有, 他又返回了同樣一個東西, 而且傳給 then 的函式可以操作這個物件裡面的值. 這個物件其實就是 Promise 了. 為什麼說這是 Monad 呢, 來試試再寫一次走鋼絲:

這裡我們用的是 ES6 的 Promise, 而不用 jQuery Defered, 記得用 firefox 哦. 另外 eweda 可以這樣裝

var ewd = document.createElement('script'); ewd.type = 'text/javascript'; ewd.async = true;
            ewd.src = 'https://rawgit.com/CrossEye/eweda/master/eweda.js';
(document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(ewd);

eweda.installTo(this); //安裝到 window 上
var land = curry(function(lr, n, pole){
    pole[lr] = pole[lr] + n;
    if(Math.abs(pole[0]-pole[1]) > 3) {
      return new Promise((resovle,reject)=>reject("dead when land " + n + " became " + pole));
    }
    return new Promise((resolve,reject)=>resolve(pole));
});

var landLeft = land(0)
var landRight = land(1);

Promise.all([0,0])
.then(landLeft(2), _=>_)
.then(landRight(3), _=>_) // => Array [ 2, 3 ]
.then(landLeft(10), _=>_)
.then(landRight(10), _=>_)
.then(_=>console.log(_),_=>console.log(_))
// => "dead when land 10 became 12,3"

這下是不承認 Promise 就是 Monad 了. 原來我們早已在使用這個神祕的 Monad, 再想想 Promise,也沒有那麼抽象和神祕了.

相關文章