iT邦幫忙

2021 iThome 鐵人賽

DAY 19
0

yo, what's up

在之前我們都是用 Identity 作為例子,但其功用並不大,所以今天要來開始介紹一些比較常用的 ADTs,今天就從 Maybe 開始進行介紹

Maybe 的用途

Maybe Monad 是專門處理無值( null || undefined ) 情境的 Monad

其大部份的使用情境就是安全的取值,舉例來說,當我們要透過 url param 的 key 去取 config 值後,再根據娶回來的資料渲染頁面

// config.js
const pageConfig = {
    "food": {
        "imageUrl": "https://s3.image.com/food-example.png",
        "foodMenu": [{"title": "fired chicked"}, {"title": "fired rice"}]
    },
    "travel": {
        "imageUrl": "https://s3.image.com/travel-example.png",
        "foodMenu": [{"title": "Taipei"}, {"title": "Tainan"}]
    }
}
// app.js
const { parse } = require('query-string');
const paramsObj = parse(location.search)

此時空值的情況可能發生在 paramsObj 或是 pageConfig[paramsObj.target], 所以我們可能會有很多 if 在程式內

const getPageConfig = (target) => {
    if(!target) return;
    const config = pageConfig[target];
    if(config) {
        ... // do something
    }
}

接下來開始介紹 Maybe 吧!

Maybe 實作

Constructor

Maybe a = Just a | Nothing 

Maybe 可以想像成是一個 Container,裡面是由兩種情境,有值 Just a 與空值 Nothing 組合而成的

Functor

class Maybe {
  static of(val) {
      return new Maybe(val);
  }
  
  get isNothing() {
      return (this.val === null || this.val === undefined);
  }
  
  constructor(val) {
    this.val = val;
  }

  map(fn) {
    return this.isNothing ? this : Maybe.of(fn(this.val));
  }

  inspect() {
    return this.isNothing ? 'Nothing' : `Just(${this.val})`;
  }
}

Maybe 是一個 Functor, 而其可以與 pure function (unary) 進行 compose

Maybe.of(10)
    .map(R.add(2))
    .inspect() // Just(12)
    
Maybe.of(null)
    .map(R.add(2)) 
    .inspect() // Nothing

可以看到,當 Maybe aa 值為 null 時,就不會繼續運算下去,而直接跳過所有運算回傳 Nothing

Applicative Functor

class Maybe {
  ...
  ap(f) {
      return this.isNothing ? this : this.map(f)
  }
}

const lift2 = R.curry((g, f1, f2) => f2.ap(f1.map(g)))

而當 pure function 參數長度為 (n-ary),其概念也是跟之前 Identity 的邏輯很像,不同的是 Maybe 一樣會判斷如果為空值就會回傳 Nothing

lift2(R.add, Maybe.of(2), Maybe.of(2)).inspect() // Just(4)

lift2(R.add, Maybe.of(2), Maybe.of(null)).inspect() // Nothing

Chain

class Maybe {
  ...
  join() {
      return this.isNothing ? this : this.val;
  }
  
  chain(f) {
      return this.map(f).join()
  }
}

如果要 compose 的函式是 effect,像是 safeHead 則我們就可以用 chain 去進行 compose

const safeHead = xs => Maybe.of(xs[0])

Maybe.of([1, 2, 3])
    .chain(safeHead)
    .inspect() // Just(1)
    
Maybe.of(null)
    .chain(safeHead)
    .inspect() // Nothing

Option

class Maybe {
  ...
  option(defaultV) {
      return this.isNothing ? defaultV : this.val;
  }
}

到這裡大家可能發現一個問題,要如何把 Container 裡的值真正取出來,而 Option 的作用就是在此,並且可以放入一個 default value,當取出來的是 Nothing 的時候,Option 會將該值作為 fallback

Maybe.of(100)
    .option('default value') // 100

Maybe.of(null)
    .option('default value') // default value

solution

接下來就來解決我們一開始遇到的問題,在之前可以先 mock parse(location.search) 後的資料,其可能的值為 {target: 'xxx', ...} 或是 {...}

首先如果我們是正向的流程會是這樣

const safeGet = R.curry((obj, target) => Maybe.of(obj[target]))

cosnt result = Maybe.of({target: "food"})
    .map(R.prop('target'))
    .chain(safeGet(pageConfig))
    .option({})
    
// {
//   "imageUrl": "https://s3.image.com/food-example.png",
//   "foodMenu": [{"title": "fired chicked"}, {"title": "fired rice"}]
// }

若值為空,則是會 fallback 到 {}

cosnt result = Maybe.of({})
    .map(R.prop('target'))
    .chain(safeGet(pageConfig))
    .option({}) // {}

avaliable on gist

小結

感謝大家閱讀,明天將介紹另外一種實作方式

NEXT: Maybe Monad II

Reference

  1. Maybe Moand

上一篇
Day 18 - Chain
下一篇
Day 20 - Maybe Monad II (Piping)
系列文
Functional Programming For Everyone30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言