chengaofeng
发布于 2024-10-14 / 7 阅读
0
0

Getting started with fp-ts: Monad

原文: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

程序 g

组合

g ∘ f

有效应

纯,n

liftAn(g) ∘ f

然而,我们必须解决最后一个案例:如果两个程序都是有效应的怎么办?

f: (a: A) => M<B>
g: (b: B) => M<C>

这种fg的“组合”是什么?

为了处理这最后一个案例,我们需要比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>)。

函数offlatMap需要遵守三个法则:

  • flatMap(of) ∘ f = f左恒等

  • flatMap(f) ∘ of = f右恒等

  • flatMap(h) ∘ (flatMap(g) ∘ f) = flatMap((flatMap(h) ∘ g)) ∘ f结合性

其中fgh都是有效应函数,是通常的函数组合。

好的,但是……为什么?

很久以前,当我第一次看到这样的定义时,我的第一反应是困惑。

所有这些问题都在我脑海中旋转:

  • 为什么是这两个特定的操作,以及为什么它们有这些类型?

  • 为什么叫“flatMap”?

  • 为什么有法则?它们意味着什么?

  • 但最重要的是,我的flatten在哪里?

本文将尝试回答每个问题。

让我们回到我们的问题:两个有效应函数(也称为Kleisli箭头)的组合是什么?

https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fgom0mol0h57jurvmy9n1.png

(两个Kleisli箭头,它们的组合是什么?)

我甚至不知道它的类型是什么。

等等……我们已经遇到了一个关于组合的抽象。你还记得我关于类别说过什么吗?

类别捕捉了组合的本质

我们可以将我们的问题转化为类别问题:我们能否找到一个类别,它模拟了Kleisli箭头的组合?

Kleisli类别

让我们尝试构建一个只包含有效应函数的类别_K_(名为Kleisli类别):

  • 对象与_TS_类别的对象相同,即所有TypeScript类型。

  • 态射是这样构建的:每当_TS_中有Kleisli箭头f: A ⟼ M<B>时,我们在_K_中绘制一个箭头f': A ⟼ B

https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fzz2xubsrhpizvq4slxxp.png

(_TS_类别上方,_K_构造下方)

那么_K_中f'g'的组合是什么?下图中标记为h'的虚线箭头

https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2F339yozksu42ql1mfbsxf.png

(_TS_类别上方的组合,_K_构造下方的组合)

由于h'是一个从AC的箭头,应该有相应的函数hAM<C>TS中。

因此,fg在_TS_中的组合的一个好候选者仍然是一个具有以下签名的有效应函数:(a: A) => M<C>

我们如何构建这样的函数?好吧,让我们试试!

逐步构建组合

单子定义的第(1)点表明M承认一个functor实例,所以我们可以将函数g: (b: B) => M<C>提升到函数lift(g): (mb: M<B>) => M<M<C>>(这里我使用它的同义词map

https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Ftawvpqf52nxtrr3gv4jh.png

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,应该有一个相应的函数从AM<A>(即of: <A>(a: A) => M<A>)。

https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2F12810n5g8u9us00ic75m.png

of从哪里来)

法则

最后一个问题:法则从哪里来?它们只是_K_中的类别法则转化为_TS_:

法则

K

TS

左恒等

1B ∘ f' = f'

flatMap(of) ∘ f = f

右恒等

f' ∘ 1A = f'

flatMap(f) ∘ of = f

结合性

h' ∘ (g' ∘ f') = (h' ∘ g') ∘ f'

flatMap(h) ∘ (flatMap(g) ∘ f) = flatMap((flatMap(h) ∘ g)) ∘ f

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


评论