fp-ts入门:Applicative
柯里化
申请
应用
起重
一般问题解决了吗?
在上一篇文章中,我们看到,我们可以f: (a: A) => F<B>
将纯程序提升为一个提供函子实例的函数,g: (b: B) => C
从而组成一个有效的程序。g
lift(g): (fb: F<B>) => F<C>
F
计划 f | 程序g | 作品 |
---|---|---|
纯的 | 纯的 | g ∘ f |
有效的 | 纯(一元) | lift(g) ∘ f |
但是g
必须是一元的,也就是说它只能接受一个参数作为输入。如果g
接受两个参数呢?我们还能g
只使用函子实例来提升吗?好吧,我们试试看!
柯里化
首先,我们必须建立一个函数,该函数接受两个参数,假设类型为B
and C
(我们可以使用元组),并返回一个类型为D
g: (args: [B, C]) => D
我们可以使用一种称为curryingg
的技术来重写。
柯里化是一种将一个接受多个参数的函数的求值转换为一系列函数求值的技术,每个函数只有一个参数。例如,一个函数接受两个参数,一个来自
B
,一个来自C
,输出为D
,通过柯里化,它被转换为一个接受一个参数的函数,输出为C
,输出B
为C
。
(来源:wikipedia.org 上的 currying)
所以我们可以重写g
为
g: (b: B) => (c: C) => D
我们想要的是一个提升操作,liftA2
为了将它与旧的 区分开来lift
,我们不要调用它,它输出具有以下签名的函数
liftA2(g): (fb: F<B>) => (fc: F<C>) => F<D>
我们怎么才能做到这一点?因为g
现在是一元的,我们可以使用函子实例和我们之前的lift
lift(g): (fb: F<B>) => F<(c: C) => D>
但现在我们陷入了困境:没有对函子实例的合法操作能够将值解包F<(c: C) => D>
为函数(fc: F<C>) => F<D>
。
申请
因此,让我们引入一个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>
)。
应用
此外,如果存在一个操作,能够将类型的值提升A
为 类型的值,那就方便了F<A>
。这样,我们可以通过提供和liftA2(g)
类型的参数,或者提升和类型的值来调用该函数。F<B>
F<C>
B
C
因此,让我们介绍一下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))
}
起重
Apply
那么,给定一个for的实例,F
我们现在可以写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
操作。
我们现在可以更新我们的“组成表”
计划 f | 程序g | 作品 |
---|---|---|
纯的 | 纯的 | g ∘ f |
有效的 | 纯的,n -ary |
liftAn(g) ∘ f |
liftA1 = lift
一般问题解决了吗?
还没有。还有一个重要的案例没有解决:如果两个方案都有效怎么办?
我们再次需要更多的东西:在下一篇文章中,我将讨论函数式编程最重要的抽象之一:monads。
TLDR:函数式编程就是组合
鏂囩珷鏉ユ簮锛�https://dev.to/gcanti/getting-started-with-fp-ts-applicative-1kb3