Typescript版图解Functor , Applicative 和 Monad

本文是经典的Functors, Applicatives, And Monads In Pictures的Typescript翻译版本。html

Functor/Applicative/Monad是函数式编程中的一些比较‘基础’的概念,反正我是不认同‘基础’这个说法的,笔者也阅读过不少相似介绍Monad的文章,最后都不了了之,这些概念是比较难以理解的,并且平时编程实践中也很难会接触到这些东西。前端

后来拜读了Functors, Applicatives, And Monads In Pictures, 不错,好像懂了。因而本身想经过翻译,再深刻消化消化这篇文章,这里使用Typescript做为描述语言,对于前端来讲会更好理解。git

有理解不正确的地方,敬请指正. 开始吧!github


这是一个简单的值:数据库

例如这些编程

1        // number
'string' // string
复制代码

你们都知道怎么将一个函数应用到这个值上面:数组

// So easy
const add3 = (val: number) => val + 3
console.log(add3(2)) // 5
复制代码

很简单了. 咱们来扩展一下, 让任意的值是能够包装在一个**上下文(context)**当中. 如今的状况你能够想象一个能够把值放进去的盒子:bash

如今你把一个函数应用到这个包装值的时候, 根据其上下文类型你会获得不一样的结果. 这就是 Functor, Applicative, Monad, Arrow 之类概念的基础.app

Maybe 就是一个典型的数据类型, 它定义了两种相关的‘上下文’, Maybe自己也是一个‘上下文’(除了值,其余类型均可以是一个上下文?):ide

原文基于Haskell,它的Maybe类型有两个上下文Just(蓝色盒子)和None(红色空盒子)。仿造Haskell在Typescript中咱们可使用可选类型(Maybe)来表示:

type Maybe<T> = Just<T> | Nothing // Just 表示值‘存在’,Nothing表示空值,相似于null、undefined的概念
复制代码

Just和Nothing的基本结构:

// 咱们只用None来取代null, 这里咱们将None做为一个值,而不是一个类
export class None {}
// 对应None的类型
export type Nothing = typeof None

// 判断是不是Nothing,这里使用Typescript的 `Type Guards`
export const isNothing = (val: any): val is Nothing => {
  return val === None
}

// Just类型
export class Just<T> {
  static of<T>(val: T) {
    return new Just(val)
  }
  value: T
  constructor(value: T) {
    this.value = value
  }
}
复制代码

使用示例:

let a: Maybe<number>;
a = None;
a = Just.of(3);
复制代码

说实在这个实现有点挫, 可是为了更加贴近原文描述,暂且使用这个实现。以前考虑过的一个版本是下面这样的, 由于没法给它们扩展方法,就放弃了这个方案:

type Optional<T> = NonNullable<T> | nul
  let a: Optional<number> = 1;
  a = null;
复制代码

很快咱们会看到对一个 Just<a> 和一个 Nothing 来讲, 函数应用有何不一样. 首先咱们来看看 Functor!



Functors

当一个值被包装在一个上下文中时, 你就不能拿普通函数来应用了:

declare let a: Just<number>;

const add3 = (v: number) => v + 3
add3(a) // ❌ 类型“Just<number>”的参数不能赋给类型“number”的参
复制代码

这时候, 该 fmap 出场了. fmap 翩翩而来,从容应对上下文(fmap is from the street, fmap is hip to contexts). 还有谁? fmap 知道怎样将一个函数应用到一个包装在上下文的值上. 你能够对任何一个类型为 Functor 的类型使用 fmap, 换句话说,Functor都定义了fmap.

好比说, 想一下你想把 add3 应用到 Just 2. 用 fmap:

Just.of(2).fmap(add3) // Just 5
复制代码

💥嘭! fmap 向咱们展现了它的成果。 可是 fmap 怎么知道如何应用该函数的呢?


究竟什么是 Functor 呢?

在 Haskell 中 Functor 是一个类型类(typeclass)。 其定义以下:

在Typescript中, 一个Functor认为是定义了fmap的任意类型. 看看fmap是如何工做的:

  1. 一个Functor类型的 fa, 例如Just 2
  2. fa 定义了一个fmap, fmap 接受一个函数fn,例如add3
  3. fmap 直到如何将fa应用到fn中, 返回一个Functor类型的 fb. fa和fb的包装上下文类型同样, 例如fa是Just, 那么fb也是Just; 反之fa是Nothing,fb也是Nothing;

用Typescript的函数签名描述一下:

<Functor T>.fmap<U>(fn: (val: T) => U): <Functor U> 复制代码

因此咱们能够这么作:

Just.of(2).fmap(add3) // Just 5
复制代码

而 fmap 神奇地应用了这个函数,由于 Maybe 是一个 Functor, 它指定了 fmap 如何应用到 Just 上与 Nothing 上:

class Just<T> {
  // ...
  // 实现fmap
  fmap<U>(fn: (val: T) => U) { return Just.of(fn(this.value)) } } class None { // None 接受任何函数都返回None static fmap(fn: any) { return None } } 复制代码

当咱们写 Just.of(2).fmap(add3) 时,这是幕后发生的事情:

那么而后,就像这样,fmap,请将 add3 应用到 Nothing 上如何?

None.fmap(add3) // Nothing
复制代码

就像《黑客帝国》中的 Morpheus,fmap 知道都要作什么;若是你从 Nothing 开始,那么你会以 Nothing 结束! fmap 是禅。

如今它告诉咱们了 Maybe 数据类型存在的意义。 例如,这是在一个没有 Maybe 的语言中处理一个数据库记录的方式, 好比Javascript:

let post = Post.findByID(1)
if (post != null) {
  return post.title
} else {
  return null
}
复制代码

有了fmap后:

// 假设findPost返回Maybe<Article>
findPost(1).fmap(getPostTitle)
复制代码

若是 findPost 返回一篇文章,咱们就会经过 getPostTitle 获取其标题。 若是它返回 Nothing,咱们就也返回 Nothing! 较以前简洁了不少对吧?

Typescript有了Optional Chaining后,处理null也能够很简洁:

findPost(1)?.title // 殊途同归
复制代码

原文还有定义了一个fmap的重载操做符版本,由于JavaScript不支持操做符重载,因此这里简单带过

getPostTitle <$> findPost(1) // 使用操做符重载<$> 来简化fmap. 等价于上面的代码
复制代码

再看一个示例:若是将一个函数应用到一个 Array(Haksell 中是 List)上会发生什么?

Array 也是 functor!

[1, 2, 3].map(add3) // [4, 5, 6]. fa是Array,输出fb也是Array,符合Functor的定义吧,因此Javascript的map就是fmap,Array就是Functor
复制代码

好了,好了,最后一个示例:若是将一个函数应用到另外一个函数上会发生什么?

const multiply3 = (v: number) => v * 3
const add3 = (v: number) => v + 3

add3.fmap(multiply3) // ❓
复制代码

这是一个函数:

这是一个应用到另外一个函数上的函数:

其结果是又一个函数!

// 仅做示例,不要模仿
interface Function {
  fmap<V, T, U>(this: (val: V) => T, fn: (val: T) => U): (val: V) => U
}
Function.prototype.fmap = function(fn) {
  return v => fn(this(v))
}
复制代码

因此函数也是 Functor! 对一个函数使用 fmap,其实就是函数组合(compose)! 也就是说: f.fmap(g) 等价于 compose(f, g)


Functor总结

经过上面的例子,能够知道Functor其实并无那么难以理解, 一个Functor就是:

<Functor T>.fmap(fn: (v: T) => U): <Functor U>
复制代码

Functor会定义一个‘fmap’操做,这个fmap接受一个函数fn,fn接收的是具体的值,返回另外一个具体的值,例如上面的add3. fmap决定如何来应用fn到源Functor(a), 返回一个新的Functor(b)。 也就是fmap的源和输出的值‘上下文’类型是同样的。好比

  • Just -> fmap -> Just
  • Nothing -> fmap -> Nothing
  • Maybe -> fmap -> Maybe
  • Array -> fmap -> Array


Applicative

如今练到二重天了。 Applicative 又提高了一个层次。

对于 Applicative,咱们的值依然和 Functor 同样包装在一个上下文中

不同的是,咱们将Functor中的函数(例如add3)也包装在一个上下文中

嗯。 咱们继续深刻。 Applicative 并无开玩笑。不像Haskell,Typescript并无内置方式来处理Applicative。咱们能够给须要支持Applicative的类型定义一个apply函数。apply函数知道怎么将包装在上下文的函数应用到一个包装在上下文的值

class None {
  static apply(fn: any) {
    return None;
  }
}

class Just<T> {
  // 使用方法重载,让Typescript更好推断
  // 若是值和函数都是Just类型,结果也是Just类型
  apply<U>(fn: Just<(a: T) => U>): Just<U>; // 若是函数是Nothing类型,结果是Nothing. // 严格上apply只应该接收同一个上下文类型的函数,即Just, // 由于MaybeTypescriptUnion类型,没办法给它扩展方法,这里将MaybeJust混在一块儿了 apply<U>(fn: Nothing): Nothing; // 若是值和函数都是Maybe类型, 返回一个Maybe类型 apply<U>(fn: Maybe<(a: T) => U>): Maybe<U> { if (!isNothing(fn)) { return Just.of(fn.value(this.value)); } return None.apply(fn); } } 复制代码

再来看看数组:

// 仅做示例
interface Array<T> {
  apply<U>(fns: Array<(e: T) => U>): U[] } // 接收一个函数‘数组(上下文)’,返回一个应用了‘函数’的新的数组 Array.prototype.apply = function<T, U>(fns: Array<(e: T) => U>) { const res: U[] = [] for (const fn of fns) { this.forEach(el => res.push(fn(el))) } return res } 复制代码

在Haskell中,使用<*>来表示apply操做: Just (+3) <*> Just 2 == Just 5. Typescript不支持操做符重载,因此忽略.

Just类型的Applicative应用图解:

数组类型的Applicative应用图解:

const num: number[] = [1, 2, 3]
console.log(num.apply([multiply2, add3]))
// [2, 4, 6, 4, 5, 6]
复制代码

这里有 Applicative 能作到而 Functor 不能作到的事情。 如何将一个接受两个参数的函数应用到两个已包装的值上?

// 一个支持两个参数的Curry型加法函数
const curriedAddition = (a: number) => (b: number) => a + b

Just.of(5).fmap(curriedAddition) // 返回 `Just.of((b: number) => 5 + b)`
// Ok 继续
Just.of(4).fmap(Just.of((b: number) => 5 + b))  // ❌不行了,报错了,Functor没办法处理包装在上下文的fn
复制代码

可是Applicative能够:

Just.of(5).fmap(curriedAddition) // 返回 `Just.of((b: number) => 5 + b)`
// ✅当当当
Just.of(3).apply(Just.of((b: number) => 5 + b)) // Just.of(8)
复制代码

这时候Applicative 把 Functor 推到一边。 “大人物可使用具备任意数量参数的函数,”它说。 “装备了 <$>(fmap) 与 <*>(apply) 以后,我能够接受具备任意个数未包装值参数的任意函数。 而后我传给它全部已包装的值,而我会获得一个已包装的值出来! 啊啊啊啊啊!”

Just.of(3).apply(Just.of(5).fmap(curriedAddition)) // 返回 `Just.of(8)`
复制代码

Applicative总结

咱们重申一个Applicative的定义, 若是Functor要求实现fmap的话,Applicative就是要求实现apply,apply符合如下定义:

// 这是Functor的fmap定义
<Functor T>.fmap(fn: (v: T) => U): <Functor U>

// 这是Applicative的apply定义,和上面对比,fn变成了一个包装在上下文的函数
<Applicative T>.apply(fn: <Applicative (v: T) => U>): <Applicative U>
复制代码


Monad

终于练到三重天了!继续⛽加油️

如何学习 Monad 呢:

  1. 你要取得计算机科学博士学位。
  2. 而后把它扔掉,由于在本文你并不须要它!

Monad 增长了一个新的转变。

Functor 将一个函数应用到一个已包装的值上:

Applicative 将一个已包装的函数应用到一个已包装的值上:

Monad 将一个返回已包装值的函数应用到一个已包装的值上。 Monad 定义一个函数flatMap(在 Haskell 中是使用操做符 >>= 来应用Monad,读做“bind”)来作这个。

让咱们来看个示例。 老搭档 Maybe 是一个 Monad:

假设 half 是一个只适用于偶数的函数:

// 这就是一个典型的: "返回已包装值"的函数
function half(value: number): Maybe<number> {
  if (value % 2 === 0) {
    return Just.of(value / 2)
  }
  return None
}
复制代码

若是咱们喂给它一个已包装的值会怎样?

咱们须要使用flatMap(Haskell 中的>>=)来将咱们已包装的值塞进该函数。 这是 >>= 的照片:

如下是它的工做方式:

Just.of(3).flatMap(half) // => Nothing, Haskell中使用操做符这样操做: Just 3 >>= half
Just.of(4).flatMap(half) // => Just 2
None.flatMap(half)       // => Nothing
复制代码

内部发生了什么?咱们再看看flatMap的方法签名:

// Maybe
Maybe<T>.flatMap<U>(fn: (val: T) => Maybe<U>): Maybe<U> // Array Array<T>.flatMap<U>(fn: (val: T) => U[]): U[] 复制代码

Array是一个Monad, Javascript的Array的flatMap已经正式成为标准, 看看它的使用示例:

const arr1 = [1, 2, 3, 4];
arr1.map(x => [x * 2]); 
// [[2], [4], [6], [8]]

arr1.flatMap(x => [x * 2]);
// [2, 4, 6, 8]

// only one level is flattened
arr1.flatMap(x => [[x * 2]]);
// [[2], [4], [6], [8]]
复制代码

Maybe 也是一个 Monad:

class None {
  static flatMap(fn: any): Nothing {
    return None;
  }
}

class Just<T> {
  // 和上面的apply差很少
  // 使用方法重载,让Typescript更好推断
  // 若是函数返回Just类型,结果也是Just类型
  flatMap<U>(fn: (a: T) => Just<U>): Just<U>; // 若是函数返回值是Nothing类型,结果是Nothing. flatMap<U>(fn: (a: T) => Nothing): Nothing; // 若是函数返回值是Maybe类型, 返回一个Maybe类型 flatMap<U>(fn: (a: T) => Maybe<U>): Maybe<U> { return fn(this.value) } } // 示例 Just.of(3).flatMap(half) // Nothing Just.of(4).flatMap(half) // Just.of(4) 复制代码

这是与 Just 3 运做的状况!

若是传入一个 Nothing 就更简单了:

你还能够将这些调用串联起来:

Just.of(20).flatMap(half).flatMap(half).flatMap(falf) // => Nothing
复制代码


很炫酷哈!因此咱们如今知道Maybe既是一个Functor、Applicative,仍是一个Monad。

原文还示范了另外一个例子: IO Monad, 咱们这里就简单了解一下

IO的签名大概以下:

class IO<T> {
  val: T
  // 具体实现咱们暂不关心
  flatMap(fn: (val: T) => IO<U>): IO<U>
}
复制代码

具体来看三个函数。 getLine 没有参数, 用来获取用户输入:

function getLine(): IO<string> 复制代码

readFile 接受一个字符串(文件名)并返回该文件的内容:

function readFile(filename: string): IO<string> 复制代码

putStrLn 输出字符串到控制台:

function putStrLn(str: string): IO<void> 复制代码

全部这三个函数都接受普通值(或无值)并返回一个已包装的值,即IO。 咱们可使用 flatMap 将它们串联起来!

getLine().flatMap(readFile).flatMap(putStrLn)
复制代码

太棒了! 前排占座来看 monad 展现!咱们不须要在取消包装和从新包装 IO monad 的值上浪费时间. flatMap 为咱们作了那些工做!

Haskell 还为 monad 提供了语法糖, 叫作 do 表达式:

foo = do
    filename <- getLine
    contents <- readFile filename
    putStrLn contents
复制代码

总结

  1. functor 是实现了 fmap 的数据类型。
  2. applicative 是实现了 apply 的数据类型。
  3. monad 是实现了 flatMap 的数据类型。
  4. Maybe 实现了这三者,因此它是 functor、 applicative、 以及 monad。

这三者有什么区别呢?

  1. functor: 可经过 fmap 将一个函数应用到一个已包装的值上。
  2. applicative: 可经过 apply 将一个已包装的函数应用到已包装的值上。
  3. monad: 可经过 flatMap 将一个返回已包装值的函数应用到已包装的值上。

综合起来看看它们的签名:

// 这是Functor的fmap定义
<Functor T>.fmap(fn: (v: T) => U): <Functor U>

// 这是Applicative的apply定义,和上面对比,fn变成了一个包装在上下文的函数
<Applicative T>.apply(fn: <Applicative (v: T) => U>): <Applicative U>

// Monad的定义, 而接受一个函数, 这个函数返回一个包装在上下文的值
<Monad T>.flatmap(fn: (v: T) => <Monad U>): <Monad U>
复制代码

因此,亲爱的朋友(我以为咱们如今是朋友了),我想咱们都赞成 monad 是一个简单且高明的主意(SMART IDEA(tm))。 如今你已经经过这篇指南润湿了你的口哨,为何不拉上 Mel Gibson 并抓住整个瓶子呢。 参阅《Haskell 趣学指南》的《来看看几种 Monad》。 不少东西我其实掩饰了由于 Miran 深刻这方面作得很棒.


扩展

本文在原文的基础上, 参考了下列这些翻译版本,再次感谢这些做者:

相关文章
相关标签/搜索