函数设计:组合器

2025-05-27

函数设计:组合器

在本文中,“组合器”指的是组合器模式

一种以组合事物为核心的库组织方式。通常包含一些 类型T、一些 类型的“原始”值T,以及一些“组合器”,它们可以以各种方式组合 类型的值T,从而构建更复杂的 类型的值。T

所以组合器的一般形状是

combinator: Thing -> Thing
Enter fullscreen mode Exit fullscreen mode

组合器的目标是从先前定义的“事物”中创建新的“事物”。

由于结果可以作为输入传回,因此您可以获得多种可能性的组合,这使得该模式非常强大。

如果将多个组合器混合搭配在一起,则会得到更大的组合爆炸。

因此,您可能经常在功能模块中发现一种设计

  • 一小组非常简单的“原语”
  • 一组“组合器”,用于将它们组合成更复杂的结构

让我们看一些例子。

例 1:Eq

组合器:给定一个forgetEq实例,我们可以派生一个for实例EqAEqReadonlyArray<A>

import { Eq, fromEquals } from 'fp-ts/Eq'

export function getEq<A>(E: Eq<A>): Eq<ReadonlyArray<A>> {
  return fromEquals(
    (xs, ys) =>
      xs.length === ys.length && xs.every((x, i) => E.equals(x, ys[i]))
  )
}
Enter fullscreen mode Exit fullscreen mode

用法

/** a primitive `Eq` instance for `number` */
export const eqNumber: Eq<number> = {
  equals: (x, y) => x === y
}

// derived
export const eqNumbers: Eq<ReadonlyArray<number>> = getEq(eqNumber)

// derived
export const eqNumbersNumbers: Eq<ReadonlyArray<ReadonlyArray<number>>> = getEq(
  eqNumbers
)

// derived
export const eqNumbersNumbersNumbers: Eq<ReadonlyArray<
  ReadonlyArray<ReadonlyArray<number>>
>> = getEq(eqNumbersNumbers)

// etc...
Enter fullscreen mode Exit fullscreen mode

另一个组合器:给定一个forcontramap的实例和一个从到 的函数,我们可以导出一个for的实例EqABAEqB

import { Eq, fromEquals } from 'fp-ts/Eq'

export const contramap = <A, B>(f: (b: B) => A) => (E: Eq<A>): Eq<B> =>
  fromEquals((x, y) => E.equals(f(x), f(y)))
Enter fullscreen mode Exit fullscreen mode

用法

import { contramap, Eq } from 'fp-ts/Eq'
import { pipe } from 'fp-ts/function'
import * as N from 'fp-ts/number'
import * as RA from 'fp-ts/ReadonlyArray'

export interface User {
  id: number
  name: string
}

export const eqUser: Eq<User> = pipe(
  N.Eq,
  contramap((user: User) => user.id)
)

export const eqUsers: Eq<Array<User>> = RA.getEq(eqUser)
Enter fullscreen mode Exit fullscreen mode

示例 2:Monoid

组合器:给定一个forgetMonoid实例,我们可以派生一个for实例MonoidAMonoidIO<A>

import { IO } from 'fp-ts/IO'
import { Monoid } from 'fp-ts/Monoid'

export function getMonoid<A>(M: Monoid<A>): Monoid<IO<A>> {
  return {
    concat: (x, y) => () => M.concat(x(), y()),
    empty: () => M.empty
  }
}
Enter fullscreen mode Exit fullscreen mode

我们可以用来getMonoid派生另一个组合器replicateIO:给定一个数字和一个类型的n动作,我们可以派生出执行次数的动作mvIO<void>nmv

import { concatAll } from 'fp-ts/Monoid'
import { replicate } from 'fp-ts/ReadonlyArray'

/** a primitive `Monoid` instance for `void` */
export const monoidVoid: Monoid<void> = {
  concat: () => undefined,
  empty: undefined
}

export function replicateIO(n: number, mv: IO<void>): IO<void> {
  return concatAll(getMonoid(monoidVoid))(replicate(n, mv))
}
Enter fullscreen mode Exit fullscreen mode

用法

//
// helpers
//

/** logs to the console */
export function log(message: unknown): IO<void> {
  return () => console.log(message)
}

/** returns a random integer between `low` and `high` */
export const randomInt = (low: number, high: number): IO<number> => {
  return () => Math.floor((high - low + 1) * Math.random() + low)
}

//
// program
//
import { chain } from 'fp-ts/IO'
import { pipe } from 'fp-ts/function'

function fib(n: number): number {
  return n <= 1 ? 1 : fib(n - 1) + fib(n - 2)
}

/** calculates a random fibonacci and prints the result to the console */
const printFib: IO<void> = pipe(
  randomInt(30, 35),
  chain((n) => log(fib(n)))
)

replicateIO(3, printFib)()
/*
1346269
9227465
3524578
*/
Enter fullscreen mode Exit fullscreen mode

示例 3:IO

我们可以为 构建许多其他组合器IO,例如time组合器模仿类似的 Unix 命令:给定一个动作IO<A>,我们可以派生出一个IO<A>将经过的时间打印到控制台的动作

import { IO, Monad } from 'fp-ts/IO'
import { now } from 'fp-ts/Date'
import { log } from 'fp-ts/Console'

export function time<A>(ma: IO<A>): IO<A> {
  return Monad.chain(now, (start) =>
    Monad.chain(ma, (a) =>
      Monad.chain(now, (end) =>
        Monad.map(log(`Elapsed: ${end - start}`), () => a)
      )
    )
  )
}
Enter fullscreen mode Exit fullscreen mode

用法

time(replicateIO(3, printFib))()
/*
5702887
1346269
14930352
Elapsed: 193
*/
Enter fullscreen mode Exit fullscreen mode

带有部分...

time(replicateIO(3, time(printFib)))()
/*
3524578
Elapsed: 32
14930352
Elapsed: 125
3524578
Elapsed: 32
Elapsed: 189
*/
Enter fullscreen mode Exit fullscreen mode

我们能让组合子更加通用吗?下一篇文章time我们会看到答案

文章来源:https://dev.to/gcanti/functions-design-combinators-14pn
PREV
编写出色的 Angular 组件的原则
NEXT
VueUse 是 Vue 3 的必备库