點燈坊

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

Maybe 初體驗

Sam Xiao's Avatar 2020-12-16

Sanctury 與 Ramda 最大的差別是 Ramda 有些 Function 會回傳 undefined,這導致有些 Function 一不小心會造成 Runtime Error,但 Sanctuary 一律回傳 Maybe,讓我們不用處理 undefined 也不會出錯。

Version

Sanctuary 3.1.0

Ramda

import { pipe, head, toUpper } from 'ramda'

let data0 = ['foo', 'bar', 'baz']
let data1 = []

let f = pipe (
  head,
  toUpper
)

f (data0) // ?
f (data1) // ?

若我們想取得 Array 的第一個 element,並將所有英文字母轉大寫,Ramda 會組合 headtoUpper,但 head 因為可能回傳 undefined,只要傳入 Empty Array 就 runtime error 了。

maybe000

import { pipe, head, toUpper, either, always as K } from 'ramda'

let data0 = ['foo', 'bar', 'baz']
let data1 = []

let f = pipe (
  either (head, K ('')),
  toUpper
)

f (data0) // ?
f (data1) // ?

Ramda 若要解決回傳 undefined 的 function,可用 either 包起來,若回傳 undefined 時改回傳 default value。

maybe001

Maybe

import { pipe, map, toUpper, head } from 'sanctuary'

let data0 = ['foo', 'bar', 'baz']
let data1 = []

let f = pipe ([
  head,
  map (toUpper)
])

f (data0) // ?
f (data1) // ?

若改用 Sanctuary 的 head 則不用擔心 Empty Array,因為 head 會回傳 Maybe,遇到 Empty Array 則回傳 Nothing,因此不會執行 map 內的 toUpper,所以不會有 runtime error。

maybe002

Maybe Chain

import { pipe, map, head, toUpper, concat, append, flip } from 'sanctuary'

let data0 = ['foo', 'bar', 'baz']
let data1 = []

let f = pipe ([
  head,
  map (toUpper),
  map (concat ('_')),
  map (flip (concat) ('_')),
])

f (data0) // ?
f (data1) // ?

Maybe 價值在於後續處理。

若你想在 toUpper 之後繼續在 string 前後加上 _,如 _FOO_,可繼續 map concatflip(concat),而不用擔心過程中出現 undefined,反正過程中只要出現 undefined 就會回傳 Nothing 並終止後續 map 處理。

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

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

maybe004

Composition Law

import { pipe, map, head, toUpper, concat, append, flip } from 'sanctuary'

let data0 = ['foo', 'bar', 'baz']
let data1 = []

let transform = pipe ([
  toUpper,
  concat ('_'),
  flip (concat) ('_')
])

let f = pipe([
  head,
  map (transform)
])

f (data0) // ?
f (data1) // ?

第 6 行

let transform = pipe ([
  toUpper,
  concat ('_'),
  flip (concat) ('_')
])

let f = pipe([
  head,
  map (transform)
])

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

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

maybe005

maybe

import { pipe, map, head, toUpper, concat, append, flip, maybe, I } from 'sanctuary'

let data0 = ['foo', 'bar', 'baz']
let data1 = []

let transform = pipe ([
  toUpper,
  concat ('_'),
  flip (concat) ('_')
])

let f = pipe ([
  head,
  map (transform),
  maybe ('') (I)
])

f (data0) // ?
f (data1) // ?

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

13 行

let f = pipe ([
  head,
  map (transform),
  maybe ('') (I)
])

使用 maybe 從 Maybe 中取出內部值。

maybe()
b -> (a -> b) -> Maybe a -> b
從 Maybe 取出內部值

b:提供 Nothing 預設值

a -> b:傳入處理 Just 的 function

Maybe a:data 為 Maybe

b:回傳 Maybe 內部值

maybe006

fromMaybe

import { pipe, map, head, toUpper, concat, append, flip, fromMaybe } from 'sanctuary'

let data0 = ['foo', 'bar', 'baz']
let data1 = []

let transform = pipe ([
  toUpper,
  concat ('_'),
  flip (concat) ('_')
])

let f = pipe ([
  head,
  map (transform),
  fromMaybe ('')
])

f (data0) // ?
f (data1) // ?

12 行

let f = pipe ([
  head,
  map (transform),
  fromMaybe ('')
])

若需求只是要處理 Nothing 如何顯示,可簡單使用 fromMaybe

fromMaybe
a -> Maybe a -> a
從 Maybe 取出內部值

a:提供 Nothing 預設值

Maybe a:data 為 Maybe

a:回傳 Maybe 內部值

maybe003

Conclusion

  • 實務上 Ramda 會回傳 undefined 的 function 都應使用 Sanctuary 取代,如 headfind 就是典型例子,如此可避免忘記判斷 undefined 而造成 runtime error,且可繼續使用 pure function 組合
  • Maybe 支援 composition law,可將 pure function 先抽出來,避免一堆 map 出現
  • 應該盡量避免過早從 Maybe 取出值,盡量維持在 Maybe 內運算,如此可避免 undefined 造成 runtime error,直到最後顯示時再使用 maybefromMaybe 取出

Reference

Sanctuary, Maybe