點燈坊

失くすものさえない今が強くなるチャンスよ

Either 初體驗

Sam Xiao's Avatar 2020-12-16

當 Runtime 出現錯誤且希望執行隨時中斷時,Imperative 會使用 Exception,但 Function Pipeline 並不支援 Exception,需使用 Either 取代。

Version

Sanctuary 3.1.0

try … catch

import { pipe, add } from 'ramda'

let div = x => y => {
  if (y === 0) throw Error ('Can not divide by zero')
  else return x / y
}

let f = x => pipe (
  div (x),
  add (1)
)

f (2) (1) // ? 
f (2) (0) // ?

第 3 行

let div = x => y => {
  if (y === 0) throw Error ('Can not divide by zero')
  else return x / y
}

最典型的 Exception 就是 除以 0,傳統會使用 throw Error 發出 Exception。

第 8 行

let f = x => pipe (
  div (x),
  add (1)
)

throw 最大問題是與 Function Pipeline 格格不入,我們無法在 Function Pipeline 內使用 try catch 處理 Exception。

either000

Either

import { pipe, Left, Right, map, add } from 'sanctuary'

let div = x => y => 
  y === 0 ? 
  Left ('Can not divide by zero') : 
  Right (x / y)

let f = x => pipe ([
  div (x),
  map (add (1))
])

f (2) (1) // ?
f (2) (0) // ?

第 3 行

let div = x => y => 
  y === 0 ? 
  Left ('Can not divide by zero') : 
  Right (x / y)

div 改回傳 Either,若失敗則回傳 Left,成功則回傳 Right。

Left
a -> Either a b
將 Exception 包在 Left 內

a:任意值

Either a b:回傳 Either

Right
b -> Either a b
將正確結果包在 Right 內

b:任意值

Either a b:回傳 Either

第 8 行

let f = x => pipe ([
  div (x),
  map (add (1))
])

由於 div 回傳 Either,這使得 Function Pipeline 得以繼續,只要使用 map 繼需接 add (1) 即可。

either001

Either Chain

import { pipe, Left, Right, map, add, mult } from 'sanctuary'

let div = x => y => 
  y === 0 ? 
  Left ('Can not divide by zero') : 
  Right (x / y)

let f = x => pipe ([
  div (x),
  map (add (1)),
  map (mult (2))
])

f (2) (1) // ?
f (2) (0) // ?

Either 價值在於後續處理。

若你想在 add 之後繼續在 Number 使用 mult (2),可繼續 map (mult (2)) ,而不用擔心過程中出現 Exception,反正過程中只要出現 Exception 就會回傳 Left 並終止後續 map 處理。

反之若你提早 either 成一般值,後面的運算就必須自行承擔 Exception 風險。

所以我們都會希望 function 能回傳 Either,只有在最後要顯示時才用 either 從 Either 內取出。

either004

Composition Law

import { pipe, Left, Right, map, add, mult } from 'sanctuary'

let div = x => y => 
  y === 0 ? 
  Left ('Can not divide by zero') : 
  Right (x / y)

let transform = pipe ([
  add (1),
  mult (2)
])  

let f = x => pipe ([
  div (x),
  map (transform)
])

f (2) (1) // ?
f (2) (0) // ?

第 8 行

let transform = pipe ([
  add (1),
  mult (2)
])  

let f = x => pipe ([
  div (x),
  map (transform)
])

Either 必須使用 map 才能改變其內部值,因此可能發現一堆 map

Either 支援 composition law,可將 pure function 先使用 pipe 組合起來,一次傳給 map 即可。

either005

either

import { pipe, Left, Right, map, add, mult, either, show, I } from 'sanctuary'

let div = x => y => 
  y === 0 ? 
  Left ('Can not divide by zero') : 
  Right (x / y)

let transform = pipe ([
  add (1),
  mult (2)
])  

let f = x => pipe ([
  div (x),
  map (transform),
  either (show) (I)
])

f (2) (1) // ?
f (2) (0) // ?

Either 畢竟無法用於顯示,因此必須從 Either 取出內部值。

13 行

let f = x => pipe ([
  div (x),
  map (transform),
  either (show) (I)
])

使用 either 從 Either 取出內部值。

either
(a -> c) -> (b -> c) -> Either a b -> c
從 Either 取出內部值

a -> c:處理 Left 的 function

b -> c:處理 Right 的 function

Either a b:data 為 Either

c:回傳 Either 內部值

either002

fromEither

import { pipe, Left, Right, map, add, mult, fromEither } from 'sanctuary'

let div = x => y => 
  y === 0 ? 
  Left ('Can not divide by zero') : 
  Right (x / y)

let transform = pipe ([
  add (1),
  mult (2)
])  

let f = x => pipe ([
  div (x),
  map (transform),
  fromEither (0)
])

f (2) (1) // ?
f (2) (0) // ?

13 行

let f = x => pipe ([
  div (x),
  map (transform),
  fromEither (0)
])

若需求是只要有 Exception 不必顯示錯誤訊息,顯示 0 即可,可簡單使用 fromEither

fromEither
b -> Either a b -> b
從 Either 取出內部值

b:提供 Left 預設值

Either a b:data 為 Either

b:回傳 Either 內部值

either003

Conclusion

  • FP 必須使用 Either 處理 Exception,因為 try catch 無法在 Function Pipeline 下使用
  • Either 支援 composition law,可將 pure function 先抽出來,避免一堆 map 出現
  • 將 Exception 包在 Either 內,使得 Function Pipeline 得以繼續,重點是我們還能如 Maybe 一樣繼續在 Either 內處理,直到最後再使用 eitherfromEither 取出

Reference

Sanctuary, Either
LogRocket, Elegant Error Handling with the JavaScript Either Monad
Michele Riva, Handling Exception Using the Either Monad in JavaScript