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 20] Edge Impulse + BLE Sense实现唤醒词辨识(上)

>>:  (10)建立基本类神经网路程序

用Firebase Web的小功能分享 (3)

上传档案後制作超连结下载档案 - 用innerHTML制作超连结 code 最後就是如何显示map...

[2020铁人赛Day28]糊里糊涂Python就上手-Pandas的观念与运用(上)

今日目标 学习了解 Python Pandas 的观念与运用 What is Pandas? Pan...

05 竞赛程序经验谈

我第一次听过程序竞赛时是在我刚进到国三的下学期。 在这之前我对於程序的相关经验除了国中科展学的 PH...

codepen

如何载入CND 1.Fork 2.齿轮 ...

【从零开始的 C 语言笔记】第二十篇-While Loop(2)

不怎麽重要的前言 上一篇介绍了while loop的概念,让大家在回圈的使用上可以相对的弹性。 这次...