到目前為止我們已經知道了 Functor 可以將 effect 跟 pure function 進行 compose,但我們沒有提到一點是 Functor 無法應付的,而今天要介紹的主角就是用來解決此一情境的,Apply。
f | g | composition |
---|---|---|
pure | pure | compose(f, g) |
effects | pure(unary) | f.map(g) |
effects | pure(n-ary) | ? |
舉例來說,現在有兩個 Identity,分別是 Identity(1)
跟 Identity(2)
,我們要如何將其進行相加
如果用一般正常函式的相加,看來是行不通
const add = (x, y) => x + y;
add(Identity.of(1), Identity.of(2)) // [object Object][object Object]
可能有讀者想到, 如果用 map 將分別將 Identity 內的值取出,再將 add 在套用到取出後的值就好了
const result = Identity.of(1).map(x => Identity.of(2).map(y => add(x, y))) // Identity(Identity(3))
確實,這樣也是一個辦法,但這種方法不但會導致前後相依,也就是必須等待前面的運算完成後才能進行下一個運算,也會使運算後變成深層結構 Identity(Identity(3))
,取值時必須
result.val.val
那該怎麼辦呢? 有沒有方法讓其既可以是獨立的也可以維持單層結構,就讓來介紹 Apply 吧!
Type Signature
ap :: Apply f => f a ~> f (a -> b) -> f b
Law
v.ap(u.ap(a.map(f => g => x => f(g(x))))) === v.ap(u).ap(a)
Implementation
所以要解決上述提到的問題可以分成四步驟
1. 將 add
函式進行 currying
const add = R.curry((x, y) => x + y);
2. 實作 ap
const Identity = (val) => ({
val,
map: (f) => {
const result = f(val);
return Identity(result);
},
ap: function (o) {
// 根據 Fantasy-land 對 Apply 定義,其為 `a.ap(b)` 其 `b` 一定會是函式,而在此範例則是 o.val 必為函式
return this.map(o.val);
},
inspect: () => `Identity(${val})`,
});
3. 建立一個 helper function lift2
const lift2 = g => f1 => f2 => f2.ap(f1.map(g));
注意: 這就為什麼我們的
g
一定要進行 currying
而其實 ap
就是將兩個 Container 進行合併,相較於前面章節提過的 Semigroup 則是透過呼叫 concat
將值進行合併。
另外如果今天是要將 g
(3-ary) 合併三個 Container
const lift3 = g => f1 => f2 => f3 => f3.ap(f2.ap(f1.map(g)));
4. 接下來就可以輕鬆的解決一開始的問題
lift2(add)(Identity(1))(Identity(2)) // Identity(3)
沒錯,我們現在解決了 effects 跟 pure(n-ary) 的問題!!! 所以來更新一下一開始提到的表吧!
f | g | composition |
---|---|---|
pure | pure | compose(f, g) |
effects | pure(unary) | f.map(g) |
effects | pure(n-ary) | f1.map(g).ap(f2) |
感謝大家的閱讀!!