原文:https://dev.to/gcanti/getting-started-with-fp-ts-applicative-1kb3
这篇文章的标题是《Getting started with fp-ts: Applicative》,由Giulio Canti发表。文章是关于如何在fp-ts
库中使用Applicative
类型类的入门指南。
在上一篇文章中,我们看到可以通过提升g
到函数lift(g): (fb: F<B>) => F<C>
来组合一个有效应程序f: (a: A) => F<B>
和一个纯程序g: (b: B) => C
,前提是F
承认一个functor实例。
然而,g
必须是一元的,也就是说,它只能接受一个参数作为输入。如果g
接受两个参数怎么办?我们是否仍然可以使用仅functor实例来提升g
?让我们尝试一下!
柯里化(Currying)
首先,我们必须模拟一个接受两个参数的函数,假设类型为B
和C
(我们可以使用元组)并返回类型D
的值
g: (args: [B, C]) => D
我们可以使用一种称为柯里化的技术来重写g
。
柯里化是将接受多个参数的函数的求值转换为评估一系列函数的技术,每个函数只有一个参数。例如,通过柯里化,一个接受两个参数的函数,一个来自
B
,一个来自C
,并在D
中产生输出,被转换为接受单个参数来自C
并产生从B
到C
的函数作为输出的函数。
(来源:currying on wikipedia.org)
因此,我们可以将g
重写为
g: (b: B) => (c: C) => D
我们想要的是一个提升操作,让我们称之为liftA2
,以区分我们的旧lift
,它输出具有以下签名的函数
liftA2(g): (fb: F<B>) => (fc: F<C>) => F<D>
我们如何实现这一点?由于g
现在是一元的,我们可以使用functor实例和我们的旧lift
lift(g): (fb: F<B>) => F<(c: C) => D>
但现在我们陷入了困境:functor实例上没有合法的操作能够解包值F<(c: C) => D>
到函数(fc: F<C>) => F<D>
。
Apply
让我们引入一个新的抽象Apply
,它拥有这样的解包操作(名为ap
)
interface Apply<F> extends Functor<F> {
ap: <C, D>(fcd: HKT<F, (c: C) => D>, fc: HKT<F, C>) => HKT<F, D>
}
ap
函数基本上是unpack
,参数重新排列
unpack: <C, D>(fcd: HKT<F, (c: C) => D>) => ((fc: HKT<F, C>) => HKT<F, D>)
ap: <C, D>(fcd: HKT<F, (c: C) => D>, fc: HKT<F, C>) => HKT<F, D>
所以ap
可以从unpack
派生(反之亦然)。
注意:HKT
类型是fp-ts
表示通用类型构造子的方式(轻量级更高阶多态性论文中提出的一种技术),所以当你看到HKT<F, X>
时,你可以想到类型构造子F
应用于类型X
(即F<X>
)。
Applicative
此外,如果存在一个操作,能够将类型A
的值提升到类型F<A>
的值,那将非常方便。这样我们可以通过提供类型F<B>
和F<C>
的参数或通过提升类型B
和C
的值来调用liftA2(g)
函数。
因此,让我们引入Applicative
抽象,它建立在Apply
之上,并拥有这样的操作(名为of
)
interface Applicative<F> extends Apply<F> {
of: <A>(a: A) => HKT<F, A>
}
让我们看看一些常见数据类型的Applicative
实例
示例(F = Array
)
import { flatten } from 'fp-ts/Array'
const applicativeArray = {
map: <A, B>(fa: Array<A>, f: (a: A) => B): Array<B> => fa.map(f),
of: <A>(a: A): Array<A> => [a],
ap: <A, B>(fab: Array<(a: A) => B>, fa: Array<A>): Array<B> =>
flatten(fab.map(f => fa.map(f)))
}
示例(F = Option
)
import { Option, some, none, isNone } from 'fp-ts/Option'
const applicativeOption = {
map: <A, B>(fa: Option<A>, f: (a: A) => B): Option<B> =>
isNone(fa) ? none : some(f(fa.value)),
of: <A>(a: A): Option<A> => some(a),
ap: <A, B>(fab: Option<(a: A) => B>, fa: Option<A>): Option<B> =>
isNone(fab) ? none : applicativeOption.map(fa, fab.value)
}
示例(F = Task
)
import { Task } from 'fp-ts/Task'
const applicativeTask = {
map: <A, B>(fa: Task<A>, f: (a: A) => B): Task<B> => () => fa().then(f),
of: <A>(a: A): Task<A> => () => Promise.resolve(a),
ap: <A, B>(fab: Task<(a: A) => B>, fa: Task<A>): Task<B> => () =>
Promise.all([fab(), fa()]).then(([f, a]) => f(a))
}
提升
因此,给定一个F
的Apply
实例,我们现在可以编写liftA2
吗?
import { HKT } from 'fp-ts/HKT'
import { Apply } from 'fp-ts/Apply'
type Curried2<B, C, D> = (b: B) => (c: C) => D
function liftA2<F>(
F: Apply<F>
): <B, C, D>(g: Curried2<B, C, D>) => Curried2<HKT<F, B>, HKT<F, C>, HKT<F, D>> {
return g => fb => fc => F.ap(F.map(fb, g), fc)
}
太好了!但是有三个参数的函数怎么办?我们需要另一种抽象吗?
好消息是不需要,Apply
足够了
type Curried3<B, C, D, E> = (b: B) => (c: C) => (d: D) => E
function liftA3<F>(
F: Apply<F>
): <B, C, D, E>(
g: Curried3<B, C, D, E>
) => Curried3<HKT<F, B>, HKT<F, C>, HKT<F, D>, HKT<F, E>> {
return g => fb => fc => fd => F.ap(F.ap(F.map(fb, g), fc), fd)
}
实际上,给定一个Apply
实例,我们可以编写一个liftAn
函数,对于每个n
。
注意。liftA1
就是lift
,Functor
操作。
我们现在可以更新我们的“组合表”
其中liftA1 = lift
一般问题解决了吗?
还没有。仍然有一个重要的案例缺失:如果两个程序都是有效应的怎么办?
再次我们需要更多的东西:在下一篇文章中,我将讨论函数式编程最重要的抽象之一:单子。
TLDR:函数式编程就是关于组合