原文: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:函数式编程确实是关于组合的。