yo, what's up 又看到了這張熟悉的表了,想必大家都已經知道這章要來介紹什麼了,
但在這之前先來複習一下,兩個程式 f
與 g
如何進行 compose
f | g | composition |
---|---|---|
pure | pure | compose(f, g) |
effects | pure(unary) | f.map(g) |
effects | pure(n-ary) | f1.map(g).ap(f2) |
effects | effects | ? |
pure & pure
在 Function Composition 這章,我們已經提到如何用 compose
將兩(多)個純函式進行 compose
compose
const compose = (...fns) =>
fns.reduce(
(acc, fn) => (...args) => acc(fn(...args)),
x => x
)
用法
const f = str => str.toUpperCase();
const g = str => str.concat('!');
compose(g, f)('fp') // In math, we called g o f
// 'FP!'
pure (un-ary) & effect
而在 Functor 這章,則是介紹到 effect 如何透過 map
與純函式進行 compose,並用 Identity
這個簡單的 ADT 作為範例
map
const Identity = val => ({
val,
map: f => Identity(f(val)),
inspect: () => `Identity(${val})`
})
用法
Identity('fp').map(f).map(g).val // 'FP!'
pure (n-ary) & effects
接下來我們又在 Apply 這章提到,如何透過 ap
去處理當純函式需放入多個 effect 作為參數的情境,同樣也用了 Identity
作為範例,並實作 helper function lift2
ap
const Identity = val => ({
val,
map: f => Identity(f(val)),
ap: function (o) {
return this.map(o.val);
},
inspect: () => `Identity(${val})`
})
Identity.of = x => Identity(x)
helper function lift2
,若 g
的參數長度為 2
const lift2 = g => f1 => f2 => f2.ap(f1.map(g));
g
一定要是 curried 函式
用法
const concat = R.curry((x, y) => x.concat(y))
lift2(concat)(Identity.of('FP'))(Identity.of('!')).val // 'FP!'
effect & effect
如果現在 f
與 g
都是 effect 時
f :: a -> f a
g :: b -> f b
如果是上面的 signature 要如何進行 compose 呢,這就是本章要來介紹的 chain
假設我們目前有 toUpper
與 exclaim
這兩個函式,但其回傳值皆是包覆在 Identity
內
// toUpper :: a -> Identity a
const toUpper = str => Identity.of(str.toUpperCase())
const exclaim = str => Identity.of(str.concat('!'))
首先,我們可以想像要怎麼將 toUpper
套用在 Identity.of('fp')
上
const result = Identity.of("fp")
.map(str => toUpper(str)) // Identity(Identity('FP'))
可以看到此結構已經是 nested 的結構,如何再將 exclaim
套用在該值
result
.map(wrappedStr =>
wrappedStr.map(upperStr => exclaim(upperStr)) // Identity(Identity(Identity('FP!')))
)
看一下目前的結構 Identity(Identity(Identity('FP!')))
更深層了呢!!
如果要將值取出來我們需要
result.val.val.val
跟之前在 Apply 那章遇到的問題一樣,那這要如何解決呢?
要解決上面的問題其實不難,就是我們需要有一個 method 是專門打平 (flatten) 包覆住的值,也就是我們只需要將其維持在一層的結構,如何實踐呢?
join
讓我們來實作 join
這個 method
const Identity = val => ({
val,
map: f => Identity(f(val)),
ap: function (o) {
return this.map(o.val);
},
join: () => val,
inspect: () => `Identity(${val})`
})
Identity.of = x => Identity(x)
join
做的事情非常簡單,就是打平結構
Identity.of("fp")
.map(toUpper)
.join()
.map(exclaim)
.join()
.val // 'FP!'
看起來是解決我們的問題了,但是每次我們要對 effect 進行 compose 時都需要 map
, join
, map
, join
... 有沒有方法可以時同時進行呢??
這就是 chain
誕生的由來了
Type Signature
chain :: Chain m => m a ~> (a -> m b) -> m b
Law
m.chain(f).chain(g) === m.chain(x => f(x).chain(g))
Implement
const Identity = val => ({
val,
map: f => Identity(f(val)),
ap: function (o) {
return this.map(o.val);
},
join: () => val,
chain: function (f) {
return this.map(f).join()
},
inspect: () => `Identity(${val})`
})
Identity.of = x => Identity(x)
可以看到 chain
就是先 map
再 join
,並要符合 associativity!
Identity.of("fp")
.chain(toUpper)
.chain(exclaim)
.val // 'FP!'
// same as
Identity.of("fp").chain(x => toUpper(x).chain(exclaim)).val // 'FP!'
isn't that neat!?
各位弟兄們!! 更新一下我們的 compose 小卡
f | g | composition |
---|---|---|
pure | pure | compose(f, g) |
effects | pure(unary) | f.map(g) |
effects | pure(n-ary) | f1.map(g).ap(f2) |
effects | effects | m.chain(f).chain(g) |
目前我們已經簡單介紹了一些常用的 Algebraic Structure,接下來我們要開始介紹一些常用的 ADTs,每種 ADT 都有它存在的意義,就像是 design pattern,都是各個大神從相似的問題中找出相通點,並針對相通點給定一個通用的解決方案整理成冊。各種 design pattern 都有適用的場景, ADT 也是一樣,但不同的是 ADT 裡的概念(Algebraic Structure) 背後都是有數學去佐證,每個 Algebraic Structure 都有 law,而這些 law 不僅幫助我們對於開發時增加信心,也確保了我們在進行 effect 的操作,後面都有數學佐證替我們背書。
前幾章主要是我們為什麼會需要此概念,它解決了什麼,但並沒有提到其背後數學的概念,也就是 category theroy,如果讀者們有想要更深入背後的原理,可以參考的課程 MIT 18.S097。
題外話一下,筆者很喜歡從網路上找各式各樣的 CS 課程,而且內容都非常不錯,未來有機會在分享給大家。
感謝大家閱讀!!!
Maybe Monad