本文是经典的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!
当一个值被包装在一个上下文中时, 你就不能拿普通函数来应用了:
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 怎么知道如何应用该函数的呢?
在 Haskell 中 Functor
是一个类型类(typeclass)。 其定义以下:
在Typescript中, 一个Functor认为是定义了fmap的任意类型. 看看fmap
是如何工做的:
用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 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,咱们的值依然和 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, // 由于Maybe是Typescript的Union类型,没办法给它扩展方法,这里将Maybe和Just混在一块儿了 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的定义, 若是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 增长了一个新的转变。
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
复制代码
fmap
的数据类型。apply
的数据类型。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 深刻这方面作得很棒.
本文在原文的基础上, 参考了下列这些翻译版本,再次感谢这些做者: