yo, what's up
在之前我們都是用 Identity 作為例子,但其功用並不大,所以今天要來開始介紹一些比較常用的 ADTs,今天就從 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 吧!
Constructor
Maybe a = Just a | Nothing
Maybe
可以想像成是一個 Container,裡面是由兩種情境,有值 Just a
與空值 Nothing
組合而成的
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 a
其 a
值為 null
時,就不會繼續運算下去,而直接跳過所有運算回傳 Nothing
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
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({}) // {}
感謝大家閱讀,明天將介紹另外一種實作方式
NEXT: Maybe Monad II