函数设计:组合器
在本文中,“组合器”指的是组合器模式
一种以组合事物为核心的库组织方式。通常包含一些 类型
T
、一些 类型的“原始”值T
,以及一些“组合器”,它们可以以各种方式组合 类型的值T
,从而构建更复杂的 类型的值。T
所以组合器的一般形状是
combinator: Thing -> Thing
组合器的目标是从先前定义的“事物”中创建新的“事物”。
由于结果可以作为输入传回,因此您可以获得多种可能性的组合,这使得该模式非常强大。
如果将多个组合器混合搭配在一起,则会得到更大的组合爆炸。
因此,您可能经常在功能模块中发现一种设计
- 一小组非常简单的“原语”
- 一组“组合器”,用于将它们组合成更复杂的结构
让我们看一些例子。
例 1:Eq
组合器:给定一个forgetEq
实例,我们可以派生一个for实例Eq
A
Eq
ReadonlyArray<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]))
)
}
用法
/** 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...
另一个组合器:给定一个forcontramap
的实例和一个从到 的函数,我们可以导出一个for的实例Eq
A
B
A
Eq
B
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)))
用法
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)
示例 2:Monoid
组合器:给定一个forgetMonoid
实例,我们可以派生一个for实例Monoid
A
Monoid
IO<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
}
}
我们可以用来getMonoid
派生另一个组合器replicateIO
:给定一个数字和一个类型的n
动作,我们可以派生出执行次数的动作mv
IO<void>
n
mv
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))
}
用法
//
// 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
*/
示例 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)
)
)
)
}
用法
time(replicateIO(3, printFib))()
/*
5702887
1346269
14930352
Elapsed: 193
*/
带有部分...
time(replicateIO(3, time(printFib)))()
/*
3524578
Elapsed: 32
14930352
Elapsed: 125
3524578
Elapsed: 32
Elapsed: 189
*/
我们能让组合子更加通用吗?下一篇文章time
我们会看到答案。