完整解釋 Monad -- 程序員範疇論入門

若是你接觸過函數式編程,你極可能遇到過 Monad 這個奇怪的名詞。因爲各類神奇的緣由,Monad 成了一個很難懂的概念。Douglas Crockford 曾轉述過這樣一句話來形容 Monad:javascript

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

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

函數組合

1. Monoid

假設你被一個奇怪的叢林部落抓住了,部落長老知道你是程序員,要你寫個應用,寫出來就放你走。做爲一個資深碼農,你暗自竊喜,內心想着老夫經歷了這麼多年產品經理各類變態需求的千錘百煉,沒什麼需求能難倒我!長老彷佛看出了你的心思,加了一個要求:這個應用只能用純函數寫,不能有狀態機,不能有反作用!而後你崩潰了……react

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

接下來咱們就來一步步推演出這個超級函數吧。程序員

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

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 函數:const identity = 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.of = value => Effect(() => value)
複製代碼

這個 Effect 函數接受一個非純函數 f 爲參數,返回一個對象。這個對象裏面的 map 方法把自身接受的非純回調函數 g 和 Effect 的非純回調函數組合後,將結果再塞回給 Effect。因爲 map 返回的也是對象,咱們須要一個方法把最終的計算結果取出來,這就是 runWith 的做用。

Effect 重現咱們上一步的計算以下:

Effect(impureAddOne)
  .map(impureMultiplyByTwo)
  .runWith(2) // => add one!multiply by two! 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)
複製代碼

xFromWindowyFromWindow 返回的都是一個 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).runWith() // => 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))
const log = s => Effect(() => console.log(s))

Effect.of(window)
  .map(win => win.userEl)
  .map($)
  .runWith() //因爲上一個 map 裏層也返回了 Effect,這裏須要抹平一層
  .map(e => e.innerHTML)
  .map(log)
  .runWith()
  .runWith()
複製代碼

勉強能作到,可是這樣子先 maprunWith 實在太繁瑣了,咱們能夠再給 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(),
})
複製代碼

而後這樣組合:

Effect.of(window)
  .map(win => win.userEl)
  .chain($)
  .map(e => e.innerHTML)
  .chain(log)
  .runWith();
複製代碼

線上 Demo 見這裏

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.runWith(ctx)))

  const chain = f => {
    return Reader(ctx => {
      const a = computation(ctx)
      return f(a).runWith(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('張三')
// => 你好, 張三...再見, 張三
複製代碼

上面這個例子過於簡單。輸出一個字符串用一個函數就行,用不了解構和組合。可是,咱們能夠很容易擴展想象,若是 greetaddFarewell 是很複雜的模塊,必須拆分,此時組合的價值就出現了。

在學習 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 => State.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 和 Haskell 等。

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

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

本文還發表在 Lambda Academy


參考:

相關文章
相關標籤/搜索