點燈坊

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

使用 Promise 實現 Either Monad

Sam Xiao's Avatar 2020-12-16

Promise 傳統都用來處理 Asynchronous 代表 未來值,但由於其具有 Rejected 與 Fulfilled,很類似 Either 的 Left 與 Right,且 then() 又兼具 map()chain() 特性,因此可藉由 Promise 實現 Either 處理 Exception。

Version

Ramda 0.27.1

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

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

第 10 行

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

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

overview000

Promise as Either

import { pipe, andThen as then, otherwise, add } from 'ramda'
import { resolve, reject } from 'wink-fp'

let div = x => y => 
  y === 0 ?
  reject('Can not divide by zero') : 
  resolve(x / y)
  
let f = x => pipe(
  div(x),
  then(add(1))
)

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

第 4 行

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

原本當 y === 0 會發出 Exception,改回傳 Rejected Promise,正常結果則回傳 Fulfilled Promise。

注意在此 div() 並非處理 asynchronous,只是借用 Promise 具有 Rejected 與 Fulfilled 特性,將 Promise 當成 Either Monad 使用

第 9 行

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

由於 div() 回傳 Promise,因此後續處理的 add(1) 必須包在 then() 內。

此處 then() 相當於 Either Monad 的 map()

overview001

可發現當 y 傳入 0 時,將回傳 Can not divide by zero Rejected Promise,類似 Either 的 Left

Promise Chain

import { pipe, andThen as then, otherwise, add } from 'ramda'
import { resolve, reject } from 'wink-fp'

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

let mul = x => y => 
  y == 3 ?
  reject('Can not multiply by 3') :
  resolve(x * y)  

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

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

第 9 行

let mul = x => y => 
  y == 3 ?
  reject('Can not multiply by 3') :
  resolve(x * y)  

假設 mul() 也是自行實作,且當 y3 時會發出 Exception,其他值則正常處理,也可繼續使用 Rejected Promise 實作 Either 的 Left。

14 行

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

mul() 雖然回傳 Either 可能造成 Nested Promise,但只要繼續使用 then() 即可,並不需要如 Either 改用 chain(),因為 Promise 的 then() 會攤平 Nested Promise,相當於 chain()flatMap()

因為 Promise 的 then() 兼具 map()chain() 特性,因此不論使用普通 function,或者回傳 Either 的 function,皆統一使用 then() 即可

overview002

Composition Law

import { pipe, andThen as then, otherwise, add } from 'ramda'
import { resolve, reject } from 'wink-fp'

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

let mul = x => y => 
  y == 3 ?
  reject('Can not multiply by 3') :
  resolve(x * y)  

let transform = pipe(
  add(1),
  mul(2)
)  

let f = x => pipe(
  div(x),
  then(transform)
)

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

14 行

let transform = pipe(
  add(1),
  mul(2)
)  

let f = x => pipe(
  div(x),
  then(transform)
)

Promise 必須使用 then() 才能改變其內部值,因此可能發現一堆 then()

Promise 支援 composition law,可將 pure function 先使用 pipe() 組合起來,一次傳給 then() 即可。

overview003

then() / otherwise()

import { pipe, andThen as then, otherwise, add } from 'ramda'
import { resolve, reject, log, error } from 'wink-fp'

let div = x => y => 
  y === 0 ?
  reject('Can not divide by zero') : 
  resolve(x / y)
  
let f = x => pipe(
  div(x),
  then(add(1)),
  then(log),
  otherwise(error)
)

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

Promise 畢竟無法用於顯示,因此必須將結果從 Promise 內取出。

第 9 行

let f = x => pipe(
  div(x),
  then(add(1)),
  then(log),
  otherwise(error)
)

Fulfilled Promise 可繼續使用 then() 取出,而 Rejected Promise 則由 otherwise() 取出。

Conclusion

  • Promise 的 Rejected 與 Fulfilled 類似 Either Monad 的 Left 與 Right,且 then() 又兼具 map()chain() 特性,因此可將 Promise 視為 Either Monad 使用,並不一定都用在處理 asynchronous

Reference

Sanctuary, Either