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

Getting started with fp-ts: Applicative

原文: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实例。

程序 f

程序 g

组合

g ∘ f

有效应

纯(一元)

lift(g) ∘ f

然而,g必须是一元的,也就是说,它只能接受一个参数作为输入。如果g接受两个参数怎么办?我们是否仍然可以使用仅functor实例来提升g?让我们尝试一下!

柯里化(Currying)

首先,我们必须模拟一个接受两个参数的函数,假设类型为BC(我们可以使用元组)并返回类型D的值

g: (args: [B, C]) => D

我们可以使用一种称为柯里化的技术来重写g

柯里化是将接受多个参数的函数的求值转换为评估一系列函数的技术,每个函数只有一个参数。例如,通过柯里化,一个接受两个参数的函数,一个来自B,一个来自C,并在D中产生输出,被转换为接受单个参数来自C并产生从BC的函数作为输出的函数。

(来源: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>的参数或通过提升类型BC的值来调用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))
}

提升

因此,给定一个FApply实例,我们现在可以编写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就是liftFunctor操作。

我们现在可以更新我们的“组合表”

程序 f

程序 g

组合

g ∘ f

有效应

纯,n

liftAn(g) ∘ f

其中liftA1 = lift

一般问题解决了吗?

还没有。仍然有一个重要的案例缺失:如果两个程序都是有效应的怎么办?

再次我们需要更多的东西:在下一篇文章中,我将讨论函数式编程最重要的抽象之一:单子

TLDR:函数式编程就是关于组合


评论