完整解释 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


参考:

相关文章
相关标签/搜索