iT邦幫忙

2021 iThome 鐵人賽

DAY 20
0
Software Development

Functional Programming For Everyone系列 第 20

Day 20 - Maybe Monad II (Piping)

Review

前一篇文章我們談到了如何實作一個 Maybe Monad,而其主要的功能就是處理無值的情境,今天我們要來介紹另外一種寫法去實作 Maybe Monad。

如果有用 rxjs 或是 fp-ts 之類的函式庫,可能會很熟悉這種寫法

pipe(1, R.add(2), R.add(3)) // 6

沒錯,在 rxjs v6 之後都改用 pipe 的方式執行操作符, fp-ts 也是以 pipe 的方式為主,那相較於傳統的鍊式寫法,用 pipe 的方式寫有什麼優點呢?

筆者認為是

  • tree shaking: 開發者只需要對其有使用的操作符買單就好,它不會像 class-basis 一樣一次引入一整包,而在講求快速呈現畫面的前端來說這更是重要,可以減少不少 bundle size.

那廢話不多說,開始改寫吧!

在這之前都假設各位讀者都知道什麼是 pattern match 了!

Piping

在這之前筆者參考 fp-ts 的命名,在 fp-ts 其 Maybe Monad 稱為 option,不同於 Maybe 的 NothingJust, option 是用 none 表示無值, some 表示有值。

然而我們是用 pipe 執行,那就需要先有該函式,來實作一個吧

實作 pipe

const pipe = (init, ...fns) => 
  fns.reduce((prevValue, fn) => fn(prevValue), init);

Function Composition 那章提到得概念一樣, 這次我們只是將我們要執行的初始值放在第一個,接下來再放入我們要執行 piping 的函式。

Constructor

Option a = some a | none
const none = {_tag: "None"}
const some = value => ({_tag: "Some", value})

const match = (onNone, onSome) => (fa) => {
  switch (fa._tag) {
    case 'None':
      return onNone;
    case 'Some':
      return onSome(fa.value);
    default:
      break;
  }
};

const of = (x) => some(x);

Functor

const map = (g) =>
  match(
    () => none,
    (b) => some(g(b))
  );
pipe(of(10), map(R.add(2))) // some(12)
pipe(none, map(R.add(2))) // none

map 就是將目前 option 的狀態 (None || Some) 去辨別在 match 時是要執行 onNone 還是 onSome,而概念都跟前一章提到得一樣,就不在此贅述了。

Applicative Functor

const ap = (f1) => (f2) =>
  pipe(
    f2,
    match(
      () => none,
      (f) =>
        pipe(
          f1,
          match(
            () => none,
            (a) => some(f(a))
          )
        )
    )
  );

const lift2 = R.curry((g, f1, f2) => pipe(f1, map(g), ap(f2)));
lift2(R.add, of(2), of(2)) // some(4)

lift2(R.add, of(2), none) // none

這邊就稍微複雜了一點,以範例來講,由於 ap 是 curried 函式,所以當我們放入 ap(some(2)) 的時候不會立即執行,而是會等到 piping 之後的 some(R.add(2)) 傳入函式才會被執行,也就是 ap 函式參數 f2,以此例子來說, f2some(R.add(2)) 並不是 none, match 函式就會 callback onSome,接下來 f1some(2) 所以 match 也會 callback 執行 onSome,最終就會回傳 some(4)

Chain

const chain = (g) => match(() => none, g);
const safeHead = xs => xs.length === 0 ? none : some(xs[0])

pipe(of([1, 2, 3]), chain(safeHead)) // some(1)
pipe(of([]), chain(safeHead)) // none

chain 也跟前一章提到的一樣,就是打平,而跟 mapjoin 的方式不同,在執行的時候就不會包著 x => some(g(x)) 而是直接執行該函式。

改寫前篇 chain 的寫法就會變成,而概念也是一樣的

class Maybe {
  ...
  chain(f) {
      return f(this.val)
  }
}

Option

const option = (dv) => match(dv, R.identity);
pipe(
  of([1, 2, 3]),
  chain(safeHead),
  option([])
); // 1

pipe(
  of([]),
  chain(safeHead),
  option([])
); // []

而 option 就是把值真正取出來,所以如果最終是有值的情況,我們就是用 identity 函式取值出來,空值則是回傳 default value.

小結

不知道大家覺得哪種寫法比較讚呢? dot chain 還是 pipe?

感謝大家閱讀

Either Monad

Reference

  1. FP Book

上一篇
Day 19 - Maybe Monad
下一篇
Day 21 - Handle Side Effect I
系列文
Functional Programming For Everyone30

尚未有邦友留言

立即登入留言