原文:https://dev.to/gcanti/getting-started-with-fp-ts-monad-6k
这篇文章的标题是《Getting started with fp-ts: Monad》,由Giulio Canti发表。文章是关于如何在fp-ts
库中使用Monad
类型类的入门指南。
在上一篇文章中,我们看到可以通过提升g
来组合一个有效应程序f: (a: A) => M<B>
和一个纯n
元程序g
,前提是M
承认一个应用函子实例。
然而,我们必须解决最后一个案例:如果两个程序都是有效应的怎么办?
f: (a: A) => M<B>
g: (b: B) => M<C>
这种f
和g
的“组合”是什么?
为了处理这最后一个案例,我们需要比Functor
更强大的东西,因为很容易最终得到嵌套的上下文。
问题:嵌套上下文
为了更好地解释为什么我们需要更多的东西,让我们看一些例子。
示例(M = Array
)
假设我们想要检索Twitter用户的追随者的追随者:
interface User {
followers: Array<User>
}
const getFollowers = (user: User): Array<User> => user.followers
declare const user: User
const followersOfFollowers: Array<Array<User>> = getFollowers(user).map(getFollowers)
这里有些问题,followersOfFollowers
的类型是Array<Array<User>>
,但我们想要的是Array<User>
。
我们需要展平嵌套的数组。
fp-ts
提供的flatten: <A>(mma: Array<Array<A>>) => Array<A>
函数很有用
import { flatten } from 'fp-ts/Array'
const followersOfFollowers: Array<User> = flatten(getFollowers(user).map(getFollowers))
太好了!其他数据结构呢?
示例(M = Option
)
假设我们想要计算数值列表的头部的倒数
import { Option, some, none, option } from 'fp-ts/Option'
import { head } from 'fp-ts/Array'
const inverse = (n: number): Option<number> => (n === 0 ? none : some(1 / n))
const inverseHead: Option<Option<number>> = option.map(head([1, 2, 3]), inverse)
我又做错了,inverseHead
的类型是Option<Option<number>>
,但我们想要的是Option<number>
。
我们需要展平嵌套的Option
。
import { isNone } from 'fp-ts/Option'
const flatten = <A>(mma: Option<Option<A>>): Option<A> => (isNone(mma) ? none : mma.value)
const inverseHead: Option<number> = flatten(option.map(head([1, 2, 3]), inverse))
所有这些flatten
函数……这不是巧合,背后有一个函数式模式。
事实上,所有这些类型构造子(还有很多其他类型构造子)都承认一个单子实例,并且
flatten
是单子最特殊的操作
那么什么是单子?
单子通常被这样介绍…
定义
单子由三件事定义:
(1) 一个类型构造子M
,它承认一个Functor实例
(2) 一个具有以下签名的函数of
of: <A>(a: A) => HKT<M, A>
(3) 一个具有以下签名的函数flatMap
flatMap: <A, B>(f: (a: A) => HKT<M, B>) => ((ma: HKT<M, A>) => HKT<M, B>)
注意:回想一下HKT
类型是fp-ts
表示通用类型构造子的方式,所以当你看到HKT<M, X>
时,你可以想到类型构造子M
应用于类型X
(即M<X>
)。
函数of
和flatMap
需要遵守三个法则:
flatMap(of) ∘ f = f
(左恒等)flatMap(f) ∘ of = f
(右恒等)flatMap(h) ∘ (flatMap(g) ∘ f) = flatMap((flatMap(h) ∘ g)) ∘ f
(结合性)
其中f
、g
、h
都是有效应函数,∘
是通常的函数组合。
好的,但是……为什么?
很久以前,当我第一次看到这样的定义时,我的第一反应是困惑。
所有这些问题都在我脑海中旋转:
为什么是这两个特定的操作,以及为什么它们有这些类型?
为什么叫“flatMap”?
为什么有法则?它们意味着什么?
但最重要的是,我的
flatten
在哪里?
本文将尝试回答每个问题。
让我们回到我们的问题:两个有效应函数(也称为Kleisli箭头)的组合是什么?
(两个Kleisli箭头,它们的组合是什么?)
我甚至不知道它的类型是什么。
等等……我们已经遇到了一个关于组合的抽象。你还记得我关于类别说过什么吗?
类别捕捉了组合的本质
我们可以将我们的问题转化为类别问题:我们能否找到一个类别,它模拟了Kleisli箭头的组合?
Kleisli类别
让我们尝试构建一个只包含有效应函数的类别_K_(名为Kleisli类别):
对象与_TS_类别的对象相同,即所有TypeScript类型。
态射是这样构建的:每当_TS_中有Kleisli箭头
f: A ⟼ M<B>
时,我们在_K_中绘制一个箭头f': A ⟼ B
(_TS_类别上方,_K_构造下方)
那么_K_中f'
和g'
的组合是什么?下图中标记为h'
的虚线箭头
(_TS_类别上方的组合,_K_构造下方的组合)
由于h'
是一个从A
到C
的箭头,应该有相应的函数h
从A
到M<C>
在TS
中。
因此,f
和g
在_TS_中的组合的一个好候选者仍然是一个具有以下签名的有效应函数:(a: A) => M<C>
。
我们如何构建这样的函数?好吧,让我们试试!
逐步构建组合
单子定义的第(1)点表明M
承认一个functor实例,所以我们可以将函数g: (b: B) => M<C>
提升到函数lift(g): (mb: M<B>) => M<M<C>>
(这里我使用它的同义词map
)
(flatMap
从哪里来)
现在我们陷入了困境:没有合法的操作可以将类型为M<M<C>>
的值展平为类型为M<C>
的值,我们需要一个额外的flatten
操作。
如果我们能定义这样的操作,那么我们就可以获得我们正在寻找的组合
h = flatten ∘ map(g) ∘ f
但等等,flatten ∘ map(g)
是flatMap,这就是名字的由来!
h = flatMap(g) ∘ f
我们现在可以更新我们的“组合表”
| 程序 f
| 程序 g | 组合 | | --- | --- | --- | | 纯 | 纯 | g ∘ f
| | 有效应 | 纯,n
元 | liftAn(g) ∘ f
| | 有效应 | 有效应 | flatMap(g) ∘ f
|
其中liftA1 = lift
of
呢?of
来自_K_中的恒等态射:对于_K_中的每个恒等态射1A,应该有一个相应的函数从A
到M<A>
(即of: <A>(a: A) => M<A>
)。
(of
从哪里来)
法则
最后一个问题:法则从哪里来?它们只是_K_中的类别法则转化为_TS_:
fp-ts
中的单子
在fp-ts
中,flatMap
函数由一个变体chain
建模,它基本上是flatMap
,参数重新排列
flatMap: <A, B>(f: (a: A) => HKT<M, B>) => ((ma: HKT<M, A>) => HKT<M, B>)
chain: <A, B>(ma: HKT<M, A>, f: (a: A) => HKT<M, B>) => HKT<M, B>
注意chain
可以从flatMap
派生(反之亦然)。
现在如果我们回到显示嵌套上下文问题的示例,我们可以通过使用chain
来修复它们
import { array, head } from 'fp-ts/Array'
import { Option, option } from 'fp-ts/Option'
const followersOfFollowers: Array<User> = array.chain(getFollowers(user), getFollowers)
const headInverse: Option<number> = option.chain(head([1, 2, 3]), inverse)
结论
函数式编程提供了通用的方法来组合有效应的函数:函子、应用函子和单子都是提供原则性工具来组合不同类型程序的抽象。
TLDR:函数式编程确实是关于组合的。