iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 28
2
Software Development

Functional Programming in JS系列 第 28

Either Monad: 更優雅的除錯

https://ithelp.ithome.com.tw/upload/images/20200928/20106426HQ5zrWI20T.jpg
(↑ 沒有誇張,這圖我畫了兩個小時以上,用 Canva 畫曲線實在是太難了啊... )

Monad 本身在 FP 中已可以榮獲最難跨越的門檻冠軍,何況是用 javaScript 來解釋又更難了,很多概念在 JS 中都沒有,甚至還有分 Either Monad、Task Monad ... 等等讓人卻步。我相信我說的只是廣大 Monad 汪洋中一小片海域而已,不過就算知道得不夠深入,但實際能運用到並感受它的好,還是蠻感動。

就像這篇要介紹的 Either Monad 是現在公司程式碼有導入的概念; 從新人訓練時我對他的一頭霧水到現在能知道這些程式碼在做什麼跟為什麼要用,就覺得過去無數個熬夜天是有收穫的。

Let's talk about the problem

開始之前先來討論一下,過去在寫程式是如何除錯的

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

再次爆錯。我們到底要怎麼做才能處理所有可能的錯誤!
https://ithelp.ithome.com.tw/upload/images/20200925/20106426S1r6vJlpKt.jpg

What's Either Monad doing?

這就是 Either Monad 厲害之處啦 ! Either Monad 就像兩條鐵路

  • 一條是 Happy Path (Right),就是運算過程一切順利;
  • 另一條是 Sad Path (Left),只要某一處的運算出現錯誤,就會跳過之後運算直接輸出失敗結果
    https://ithelp.ithome.com.tw/upload/images/20200928/20106426C5NODHJo9d.jpg
// ====================
// 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 這個方法,之前提過 fold 就是把 Data Type 裡面東西取出,flatMap 也是同義詞。
可以看到 Rightfold 是執行第二個參數的函式,而 Leftfold 是執行第一個參數的函式。因為這樣的特性讓我們可以完全掌控希望輸出的內容

// 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

來用 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'

如何 debug

若中間想要知道現在回傳到底變什麼,要怎麼寫 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


參考文章

如有錯誤或需要改進的地方,拜託跟我說。
我會以最快速度修改,感謝您

歡迎追蹤我的部落格,除了技術文也會分享一些在矽谷工作的甘苦。


上一篇
Monad: 圖解篇
下一篇
Either Monad: 應用在真實專案
系列文
Functional Programming in JS30

1 則留言

0
Ken
iT邦新手 5 級 ‧ 2020-10-20 10:58:20

今天好像又更看懂一點了!感謝大大

我要留言

立即登入留言