(↑ 沒有誇張,這圖我畫了兩個小時以上,用 Canva 畫曲線實在是太難了啊... )
Monad 本身在 FP 中已可以榮獲最難跨越的門檻冠軍,何況是用 javaScript 來解釋又更難了,很多概念在 JS 中都沒有,甚至還有分 Either Monad、Task Monad ... 等等讓人卻步。我相信我說的只是廣大 Monad 汪洋中一小片海域而已,不過就算知道得不夠深入,但實際能運用到並感受它的好,還是蠻感動。
就像這篇要介紹的 Either Monad 是現在公司程式碼有導入的概念; 從新人訓練時我對他的一頭霧水到現在能知道這些程式碼在做什麼跟為什麼要用,就覺得過去無數個熬夜天是有收穫的。
開始之前先來討論一下,過去在寫程式是如何除錯的
const findColor = name => ({
red: '#ff4444',
blue: '#3b5998'
}[name])
const res = findColor('red').toUpperCase(); // '#FF4444',
以上沒什麼問題,但假如不小心打錯
findColor('RED').toUpperCase();
// TypeError: Cannot read property 'toUpperCase' of undefined
這不得了,整個爆錯,好吧那針對錯誤 debug
findColor('RED'.toLowerCase()).toUpperCase();
// '#FF$$$$'
但假如又打錯
findColor('redd'.toLowerCase()).toUpperCase();
// TypeError: Cannot read property 'toUpperCase' of undefined
再次爆錯。我們到底要怎麼做才能處理所有可能的錯誤!
這就是 Either Monad 厲害之處啦 ! Either Monad 就像兩條鐵路
// ====================
// Definitions
// ====================
// This is Happy Path
const Right = x =>
({
chain: f => f(x),
map: f => Right(f(x)),
fold: (f, g) => g(x), // run the second g
toString: `Right(${x})` // 方便檢視
})
// This is Sad Path
const Left = x =>
({
chain: f => Left(x),
map: f => Left(x),
fold: (f, g) => f(x), // run the first f
toString: `Left(${x})`
})
你可以看到 Right
這個 data type 幾乎跟之前所說的 Box data type 一模一樣; 而 Left
比較不一樣一點,並不是跑 Function。
const findColor = name => {
const found = {
red: '#ff4444',
blue: '#3b5998'
}[name];
return found ? Right(found) : Left('not found');
}
const res = findColor('red') // return 一個 Right() data type
const res = findColor('reddd') // return 一個 Left()
const res = findColor('red').map(x => x.toUpperCase()) // Right('#FF$$$$')
const res = findColor('red').map(x => x.toUpperCase()) // Left('not found')
這樣子寫就永遠不會報錯了,因為一旦有錯誤就會 return Left 這個 Sad Path
另外要提的是 fold
這個方法,之前提過 fold
就是把 Data Type 裡面東西取出,flatMap
也是同義詞。
可以看到 Right
的 fold
是執行第二個參數的函式,而 Left
的 fold
是執行第一個參數的函式。因為這樣的特性讓我們可以完全掌控希望輸出的內容
// Success Happy Path
const res = () =>
findColor('red')
.map(x => x.toUpperCase())
.fold(
() => 'no Color',
color => color
) // '#FF4444'
// Failed Sad Path
const res = () =>
findColor('redddd')
.map(x => x.toUpperCase()) // 不會執行這行,會直接輸出 'no Color'
.fold(
() => 'no Color',
color => color
) // 'no Color'
Either Monad 幫我們處理 Sad Path,所以不會 throw 任何 Error 出來
來用 fromNullable refactor 也可以更簡潔
const fromNullable = x =>
x != null ? Right(x) : Left(null)
const findColor = name =>
fromNullable({
red: '#ff4444',
blue: '#3b5998'
}[name]);
// Success Happy Path
const res = () =>
findColor('red')
.map(x => x.toUpperCase())
.fold(
() => 'no Color',
color => color
) // '#FF4444'
若中間想要知道現在回傳到底變什麼,要怎麼寫 console.log
? 這裡提供一個很不錯的方法
const log = msg => x => {
console.log(msg, '--->', x, '<---');
return x;
};
findColor('red')
.map(log('Before upper Case'))
// "Before upper Case" "--->" "#ff4444" "<---"
.map(x => x.toUpperCase())
.map(log('After upper Case'))
// "After upper Case" "--->" "#FF4444" "<---"
.fold(
() => 'no Color',
color => color
) // '#FF4444'
const street_ = user => {
const address = user.address
if(address) {
return address.street
} else {
return 'no street'
}
}
這解答其實可以用三元運算式就結束也很符合 FP,但只是為了練習用 ~
const Right = x =>
({
chain: f => f(x),
map: f => Right(f(x)),
fold: (f, g) => g(x),
toString: () => `Right(${x})`
})
const Left = x =>
({
chain: f => Left(x),
map: f => Left(x),
fold: (f, g) => f(x),
toString: () => `Left(${x})`
})
const fromNullable = x =>
x != null ? Right(x) : Left(null)
const street = user =>
fromNullable(user.address)
.map(address => address.street)
.fold( () => 'no street', x => x )
想看更多練習可以到 Either Monad 提供的 codepen
如有錯誤或需要改進的地方,拜託跟我說。
我會以最快速度修改,感謝您
歡迎追蹤我的部落格,除了技術文也會分享一些在矽谷工作的甘苦。