日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

完整年份值必须介于_上 | 完整解释 Monad 程序员范畴论入门

發布時間:2025/10/17 编程问答 13 豆豆
生活随笔 收集整理的這篇文章主要介紹了 完整年份值必须介于_上 | 完整解释 Monad 程序员范畴论入门 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

如果你接觸過函數式編程,你很可能遇到過 Monad 這個奇怪的名詞。由于各種神奇的原因,Monad 成了一個很難懂的概念。Douglas Crockford 曾轉述過這樣一句話來形容 Monad:

Once you understand Monad, you lose the ability to explain it to someone else.

這篇文章中,我會從使用場景出發來一步步推演出 Monad。然后,我會進一步展示一些 Monad 的使用場景,并解釋一些我從 Haskell 翻譯成 JS 的 ADT (Algebraic Data Type)。最后,我會介紹 Monad 在范疇論中的意義,并簡單介紹下范疇論。

函數組合

1. Monoid

假設你被一個奇怪的叢林部落抓住了,部落長老知道你是程序員,要你寫個應用,寫出來就放你走。作為一個資深碼農,你暗自竊喜,心里想著老夫經歷了這么多年產品經理各種變態需求的千錘百煉,沒什么需求能難倒我!長老似乎看出了你的心思,加了一個要求:這個應用只能用純函數寫,不能有狀態機,不能有副作用!然后你崩潰了……

再假設你不知道函數式編程,但你足夠聰明,你可能會發明出一個函數來滿足這個奇葩的要求。這個函數如此強大,你可能會叫它超級函數,但其實它無可避免就是一個 Monad。

接下來我們就來一步步推演出這個超級函數吧。

函數組合大家都應該非常熟悉。比如,Redux 里面在組合中間件的時候會用到一個 compose 函數 compose(middleware1,middleware2)。函數組合的意思就是,在若干個函數中,依順序把前一個函數執行的結果傳個下一個函數,逐次執行完。 compose 函數的簡單實現如下:

const compose = (...fns) => fns.reduce((f, g) => (...args) => f(g(...args)))

函數組合是個很強大的思想。我們可以利用它把復雜問題拆解成簡單問題,把這些簡單問題逐個解決了之后,再把這些解決方案組合起來,就形成了最終的解決方案。

這里偷個懶再舉一下我之前文章的例子吧:

// 在產品列表中找到相應產品,提取出價格,再把價格格式化

const formalizeData = compose(formatCurrency, pluckPrice, findProduct);

formalizeData(products)

如果你理解了上面的代碼,那么恭喜你,你已經懂了 Monoid!

所謂 Monoid 可以簡單定義如下:

  • 它是一個集合 S

  • S 的元素之間有一個二元運算 x,運算的結果也屬于 S:S a x S b --> S c

  • 存在一個特殊元素 e,使得 S 中的任意元素與 e 運算,都返回此元素本身:S e x S m --> S m

同時,這個二元運算要滿足這些條件:

  • 結合律:(a x b) x c = a x (b x c), a,b,c 為 S 中元素

  • 單元律:e x a = a x e = a,e 為特殊元素,a 為 S 中任意元素

注意,上面這個定義是集合論中的定義,這里還沒涉及到范疇論。

函數要能組合,類型簽名必須一致。如果前一個函數返回一個數字,后一個函數接受的是字符串,那么是沒辦法組合的。所以, compose 函數接受的函數都符合如下函數簽名:fn :: a -> a 也就是說函數接受的參數和返回的值類型一樣。滿足這些類型簽名的函數就組成了 Monoid,而這個 Monoid 中的特殊元素就是 identity 函數: constidentity=x=>x; 結合律和單元律的證明比較簡單,我就不演示了。

2. Functor

上面演示的函數組合看起來很舒服,但是實際用處還不是很大。因為 compose 接受的函數都是純函數,只適合用來計算。而現實世界沒有那么純潔,我們要處理 IO,邏輯分支,異常捕獲,狀態管理等等。單靠簡單的純函數組合是不行的。

先假設我們有兩個純函數:

const addOne = x => x + 1

const multiplyByTwo = x => 2 * x

理想狀態下是我們可以組合這兩個函數:

compose(

?addOne,

?multiplyByTwo

)(2) // => 5

但是我們出于各種原因要執行一些副作用。這里僅為了演示,就簡單化了。假設上面兩個函數在返回值之前還向控制臺打印了內容:

const impureAddOne = x => {

?console.log('add one!')

?return x + 1

}

const impureMultiplyByTwo = x => {

?console.log('multiply by two!')

?return 2 * x

}

現在這兩個函數不再純潔了,我們看不順眼了。怎樣讓他們恢復純潔?很簡單,作弊偷個懶:

const lazyImpureAddOne = x => () => {

?console.log('add one!')

?return x + 1

}

// Java 代碼看多了之后我也學會取長變量名了^_^

const lazyImpureMultiplyByTwo = x => () => {

?console.log('multiply by two!')

?return 2 * x

}

修改之后的函數,提供同樣的參數,每次執行他們都返回同樣的函數,可以做到引用透明。這就叫純潔啊!

然后我們可以這樣組合這兩個偷懶函數:

composeImpure = (f, g) => x => () => f(g(x)())()

const computation = composeImpure(lazyImpureAddOne, lazyImpureMultiplyByTwo)(8)

computation() // => 'multiply by two!''add one!' 17

在執行 computation 之前,我們都在寫純函數。

我知道,我知道,上面的寫法可讀性很差。這樣子寫也不可維護。我們來寫個工具函數方便我們組合這些不純潔的函數:

const Effect = f => ({

?map: g => Effect(x => g(f(x))),

?runWith: x => f(x),

})

這個 Effect 函數接受一個非純函數 f 為參數,返回一個對象。這個對象里面的 map 方法把自身接受的非純回調函數 g 和 Effect 的非純回調函數組合后,將結果再塞回給 Effect。由于 map 返回的也是對象,我們需要一個方法把最終的計算結果取出來,這就是 runWith 的作用。

用 Effect 重現我們上一步的計算如下:

Effect(impureAddOne)

?.map(impureMultiplyByTwo)

?.runWith(2) // => 'multiply by two!''add one!' 6

現在我們就可以直接用非純函數了,不用再用那么難讀的函數調用了。在執行 runWith 之前,程序都是純的,任你怎么組合和 map。

如果你懂了上面的代碼,那么恭喜你,你已經懂了 Functor!

同樣,Functor 還要滿足一些條件:

  • 單元律:a.map(x => x) === a

  • 保存原有數據結構(可組合):a.map(x => f(g(x))) === a.map(g).map(f)

  • 提供接口往里面塞值:Effect.of = value => Effect(() => value)

你可以把 Functor 理解成一個映射函數,它把一個類型里的值映射到同一個類型的其它值。比如數組操作 [1, 2, 3].map(String) // -> ['1', '2', '3'], 映射之后數據類型一樣(還是數組),內部結構不變。我在之前的文章中說數組就是個 Functor,這種表述是有誤的,應該是說數組滿足 Functor 的返回值條件。

3. Applicative

上面的 Effect 函數把非純操作都放進了一個容器里面,這樣子做了之后,如果要對兩個獨立非純操作的結果進行運算,就會很麻煩。

比如,我們在 window 全局讀取兩個值 x, y, 并將讀取結果求和。我知道這個例子很簡單,不用函數式編程很容易做到,我只是在舉簡單例子方便理解。

假設 window 對象已經存在兩個值 {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 返回的都是一個 Effect 容器,我們需要給這個容器新添加一個方法,以便將兩個容器里層的值進行計算。

const Effect = f => ({

?map: g => Effect(x => g(f(x))),

?runWith: x => f(x),

?ap: other => Effect(x => other.map(f(x)).runWith()),

})

然后,我們提供一個相加函數 add:

const add = x => y => x + y

接下來借助這個 ap 函數,我們可以進行計算了:

xFromWindow

?.map(add)

?.ap(yFromWindow)

?.runWith() // => 3

由于這種先 map 再 ap 的操作很普遍,我們可以抽象出一個工具函數 liftA2:

const liftA2 = (f, m1, m2) => m1.map(f).ap(m2)

然后可以簡化點寫了:

liftA2(add, xFromWindow, yFromWindow) // => 3;

注意運算函數必須是柯里化函數。

新增 ap 方法之后的 Effect 函數除了是 Functor,還是 Applicative Functor。這部分完全看代碼還不是很好懂。如果你不理解上面的代碼,沒有關系,它并不影響你理解 Monad。另外,不用糾結于本文代碼里的具體實現。不同的 Applicative 的 ap 方法實現都不一樣,可以多看幾個。Applicative 是介于 Functor 和 Monad 之間的數據類型,不提它就不完整了。

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 (!!!)

假設我們要從 window 全局讀取配置信息,此配置信息提供目標 DOM 節點的類名 userEl;根據這個類名,我們定位到 DOM 節點,取出內容,然后打印到控制臺。啊,讀取全局對象,讀取 DOM,控制臺輸出,全是作用,好可怕…… 我們先用之前定義的 Effect 試試看行不行:

// DOM 讀取的行為放進 Effect

const $ = s => Effect(() => document.querySelector(s))

Effect.of(window)

?.map(win => win.userEl)

?.map($)

?.runWith() //由于上一個 map 里層也返回了 Effect,這里需要抹平一層

?.map(e => console.log(e.innerHTML))

?.runWith()

勉強能做到,但是這樣子先 map 再 runWith 實在太繁瑣了,我們可以再給 Effect 新增一個方法 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! 我們發現了 Monad!

在寫上面的代碼的時候我還是覺得逐行解釋代碼比較繁瑣。我們先不管代碼具體實現,從函數簽名開始看 Monad 是怎么回事。

讓我們回到 Monoid。我們知道函數組合的前提條件是類型簽名一致。fn :: a -> a. 但在寫應用時,我們會讓函數除了返回值之外還干其他事。這里不管具體干了哪些事,我們可以把這些行為扔到一個黑盒子里(比如剛剛寫的 Effect),然后函數簽名就成了 fn :: a -> m a。m 指的是黑盒子的類型,m a 意思是黑盒子里的 a. 這樣操作之后,Monoid 接口不再滿足,函數不能簡單組合。

但我們還是要組合。

其實很簡單,在組合之前把黑盒子里的值提升一層就行了。最終我們實現的組合其實是這樣:fn :: m a -> (a -> m b) -> m b. 這個簽名里,函數 fn 接受黑盒子里的 a 為參數,再接受一個函數為參數,這個函數的入參類型是 a,返回類型是黑盒子里的 b。最終,外層函數返回的類型是黑盒子里的 b。這個就是 chain 函數的類型簽名。

fn :: a -> m a 簽名里面的箭頭叫 Kleisli Arrow,其實就是一種特殊的函數。Kleisli 箭頭的組合叫 Kleisli Composition,這也是 Ramda 里面 composeK 函數的來源。這里先了解一下,等下還會用到這個概念。

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 就是個 Monad,我之前也有這樣的誤解,但后來想明白了。按照上面的定律來看檢查 Promise:

Left identity:

Promise.resolve(a).then(f) === f(a)

看起來滿足。但是如果 a 是個 Promise 呢?要處理 Promise,那 f 應該符合符合這個函數的類型簽名:

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

報錯的原因是,a 在傳給 f 之前,就已經被 resolve 掉了。

Right identity:

p.then(x => Promise.resolve(x)) === p

滿足。

Associativity:

p.then(f).then(g) === p.then(x => f(x).then(g))

和左單元律一樣,只有當 f 和 g 接受的參數不為 Promise,上面才成立。

所以,Monad 的三個條件,Promise 只符合一條。

更多 ADT

上面演示的 Effect 函數,和我之前文章《不完整解釋 Monad 有什么用》 里面演示的 IO 函數是同一個 ADT,它是用來處理程序中的作用的。函數式編程中還有很多不同用處的 ADT,比如,處理異步的 Future,處理狀態管理的 State,處理依賴注入的 Reader 等。關于為什么這個 Monad 是代數數據類型,Monad 和大家熟知的代數有什么關系,這里不展開了,有興趣進一步了解的話可以參考 Category Theory for Programmers 這本書。

這里再展示兩個 ADT,Reader 和 State,比較它們 chain 和 ap 的不同實現,對比 Monadic bind 函數類型簽名 chain :: m a -> (a -> m b) -> m b,思考下它們是怎樣實現 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)

題外話補充下,上面這種叫“冰凍工廠”的工廠函數寫法,是我個人偏好。這樣寫會有一定性能和內存消耗問題。用 Class 性能更好,看你選擇。

程序中可能會遇到某個函數對外部環境有依賴。用純函數的寫法,我們可以把這個依賴同時傳進函數。這樣子,函數簽名就是 fn :: (a, e) -> b。e 代表外部環境。這個簽名不符合我們前面提到的 a -> m b. 我們到現在還只提到了一次函數柯里化,這個時候再一次要用柯里化了。柯里化后,有依賴的函數類型簽名是 fn :: a -> (e, b), 你可能認出來了,中間那個箭頭就是 Kleisli Arrow。

假設我們有一段程序的多個模塊依賴了共同的外部環境。要做到引用透明,我們必須把這個環境傳進函數。但是每一個模塊如果都接受外部環境為多余參數,那這些模塊是沒辦法組合的。Reader 幫我們解決這個問題。

來寫個簡單程序,執行這個程序時輸出“你好,xx ... 再見,xx”。xx 由執行時的參數決定。

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('張三')

// => 你好, 張三...再見, 張三

上面這個例子過于簡單。輸出一個字符串用一個函數就行,用不了解構和組合。但是,我們可以很容易擴展想象,如果 greet 和 addFarewell 是很復雜的模塊,必須拆分,此時組合的價值就出現了。

在學習 Reader 時,我發現一篇很不錯的文章。這篇文章大開腦洞,用 Reader 實現 React 里面的 Context。有興趣可以了解下。The Reader monad and read-only context

2. State

// 這個寫法你可能不習慣。

// 這是 K Combinator,Ramda 里面對應函數是 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 里層最終返回的值由對象構成,對象里面包含了此時計算結果,以及當前的應用狀態。

再舉個簡單的例子。假設我們根據某狀態數字進行計算,首先我們在這個初始狀態上加某個數字,然后我們把狀態 + 1, 再把新的狀態和前一步的計算相乘,算出最終結果。同樣,例子很簡單,但已經包含了狀態管理的核心。來看代碼:

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}

上面最后一步組合,每個函數類型簽名一致,a -> m b, 構成 kleisli 組合,我們還可以用工具函數改進一下寫法:

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 有一個“臭名昭著”的定義,是這樣:

A monad is just a monoid in the category of endofunctors, what's the problem?

我見過這句話的中文翻譯。但是這種“鬼話”不管翻不翻譯都差不多的表達效果,我覺得還是不用翻譯了。很多人看到這句話不去查出處和上下文,就以此為據來批評 FP 社區故弄玄虛,我感到很無奈。

這句話出自這篇文章 Brief, Incomplete and Mostly Wrong History of Programming Languages. 這篇文章用戲謔調侃的方式把所有主流編程語言黑了一個遍。上面那句話是用來黑 Haskell 的。本來是句玩笑,結果就以訛傳訛了。

上面那句話的原始出處是范疇論的奠基之作 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.

注意書名,那是給數學家看的,不是給程序員看的。你看不懂很正常,看不懂還要罵這些學術泰斗裝逼就是你的不對了。

范疇論背景

首先,說明下我數學學得差,我接下來要講的名詞我知道是在研究什么,再深入細節我就不知道了。

大家知道數學有很多分支,比如集合論,邏輯學,類型論(有這個嗎?Type Theory) 等等。后來,有些數學家發現,如果用足夠抽象的概念工具去考察這些分支,其實他們都在講同樣的東西。橋接這些概念的工具是 isomorphism (同構)。isomorphic 就是在對象之間可以來回轉換,每次轉換沒有信息丟失。比如,在邏輯學里面研究的某個問題,可能和類型論里面研究是同一個問題,只要兩者之間能形成 isomorphism。

再后來,FP 祖師爺之一 Haskell Curry,和另一個數學家一起發現了 Curry–Howard Isomorphism。這個理論證明了 proofs as programs, 就是說寫電腦程序(當然是函數式)和寫邏輯證明是一回事,兩者形成同構。再后來,這個理論被擴展了一下,成了 Curry–Howard-Lambek Isomorphism, 就是說邏輯學,程序函數,和范疇論,三者之間形成同構。

看了上面的理論背景,你應該明白了為什么函數式編程要從范疇論里面獲取理論資源。

什么是范疇 (Category)

范疇其實是很簡單的一個概念。范疇由一堆(這個量詞好難翻譯,我見過 a bunch, a collection, 但是不能說 a set)對象,以及對象之間的關系構成。我分兩部分介紹。

對象 (Object): 范疇論里面的對象和編程里面的對象是兩回事。范疇中的對象沒有屬性,沒有結構,你可以把它理解為不可描述的點。

箭頭 (arrow, morphism, 兩個詞說的是同一個東西, 我后面就用箭頭了): 連接對象,表示對象之間的關系。同樣,箭頭也是一個沒有結構沒有屬性的一種 primitive。它只說明了對象之間存在關系,并不能說明是什么關系。

對象和箭頭要構成一個范疇,還要滿足這兩個條件:

  • 單元律。每個對象至少有一個箭頭能從自己出發回到自身。

  • 結合律。如果對象 a 和 b 之間存在箭頭 f,對象 b 和 c 之間存在箭頭 g,則必然存在箭頭 h 由 a 到 c,h 就是 f 和 g 的組合。

可以看出范疇論的起點真的非常簡單。很難想象基于這么簡單的概念能構建出一個完整的數學理論。

我一開始試著在范疇論中來解釋 Monad,以失敗告終。要介紹的拗口名詞太多了,一篇文章根本講不完。所以本文會折中一下,還是用集合論的視角來解釋一下范疇論概念。(范疇論的單個對象可以對應成一個集合,但是范疇論禁止談論集合元素,所有關于對象的知識都由箭頭和組合推理出來,所以很頭疼。)

還記得我們是用集合來定義 Monoid 的吧?Monoid 其實就是一個只有一個對象的范疇。對象和對象之間的映射叫 Functor。如果一個 Functor 把對象映射回自身,那么這個 Functor 就叫 Endofunctor。Functor 和 Functor 之間的映射叫 Natural Transformation. 函數式編程其實只處理一個對象,就是數據類型(Types)。所以,我們前面提到的 Functor 也是 Endofunctor。

回到前面 Monad 中 chain 的類型簽名:

chain :: m a -> (a -> m b) -> m b

可以看出 Monad 是把一個類型映射回自身(m a -> m b),那么它就是一個 Endofunctor。

再看看 Monad 中所運用的 Natural Transformation。還是看 chain 的簽名,前半部分 m a -> (a -> m b) 執行之后,類型簽名是 m (m b), 然后再和后面的連起來,就是 m (m b) -> m b. 這其實就是把一個 functor (m (m b)) 映射到另一個 Functor (m b)。m (m b) -> m b 看起來是不是很眼熟?一個 Functor 和自己組合,形成同一個范疇里的 Functor,這就是 Monoid 啊!我們一開始定義的 Monoid 中的二元運算,在 Monad 中其實就是 Natural Transformation。

那么,再回到這一部分開始時的定義:

A monad is just a monoid in the category of endofunctors.

有沒有好理解一點?

為什么要這樣寫程序

這篇文章的目的不是鼓勵你在你的代碼中消滅狀態機,消滅副作用,我自己都做不到的。我司后端是用 Java 寫的,如果我告訴后端同事 “Yo,你的程序里不能出現狀態機哦……”,怕是會被哄出辦公室的。那么,為什么要了解這些知識?

計算機科學中有兩條截然相反的路徑。一條是自下而上,從底層指令開始往上抽象(優先考慮性能),逐漸靠近數學。比如,一開始的 Unix 操作系統是用匯編寫的,后來發現用匯編寫程序太痛苦了,需要一些抽象,所以出現了高級語言 C,再后來由于各種編寫應用的需求,出現了更高級的語言如 Python 和 JavaScript。另一條路徑是自上而下的,直接從數學開始(Lambda 演算),不考慮性能和硬件狀況,按需逐漸減少抽象。前一條路徑明顯占了主流,代表語言是 Fortran, C, C++, Pascal, 和 Java 等。后面一條路徑不夠實用,比較小眾,代表語言是 Algo, LISP, Small Talk 和 Haskell 等。

這兩個陣營肯定是由爭論的。前者想勸后者從良:你別扔給我這么多函數,我沒法不影響性能情況下處理那么多垃圾回收和函數調用!后者也想叫醒前者:不要過早深入硬件細節,你會把自己鎖定在無法逆轉的設計錯誤上!兩者分道揚鑣了 60 多年,這些年總算開始融合了。比如,新出現的程序語言如 Scala,Kotlin,甚至系統編程語言 Rust,都大量借鑒了函數式編程的思想。

學些高階抽象還能幫助你更容易理解一些看起來很復雜的概念。轉述一個例子。C++ 編程里面最高的抽象是模板元編程(Template Meta Programming),據說很難懂。但是據 Bartosz Milewski 的解釋,之所以這個概念難懂,是因為 C++ 的語言設計不適合表達這些抽象。如果你會 Haskell,就會發現其實一行代碼就完成了。


參考:

  • 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

  • 總結

    以上是生活随笔為你收集整理的完整年份值必须介于_上 | 完整解释 Monad 程序员范畴论入门的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。