完整年份值必须介于_上 | 完整解释 Monad 程序员范畴论入门
如果你接觸過函數(shù)式編程,你很可能遇到過 Monad 這個(gè)奇怪的名詞。由于各種神奇的原因,Monad 成了一個(gè)很難懂的概念。Douglas Crockford 曾轉(zhuǎn)述過這樣一句話來形容 Monad:
Once you understand Monad, you lose the ability to explain it to someone else.
這篇文章中,我會(huì)從使用場(chǎng)景出發(fā)來一步步推演出 Monad。然后,我會(huì)進(jìn)一步展示一些 Monad 的使用場(chǎng)景,并解釋一些我從 Haskell 翻譯成 JS 的 ADT (Algebraic Data Type)。最后,我會(huì)介紹 Monad 在范疇論中的意義,并簡(jiǎn)單介紹下范疇論。
函數(shù)組合
1. Monoid
假設(shè)你被一個(gè)奇怪的叢林部落抓住了,部落長(zhǎng)老知道你是程序員,要你寫個(gè)應(yīng)用,寫出來就放你走。作為一個(gè)資深碼農(nóng),你暗自竊喜,心里想著老夫經(jīng)歷了這么多年產(chǎn)品經(jīng)理各種變態(tài)需求的千錘百煉,沒什么需求能難倒我!長(zhǎng)老似乎看出了你的心思,加了一個(gè)要求:這個(gè)應(yīng)用只能用純函數(shù)寫,不能有狀態(tài)機(jī),不能有副作用!然后你崩潰了……
再假設(shè)你不知道函數(shù)式編程,但你足夠聰明,你可能會(huì)發(fā)明出一個(gè)函數(shù)來滿足這個(gè)奇葩的要求。這個(gè)函數(shù)如此強(qiáng)大,你可能會(huì)叫它超級(jí)函數(shù),但其實(shí)它無可避免就是一個(gè) Monad。
接下來我們就來一步步推演出這個(gè)超級(jí)函數(shù)吧。
函數(shù)組合大家都應(yīng)該非常熟悉。比如,Redux 里面在組合中間件的時(shí)候會(huì)用到一個(gè) compose 函數(shù) compose(middleware1,middleware2)。函數(shù)組合的意思就是,在若干個(gè)函數(shù)中,依順序把前一個(gè)函數(shù)執(zhí)行的結(jié)果傳個(gè)下一個(gè)函數(shù),逐次執(zhí)行完。 compose 函數(shù)的簡(jiǎn)單實(shí)現(xiàn)如下:
const compose = (...fns) => fns.reduce((f, g) => (...args) => f(g(...args)))
函數(shù)組合是個(gè)很強(qiáng)大的思想。我們可以利用它把復(fù)雜問題拆解成簡(jiǎn)單問題,把這些簡(jiǎn)單問題逐個(gè)解決了之后,再把這些解決方案組合起來,就形成了最終的解決方案。
這里偷個(gè)懶再舉一下我之前文章的例子吧:
// 在產(chǎn)品列表中找到相應(yīng)產(chǎn)品,提取出價(jià)格,再把價(jià)格格式化
const formalizeData = compose(formatCurrency, pluckPrice, findProduct);
formalizeData(products)
如果你理解了上面的代碼,那么恭喜你,你已經(jīng)懂了 Monoid!
所謂 Monoid 可以簡(jiǎn)單定義如下:
它是一個(gè)集合 S
S 的元素之間有一個(gè)二元運(yùn)算 x,運(yùn)算的結(jié)果也屬于 S:S a x S b --> S c
存在一個(gè)特殊元素 e,使得 S 中的任意元素與 e 運(yùn)算,都返回此元素本身:S e x S m --> S m
同時(shí),這個(gè)二元運(yùn)算要滿足這些條件:
結(jié)合律:(a x b) x c = a x (b x c), a,b,c 為 S 中元素
單元律:e x a = a x e = a,e 為特殊元素,a 為 S 中任意元素
注意,上面這個(gè)定義是集合論中的定義,這里還沒涉及到范疇論。
函數(shù)要能組合,類型簽名必須一致。如果前一個(gè)函數(shù)返回一個(gè)數(shù)字,后一個(gè)函數(shù)接受的是字符串,那么是沒辦法組合的。所以, compose 函數(shù)接受的函數(shù)都符合如下函數(shù)簽名:fn :: a -> a 也就是說函數(shù)接受的參數(shù)和返回的值類型一樣。滿足這些類型簽名的函數(shù)就組成了 Monoid,而這個(gè) Monoid 中的特殊元素就是 identity 函數(shù): constidentity=x=>x; 結(jié)合律和單元律的證明比較簡(jiǎn)單,我就不演示了。
2. Functor
上面演示的函數(shù)組合看起來很舒服,但是實(shí)際用處還不是很大。因?yàn)?compose 接受的函數(shù)都是純函數(shù),只適合用來計(jì)算。而現(xiàn)實(shí)世界沒有那么純潔,我們要處理 IO,邏輯分支,異常捕獲,狀態(tài)管理等等。單靠簡(jiǎn)單的純函數(shù)組合是不行的。
先假設(shè)我們有兩個(gè)純函數(shù):
const addOne = x => x + 1
const multiplyByTwo = x => 2 * x
理想狀態(tài)下是我們可以組合這兩個(gè)函數(shù):
compose(
?addOne,
?multiplyByTwo
)(2) // => 5
但是我們出于各種原因要執(zhí)行一些副作用。這里僅為了演示,就簡(jiǎn)單化了。假設(shè)上面兩個(gè)函數(shù)在返回值之前還向控制臺(tái)打印了內(nèi)容:
const impureAddOne = x => {
?console.log('add one!')
?return x + 1
}
const impureMultiplyByTwo = x => {
?console.log('multiply by two!')
?return 2 * x
}
現(xiàn)在這兩個(gè)函數(shù)不再純潔了,我們看不順眼了。怎樣讓他們恢復(fù)純潔?很簡(jiǎn)單,作弊偷個(gè)懶:
const lazyImpureAddOne = x => () => {
?console.log('add one!')
?return x + 1
}
// Java 代碼看多了之后我也學(xué)會(huì)取長(zhǎng)變量名了^_^
const lazyImpureMultiplyByTwo = x => () => {
?console.log('multiply by two!')
?return 2 * x
}
修改之后的函數(shù),提供同樣的參數(shù),每次執(zhí)行他們都返回同樣的函數(shù),可以做到引用透明。這就叫純潔啊!
然后我們可以這樣組合這兩個(gè)偷懶函數(shù):
composeImpure = (f, g) => x => () => f(g(x)())()
const computation = composeImpure(lazyImpureAddOne, lazyImpureMultiplyByTwo)(8)
computation() // => 'multiply by two!''add one!' 17
在執(zhí)行 computation 之前,我們都在寫純函數(shù)。
我知道,我知道,上面的寫法可讀性很差。這樣子寫也不可維護(hù)。我們來寫個(gè)工具函數(shù)方便我們組合這些不純潔的函數(shù):
const Effect = f => ({
?map: g => Effect(x => g(f(x))),
?runWith: x => f(x),
})
這個(gè) Effect 函數(shù)接受一個(gè)非純函數(shù) f 為參數(shù),返回一個(gè)對(duì)象。這個(gè)對(duì)象里面的 map 方法把自身接受的非純回調(diào)函數(shù) g 和 Effect 的非純回調(diào)函數(shù)組合后,將結(jié)果再塞回給 Effect。由于 map 返回的也是對(duì)象,我們需要一個(gè)方法把最終的計(jì)算結(jié)果取出來,這就是 runWith 的作用。
用 Effect 重現(xiàn)我們上一步的計(jì)算如下:
Effect(impureAddOne)
?.map(impureMultiplyByTwo)
?.runWith(2) // => 'multiply by two!''add one!' 6
現(xiàn)在我們就可以直接用非純函數(shù)了,不用再用那么難讀的函數(shù)調(diào)用了。在執(zhí)行 runWith 之前,程序都是純的,任你怎么組合和 map。
如果你懂了上面的代碼,那么恭喜你,你已經(jīng)懂了 Functor!
同樣,Functor 還要滿足一些條件:
單元律:a.map(x => x) === a
保存原有數(shù)據(jù)結(jié)構(gòu)(可組合):a.map(x => f(g(x))) === a.map(g).map(f)
提供接口往里面塞值:Effect.of = value => Effect(() => value)
你可以把 Functor 理解成一個(gè)映射函數(shù),它把一個(gè)類型里的值映射到同一個(gè)類型的其它值。比如數(shù)組操作 [1, 2, 3].map(String) // -> ['1', '2', '3'], 映射之后數(shù)據(jù)類型一樣(還是數(shù)組),內(nèi)部結(jié)構(gòu)不變。我在之前的文章中說數(shù)組就是個(gè) Functor,這種表述是有誤的,應(yīng)該是說數(shù)組滿足 Functor 的返回值條件。
3. Applicative
上面的 Effect 函數(shù)把非純操作都放進(jìn)了一個(gè)容器里面,這樣子做了之后,如果要對(duì)兩個(gè)獨(dú)立非純操作的結(jié)果進(jìn)行運(yùn)算,就會(huì)很麻煩。
比如,我們?cè)?window 全局讀取兩個(gè)值 x, y, 并將讀取結(jié)果求和。我知道這個(gè)例子很簡(jiǎn)單,不用函數(shù)式編程很容易做到,我只是在舉簡(jiǎn)單例子方便理解。
假設(shè) window 對(duì)象已經(jīng)存在兩個(gè)值 {x: 1, y: 2, ...otherProps}。我們這樣取:
const win = Effect.of(window)
const xFromWindow = win.map(g => g.x)
const yFromWindow = win.map(g => g.y)
xFromWindow 和 yFromWindow 返回的都是一個(gè) Effect 容器,我們需要給這個(gè)容器新添加一個(gè)方法,以便將兩個(gè)容器里層的值進(jìn)行計(jì)算。
const Effect = f => ({
?map: g => Effect(x => g(f(x))),
?runWith: x => f(x),
?ap: other => Effect(x => other.map(f(x)).runWith()),
})
然后,我們提供一個(gè)相加函數(shù) add:
const add = x => y => x + y
接下來借助這個(gè) ap 函數(shù),我們可以進(jìn)行計(jì)算了:
xFromWindow
?.map(add)
?.ap(yFromWindow)
?.runWith() // => 3
由于這種先 map 再 ap 的操作很普遍,我們可以抽象出一個(gè)工具函數(shù) liftA2:
const liftA2 = (f, m1, m2) => m1.map(f).ap(m2)
然后可以簡(jiǎn)化點(diǎn)寫了:
liftA2(add, xFromWindow, yFromWindow) // => 3;
注意運(yùn)算函數(shù)必須是柯里化函數(shù)。
新增 ap 方法之后的 Effect 函數(shù)除了是 Functor,還是 Applicative Functor。這部分完全看代碼還不是很好懂。如果你不理解上面的代碼,沒有關(guān)系,它并不影響你理解 Monad。另外,不用糾結(jié)于本文代碼里的具體實(shí)現(xiàn)。不同的 Applicative 的 ap 方法實(shí)現(xiàn)都不一樣,可以多看幾個(gè)。Applicative 是介于 Functor 和 Monad 之間的數(shù)據(jù)類型,不提它就不完整了。
Applicative 要滿足下面這些條件:
Identity: A.of(x => x).ap(v) === v
Homomorphism: A.of(f).ap(A.of(x)) === A.of(f(x))
Interchange: u.ap(A.of(y)) === A.of(f => f(y)).ap(u)
4. Monad (!!!)
假設(shè)我們要從 window 全局讀取配置信息,此配置信息提供目標(biāo) DOM 節(jié)點(diǎn)的類名 userEl;根據(jù)這個(gè)類名,我們定位到 DOM 節(jié)點(diǎn),取出內(nèi)容,然后打印到控制臺(tái)。啊,讀取全局對(duì)象,讀取 DOM,控制臺(tái)輸出,全是作用,好可怕…… 我們先用之前定義的 Effect 試試看行不行:
// DOM 讀取的行為放進(jìn) Effect
const $ = s => Effect(() => document.querySelector(s))
Effect.of(window)
?.map(win => win.userEl)
?.map($)
?.runWith() //由于上一個(gè) map 里層也返回了 Effect,這里需要抹平一層
?.map(e => console.log(e.innerHTML))
?.runWith()
勉強(qiáng)能做到,但是這樣子先 map 再 runWith 實(shí)在太繁瑣了,我們可以再給 Effect 新增一個(gè)方法 chain:
const Effect = f => ({
?map: g => Effect(x => g(f(x))),
?runWith: x => f(x),
?ap: other => Effect(x => other.map(f(x)).runWith()),
?chain: g =>
? ?Effect(f)
? ? ?.map(g)
? ? ?.runWith(),
})
Voila! 我們發(fā)現(xiàn)了 Monad!
在寫上面的代碼的時(shí)候我還是覺得逐行解釋代碼比較繁瑣。我們先不管代碼具體實(shí)現(xiàn),從函數(shù)簽名開始看 Monad 是怎么回事。
讓我們回到 Monoid。我們知道函數(shù)組合的前提條件是類型簽名一致。fn :: a -> a. 但在寫應(yīng)用時(shí),我們會(huì)讓函數(shù)除了返回值之外還干其他事。這里不管具體干了哪些事,我們可以把這些行為扔到一個(gè)黑盒子里(比如剛剛寫的 Effect),然后函數(shù)簽名就成了 fn :: a -> m a。m 指的是黑盒子的類型,m a 意思是黑盒子里的 a. 這樣操作之后,Monoid 接口不再滿足,函數(shù)不能簡(jiǎn)單組合。
但我們還是要組合。
其實(shí)很簡(jiǎn)單,在組合之前把黑盒子里的值提升一層就行了。最終我們實(shí)現(xiàn)的組合其實(shí)是這樣:fn :: m a -> (a -> m b) -> m b. 這個(gè)簽名里,函數(shù) fn 接受黑盒子里的 a 為參數(shù),再接受一個(gè)函數(shù)為參數(shù),這個(gè)函數(shù)的入?yún)㈩愋褪?a,返回類型是黑盒子里的 b。最終,外層函數(shù)返回的類型是黑盒子里的 b。這個(gè)就是 chain 函數(shù)的類型簽名。
fn :: a -> m a 簽名里面的箭頭叫 Kleisli Arrow,其實(shí)就是一種特殊的函數(shù)。Kleisli 箭頭的組合叫 Kleisli Composition,這也是 Ramda 里面 composeK 函數(shù)的來源。這里先了解一下,等下還會(huì)用到這個(gè)概念。
Monad 要滿足的一些定律如下:
Left identity: M.of(a).chain(f) === f(a)
Right identity: m.chain(M.of) === m
Associativity: m.chain(f).chain(g) === m.chain(x => f(x).chain(g))
很多人誤解 JS 里面的 Promise 就是個(gè) Monad,我之前也有這樣的誤解,但后來想明白了。按照上面的定律來看檢查 Promise:
Left identity:
Promise.resolve(a).then(f) === f(a)
看起來滿足。但是如果 a 是個(gè) Promise 呢?要處理 Promise,那 f 應(yīng)該符合符合這個(gè)函數(shù)的類型簽名:
const f = p => p.then(n => n * 2)
來試一下:
const a = Promise.resolve(1)
const output = Promise.resolve(a).then(f)
// output :: RejectedPromise TypeError: p.then is not a function
報(bào)錯(cuò)的原因是,a 在傳給 f 之前,就已經(jīng)被 resolve 掉了。
Right identity:
p.then(x => Promise.resolve(x)) === p
滿足。
Associativity:
p.then(f).then(g) === p.then(x => f(x).then(g))
和左單元律一樣,只有當(dāng) f 和 g 接受的參數(shù)不為 Promise,上面才成立。
所以,Monad 的三個(gè)條件,Promise 只符合一條。
更多 ADT
上面演示的 Effect 函數(shù),和我之前文章《不完整解釋 Monad 有什么用》 里面演示的 IO 函數(shù)是同一個(gè) ADT,它是用來處理程序中的作用的。函數(shù)式編程中還有很多不同用處的 ADT,比如,處理異步的 Future,處理狀態(tài)管理的 State,處理依賴注入的 Reader 等。關(guān)于為什么這個(gè) Monad 是代數(shù)數(shù)據(jù)類型,Monad 和大家熟知的代數(shù)有什么關(guān)系,這里不展開了,有興趣進(jìn)一步了解的話可以參考 Category Theory for Programmers 這本書。
這里再展示兩個(gè) ADT,Reader 和 State,比較它們 chain 和 ap 的不同實(shí)現(xiàn),對(duì)比 Monadic bind 函數(shù)類型簽名 chain :: m a -> (a -> m b) -> m b,思考下它們是怎樣實(shí)現(xiàn) Monad 的。
1. Reader
const Reader = computation => {
?const map = f => Reader(ctx => f(computation(ctx)))
?const contramap = f => Reader(ctx => computation(f(ctx)))
?const ap = other => Reader(ctx => computation(ctx)(other.runReader(ctx)))
?const chain = f => {
? ?return Reader(ctx => {
? ? ?const a = computation(ctx)
? ? ?return f(a).runReader(ctx)
? ?})
?}
?const runWith = computation
?return Object.freeze({
? ?map,
? ?contramap,
? ?ap,
? ?chain,
? ?runWith,
?})
}
Reader.of = x => Reader(() => x)
題外話補(bǔ)充下,上面這種叫“冰凍工廠”的工廠函數(shù)寫法,是我個(gè)人偏好。這樣寫會(huì)有一定性能和內(nèi)存消耗問題。用 Class 性能更好,看你選擇。
程序中可能會(huì)遇到某個(gè)函數(shù)對(duì)外部環(huán)境有依賴。用純函數(shù)的寫法,我們可以把這個(gè)依賴同時(shí)傳進(jìn)函數(shù)。這樣子,函數(shù)簽名就是 fn :: (a, e) -> b。e 代表外部環(huán)境。這個(gè)簽名不符合我們前面提到的 a -> m b. 我們到現(xiàn)在還只提到了一次函數(shù)柯里化,這個(gè)時(shí)候再一次要用柯里化了。柯里化后,有依賴的函數(shù)類型簽名是 fn :: a -> (e, b), 你可能認(rèn)出來了,中間那個(gè)箭頭就是 Kleisli Arrow。
假設(shè)我們有一段程序的多個(gè)模塊依賴了共同的外部環(huán)境。要做到引用透明,我們必須把這個(gè)環(huán)境傳進(jìn)函數(shù)。但是每一個(gè)模塊如果都接受外部環(huán)境為多余參數(shù),那這些模塊是沒辦法組合的。Reader 幫我們解決這個(gè)問題。
來寫個(gè)簡(jiǎn)單程序,執(zhí)行這個(gè)程序時(shí)輸出“你好,xx ... 再見,xx”。xx 由執(zhí)行時(shí)的參數(shù)決定。
const concat = x => y => y.concat.call(y, x)
const greet = greeting => Reader(name => `${greeting}, ${name}`)
const addFarewell = farewell => str =>
?Reader(name => `${str}${farewell}, ${name}`)
const buildSentence = greet('你好')
?.map(concat('...'))
?.chain(addFarewell('再見'))
buildSentence.runWith('張三')
// => 你好, 張三...再見, 張三
上面這個(gè)例子過于簡(jiǎn)單。輸出一個(gè)字符串用一個(gè)函數(shù)就行,用不了解構(gòu)和組合。但是,我們可以很容易擴(kuò)展想象,如果 greet 和 addFarewell 是很復(fù)雜的模塊,必須拆分,此時(shí)組合的價(jià)值就出現(xiàn)了。
在學(xué)習(xí) Reader 時(shí),我發(fā)現(xiàn)一篇很不錯(cuò)的文章。這篇文章大開腦洞,用 Reader 實(shí)現(xiàn) React 里面的 Context。有興趣可以了解下。The Reader monad and read-only context
2. State
// 這個(gè)寫法你可能不習(xí)慣。
// 這是 K Combinator,Ramda 里面對(duì)應(yīng)函數(shù)是 always, Haskell 里面是 const
const K = x => y => x
const State = computation => {
?const map = f =>
? ?State(state => {
? ? ?const prev = computation(state)
? ? ?return { value: f(prev.value), state: prev.state }
? ?})
?const ap = other =>
? ?State(state => {
? ? ?const prev = computation(state)
? ? ?const fn = prev.value
? ? ?return other.map(fn).runWith(prev.state)
? ?})
?const chain = fn =>
? ?State(state => {
? ? ?const prev = computation(state)
? ? ?const next = fn(prev.value)
? ? ?return next.runWith(prev.state)
? ?})
?const runWith = computation
?const evalWith = initState => computation(initState).value
?const execWith = initState => computation(initState).state
?return Object.freeze({
? ?map,
? ?ap,
? ?chain,
? ?evalWith,
? ?runWith,
? ?execWith,
?})
}
const modify = f => State(state => ({ value: undefined, state: f(state) }))
State.get = (f = x => x) => State(state => ({ value: f(state), state }))
State.modify = modify
State.put = state => modify(K(state))
State.of = value => State(state => ({ value, state }))
State 里層最終返回的值由對(duì)象構(gòu)成,對(duì)象里面包含了此時(shí)計(jì)算結(jié)果,以及當(dāng)前的應(yīng)用狀態(tài)。
再舉個(gè)簡(jiǎn)單的例子。假設(shè)我們根據(jù)某狀態(tài)數(shù)字進(jìn)行計(jì)算,首先我們?cè)谶@個(gè)初始狀態(tài)上加某個(gè)數(shù)字,然后我們把狀態(tài) + 1, 再把新的狀態(tài)和前一步的計(jì)算相乘,算出最終結(jié)果。同樣,例子很簡(jiǎn)單,但已經(jīng)包含了狀態(tài)管理的核心。來看代碼:
const add = x => y => x + y
const inc = add(1)
const addBy = n => State.get(add(n))
const multiplyBy = a => State.get(b => b * a)
const incState = n => modify(inc).map(K(n))
addBy(10)
?.chain(incState)
?.chain(multiplyBy)
?.runWith(2) // => {value: 36, state: 3}
上面最后一步組合,每個(gè)函數(shù)類型簽名一致,a -> m b, 構(gòu)成 kleisli 組合,我們還可以用工具函數(shù)改進(jìn)一下寫法:
const composeK = (...fns) =>
?fns.reduce((f, g) => (...args) => g(...args).chain(f))
const calculate = composeK(
?multiplyBy,
?incState,
?addBy
)
calculate(10).runWith(2) // => {value: 36, state: 3}
范疇論介紹
Monad 有一個(gè)“臭名昭著”的定義,是這樣:
A monad is just a monoid in the category of endofunctors, what's the problem?
我見過這句話的中文翻譯。但是這種“鬼話”不管翻不翻譯都差不多的表達(dá)效果,我覺得還是不用翻譯了。很多人看到這句話不去查出處和上下文,就以此為據(jù)來批評(píng) FP 社區(qū)故弄玄虛,我感到很無奈。
這句話出自這篇文章 Brief, Incomplete and Mostly Wrong History of Programming Languages. 這篇文章用戲謔調(diào)侃的方式把所有主流編程語(yǔ)言黑了一個(gè)遍。上面那句話是用來黑 Haskell 的。本來是句玩笑,結(jié)果就以訛傳訛了。
上面那句話的原始出處是范疇論的奠基之作 Categories for the Working Mathematician 原話更拗口:
All told, a monad in X is just a monoid in the category of endofunctors of X, with product × replaced by composition of endofunctors and unit set by the identity endofunctor.
注意書名,那是給數(shù)學(xué)家看的,不是給程序員看的。你看不懂很正常,看不懂還要罵這些學(xué)術(shù)泰斗裝逼就是你的不對(duì)了。
范疇論背景
首先,說明下我數(shù)學(xué)學(xué)得差,我接下來要講的名詞我知道是在研究什么,再深入細(xì)節(jié)我就不知道了。
大家知道數(shù)學(xué)有很多分支,比如集合論,邏輯學(xué),類型論(有這個(gè)嗎?Type Theory) 等等。后來,有些數(shù)學(xué)家發(fā)現(xiàn),如果用足夠抽象的概念工具去考察這些分支,其實(shí)他們都在講同樣的東西。橋接這些概念的工具是 isomorphism (同構(gòu))。isomorphic 就是在對(duì)象之間可以來回轉(zhuǎn)換,每次轉(zhuǎn)換沒有信息丟失。比如,在邏輯學(xué)里面研究的某個(gè)問題,可能和類型論里面研究是同一個(gè)問題,只要兩者之間能形成 isomorphism。
再后來,FP 祖師爺之一 Haskell Curry,和另一個(gè)數(shù)學(xué)家一起發(fā)現(xiàn)了 Curry–Howard Isomorphism。這個(gè)理論證明了 proofs as programs, 就是說寫電腦程序(當(dāng)然是函數(shù)式)和寫邏輯證明是一回事,兩者形成同構(gòu)。再后來,這個(gè)理論被擴(kuò)展了一下,成了 Curry–Howard-Lambek Isomorphism, 就是說邏輯學(xué),程序函數(shù),和范疇論,三者之間形成同構(gòu)。
看了上面的理論背景,你應(yīng)該明白了為什么函數(shù)式編程要從范疇論里面獲取理論資源。
什么是范疇 (Category)
范疇其實(shí)是很簡(jiǎn)單的一個(gè)概念。范疇由一堆(這個(gè)量詞好難翻譯,我見過 a bunch, a collection, 但是不能說 a set)對(duì)象,以及對(duì)象之間的關(guān)系構(gòu)成。我分兩部分介紹。
對(duì)象 (Object): 范疇論里面的對(duì)象和編程里面的對(duì)象是兩回事。范疇中的對(duì)象沒有屬性,沒有結(jié)構(gòu),你可以把它理解為不可描述的點(diǎn)。
箭頭 (arrow, morphism, 兩個(gè)詞說的是同一個(gè)東西, 我后面就用箭頭了): 連接對(duì)象,表示對(duì)象之間的關(guān)系。同樣,箭頭也是一個(gè)沒有結(jié)構(gòu)沒有屬性的一種 primitive。它只說明了對(duì)象之間存在關(guān)系,并不能說明是什么關(guān)系。
對(duì)象和箭頭要構(gòu)成一個(gè)范疇,還要滿足這兩個(gè)條件:
單元律。每個(gè)對(duì)象至少有一個(gè)箭頭能從自己出發(fā)回到自身。
結(jié)合律。如果對(duì)象 a 和 b 之間存在箭頭 f,對(duì)象 b 和 c 之間存在箭頭 g,則必然存在箭頭 h 由 a 到 c,h 就是 f 和 g 的組合。
可以看出范疇論的起點(diǎn)真的非常簡(jiǎn)單。很難想象基于這么簡(jiǎn)單的概念能構(gòu)建出一個(gè)完整的數(shù)學(xué)理論。
我一開始試著在范疇論中來解釋 Monad,以失敗告終。要介紹的拗口名詞太多了,一篇文章根本講不完。所以本文會(huì)折中一下,還是用集合論的視角來解釋一下范疇論概念。(范疇論的單個(gè)對(duì)象可以對(duì)應(yīng)成一個(gè)集合,但是范疇論禁止談?wù)摷显?#xff0c;所有關(guān)于對(duì)象的知識(shí)都由箭頭和組合推理出來,所以很頭疼。)
還記得我們是用集合來定義 Monoid 的吧?Monoid 其實(shí)就是一個(gè)只有一個(gè)對(duì)象的范疇。對(duì)象和對(duì)象之間的映射叫 Functor。如果一個(gè) Functor 把對(duì)象映射回自身,那么這個(gè) Functor 就叫 Endofunctor。Functor 和 Functor 之間的映射叫 Natural Transformation. 函數(shù)式編程其實(shí)只處理一個(gè)對(duì)象,就是數(shù)據(jù)類型(Types)。所以,我們前面提到的 Functor 也是 Endofunctor。
回到前面 Monad 中 chain 的類型簽名:
chain :: m a -> (a -> m b) -> m b
可以看出 Monad 是把一個(gè)類型映射回自身(m a -> m b),那么它就是一個(gè) Endofunctor。
再看看 Monad 中所運(yùn)用的 Natural Transformation。還是看 chain 的簽名,前半部分 m a -> (a -> m b) 執(zhí)行之后,類型簽名是 m (m b), 然后再和后面的連起來,就是 m (m b) -> m b. 這其實(shí)就是把一個(gè) functor (m (m b)) 映射到另一個(gè) Functor (m b)。m (m b) -> m b 看起來是不是很眼熟?一個(gè) Functor 和自己組合,形成同一個(gè)范疇里的 Functor,這就是 Monoid 啊!我們一開始定義的 Monoid 中的二元運(yùn)算,在 Monad 中其實(shí)就是 Natural Transformation。
那么,再回到這一部分開始時(shí)的定義:
A monad is just a monoid in the category of endofunctors.
有沒有好理解一點(diǎn)?
為什么要這樣寫程序
這篇文章的目的不是鼓勵(lì)你在你的代碼中消滅狀態(tài)機(jī),消滅副作用,我自己都做不到的。我司后端是用 Java 寫的,如果我告訴后端同事 “Yo,你的程序里不能出現(xiàn)狀態(tài)機(jī)哦……”,怕是會(huì)被哄出辦公室的。那么,為什么要了解這些知識(shí)?
計(jì)算機(jī)科學(xué)中有兩條截然相反的路徑。一條是自下而上,從底層指令開始往上抽象(優(yōu)先考慮性能),逐漸靠近數(shù)學(xué)。比如,一開始的 Unix 操作系統(tǒng)是用匯編寫的,后來發(fā)現(xiàn)用匯編寫程序太痛苦了,需要一些抽象,所以出現(xiàn)了高級(jí)語(yǔ)言 C,再后來由于各種編寫應(yīng)用的需求,出現(xiàn)了更高級(jí)的語(yǔ)言如 Python 和 JavaScript。另一條路徑是自上而下的,直接從數(shù)學(xué)開始(Lambda 演算),不考慮性能和硬件狀況,按需逐漸減少抽象。前一條路徑明顯占了主流,代表語(yǔ)言是 Fortran, C, C++, Pascal, 和 Java 等。后面一條路徑不夠?qū)嵱?#xff0c;比較小眾,代表語(yǔ)言是 Algo, LISP, Small Talk 和 Haskell 等。
這兩個(gè)陣營(yíng)肯定是由爭(zhēng)論的。前者想勸后者從良:你別扔給我這么多函數(shù),我沒法不影響性能情況下處理那么多垃圾回收和函數(shù)調(diào)用!后者也想叫醒前者:不要過早深入硬件細(xì)節(jié),你會(huì)把自己鎖定在無法逆轉(zhuǎn)的設(shè)計(jì)錯(cuò)誤上!兩者分道揚(yáng)鑣了 60 多年,這些年總算開始融合了。比如,新出現(xiàn)的程序語(yǔ)言如 Scala,Kotlin,甚至系統(tǒng)編程語(yǔ)言 Rust,都大量借鑒了函數(shù)式編程的思想。
學(xué)些高階抽象還能幫助你更容易理解一些看起來很復(fù)雜的概念。轉(zhuǎn)述一個(gè)例子。C++ 編程里面最高的抽象是模板元編程(Template Meta Programming),據(jù)說很難懂。但是據(jù) Bartosz Milewski 的解釋,之所以這個(gè)概念難懂,是因?yàn)?C++ 的語(yǔ)言設(shè)計(jì)不適合表達(dá)這些抽象。如果你會(huì) Haskell,就會(huì)發(fā)現(xiàn)其實(shí)一行代碼就完成了。
參考:
Brian Beckman: Don't fear the Monad
What Does Haskell Have to Do with C++?
HOW TO DEAL WITH DIRTY SIDE EFFECTS IN YOUR PURE FUNCTIONAL JAVASCRIPT
Category Theory for Programmers
總結(jié)
以上是生活随笔為你收集整理的完整年份值必须介于_上 | 完整解释 Monad 程序员范畴论入门的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 中柏平板触摸驱动_工业平板电脑触摸屏种类
- 下一篇: adc的使用屏幕上显示单位v。显示结果精