- 开发工具
【免费下载链接】fp-ts
Functional programming in TypeScript
导读
ReaderEither是 fp-ts 中把Reader(从环境中读取配置/依赖的函数式能力)与Either(可中断的类型化错误通道)组合起来的核心数据类型。它让你可以编写"读取配置 → 执行可能失败的运算"的纯函数式代码,错误以Left值显式传递而无需抛出异常,依赖以参数形式注入便于测试。读完本文,你将掌握ReaderEither<R, E, A>的模型、全套构造器与转换器、do notation 链式写法、错误处理策略(含 Validation 聚合模式)以及数组遍历等实战技能。
1. 模型:ReaderEither<R, E, A>是什么
ReaderEither的定义极其简洁,见 src/ReaderEither.ts:
export interface ReaderEither<R, E, A> extends Reader<R, Either<E, A>> {}它本质上是一个"先读环境、再出结果"的函数:
R:环境类型(Environment),即依赖/配置的类型,运行时通过调用ma(config)注入;E:错误类型(Error),失败时产生Left<E>;A:成功值类型(Success),成功时产生Right<A>。
由于它继承自Reader<R, Either<E, A>>,调用一个ReaderEither时必须传入环境值,返回Either<E, A>。这使得"依赖注入"和"错误处理"被显式地编码在类型里:只要函数签名中出现ReaderEither<R, E, A>,调用方就知道它需要什么样的配置、可能以什么方式失败。
与之配套的类型 lambda 定义在 src/ReaderEither.ts:
export const URI = 'ReaderEither' export type URI = typeof URI并通过URItoKind3注册到 HKT 系统中(src/ReaderEither.ts),因此它可以在泛型编程中以F<R, E, A>的 kind 形式参与组合。
2. 构造器:如何创建 ReaderEither 值
文档的constructors分类提供了一组入口(均自 v2.0.0 起提供,of为 v2.8.5):
| 函数 | 签名(简化) | 作用 |
|---|---|---|
right | <R, E = never, A = never>(a: A) => ReaderEither<R, E, A> | 直接放入成功值,忽略环境 |
left | <R, E = never, A = never>(e: E) => ReaderEither<R, E, A> | 直接放入错误值,忽略环境 |
of | <R = unknown, E = never, A = never>(a: A) => ReaderEither<R, E, A> | right的别名,Pointed/Monad 的of |
rightReader | <R, E = never, A = never>(ma: Reader<R, A>) => ReaderEither<R, E, A> | 把 Reader 的成功结果提升为Right |
leftReader | <R, E = never, A = never>(me: Reader<R, E>) => ReaderEither<R, E, A> | 把 Reader 的返回值作为错误放入Left |
ask | <R, E = never>() => ReaderEither<R, E, R> | 读取整个环境,作为成功值返回 |
asks | <R, A, E = never>(f: (r: R) => A) => ReaderEither<R, E, A> | 从环境投影出一个值作为成功值 |
asksReaderEither | <R, E, A>(f: (r: R) => ReaderEither<R, E, A>) => ReaderEither<R, E, A> | 效果化地访问环境:f返回 ReaderEither |
asksReaderEitherW | <R1, R2, E, A>(f: (r1: R1) => ReaderEither<R2, E, A>) => ReaderEither<R1 & R2, E, A> | asksReaderEither的宽松版本,环境类型取交集合并 |
ask与asks是依赖注入的核心:例如在文档示例(docs/modules/ReaderEither.ts.md)与源码tapEither的用例中,RE.ask<number>()读取环境中的最小长度配置,再交给后续校验逻辑使用。
从源码看,这些构造器大多是对EitherT(src/EitherT.ts)的薄封装,例如 src/ReaderEither.ts:
export const left: <R, E = never, A = never>(e: E) => ReaderEither<R, E, A> = ET.left(R.Pointed) export const right: <R, E = never, A = never>(a: A) => ReaderEither<R, E, A> = ET.right(R.Pointed) export const rightReader = ET.rightF(R.Functor) export const leftReader = ET.leftF(R.Functor)3. 转换:与 Either、Option、Reader 互操作
conversions分类提供了 4 个转换函数:
fromEither(v2.0.0):<E, A, R = unknown>(fa: Either<E, A>) => ReaderEither<R, E, A>,把Either原样提升进 Reader 环境。从源码看它就是R.of(src/ReaderEither.ts),即"忽略环境、返回该 Either"。fromOption(v2.0.0):<E>(onNone: LazyArg<E>) => <A, R = unknown>(fa: Option<A>) => ReaderEither<R, E, A>,Some变Right,None时调用onNone惰性生成错误。fromReader(v2.11.0):<R, A, E = never>(fa: Reader<R, A>) => ReaderEither<R, E, A>,把不会失败的 Reader 提升为永不失败(E = never)的 ReaderEither。toUnion(v2.10.0):<R, E, A>(fa: ReaderEither<R, E, A>) => Reader<R, E | A>,把Either<E, A>的左右两边压平成联合类型,便于丢弃错误语义、只取"要么错误要么值"。
测试 test/ReaderEither.ts 验证了fromOption的行为:O.none经fromOption(() => 'none')后调用({})得到E.left('none');O.some(1)得到E.right(1)。
4. 模式匹配:match / matchE / fold
pattern matching分类解决"如何消费 ReaderEither"的问题:
| 函数 | 说明 |
|---|---|
match(v2.10.0) | <E, B, A>(onLeft: (e: E) => B, onRight: (a: A) => B) => <R>(ma) => Reader<R, B>,两个处理器返回普通值,最终得到一个 Reader |
matchW(v2.10.0) | match的宽松版,两个处理器返回值可以不同,结果类型合并为B \| C |
matchE(v2.10.0) | 后缀E表示Effect:处理器返回Reader<R, B>,用于在分支内继续执行效果 |
matchEW(v2.10.0) | matchE的宽松版,环境与返回类型分别取交集/合并 |
fold(v2.0.0) | matchE的别名(历史命名) |
foldW(v2.10.0) | matchEW的别名 |
从源码看,这些函数通过ET.match(R.Functor)、ET.matchE(R.Monad)组合实现(src/ReaderEither.ts),fold = matchE、foldW = matchEW是直接的函数别名。
5. 映射与副作用:map / as / asUnit / flap / tap 家族
5.1 成功值映射
map(v2.0.0):<A, B>(f: (a: A) => B) => <R, E>(fa) => ReaderEither<R, E, B>,只映射Right分支。as(v2.16.0):把Right值替换为指定常量。asUnit(v2.16.0):把Right值替换为void。flap(v2.10.0):<A>(a: A) => <R, E, B>(fab: ReaderEither<R, E, (a: A) => B>) => ReaderEither<R, E, B>,把值应用到容器内的函数上(函数在上下文中)。
测试 test/ReaderEither.ts 展示了map的 pipeable 用法:pipe(_.right(1), _.map(U.double))({})得到E.right(2)。
5.2 错误通道映射
mapError(v2.16.0):<R, E, G>(f: (e: E) => G) => <A>(self) => ReaderEither<R, G, A>,只映射Left分支,常用于把底层错误转换为业务错误类型。文档给出了完整示例:ReaderEither.mapError(ReaderEither.left('err'), f)({})得到Either.left(new Error('err')),其中f = (s: string) => new Error(s)。mapLeft(v2.0.0):mapError的别名(legacy)。mapBoth(v2.16.0):<E, G, A, B>(f: (e: E) => G, g: (a: A) => B) => <R>(self) => ReaderEither<R, G, B>,同时映射失败与成功两个通道。文档示例:ReaderEither.mapBoth(ReaderEither.right(1), f, g)({})得Either.right(2);ReaderEither.mapBoth(ReaderEither.left('err'), f, g)({})得Either.left(new Error('err'))。bimap(v2.0.0):mapBoth的别名(legacy)。
在源码中bimap = mapBoth、mapLeft = mapError均为直接别名(src/ReaderEither.ts),测试 test/ReaderEither.ts 覆盖了mapLeft与bimap的左右分支行为。
5.3 tap 系列:保留原结果、附带执行副作用
tap家族的特点是"顺序执行后续计算但保留第一个计算的结果"(类似打点/日志/审计):
tap(v2.15.0):<R1, E1, A, R2, E2, _>(self, f: (a: A) => ReaderEither<R2, E2, _>) => ReaderEither<R1 & R2, E1 | E2, A>,f返回 ReaderEither。tapEither(v2.16.0):f返回Either<E2, _>,用于把纯 Either 校验"接到"流上。源码中的完整示例(src/ReaderEither.ts):
import * as E from 'fp-ts/Either' import { pipe } from 'fp-ts/function' import * as RE from 'fp-ts/ReaderEither' const checkString = (value: string) => pipe( RE.ask<number>(), // 从环境读取最小长度 RE.tapEither( (minLength) => value.length > minLength ? E.right('ok') : E.left('error') ) ) assert.deepStrictEqual(checkString('')(1), E.left('error')) // '': 长度 0 > 1 为假 → 失败 assert.deepStrictEqual(checkString('fp-ts')(2), E.right(2)) // 'fp-ts': 长度 5 > 2 为真 → 保留原环境值 2注意tapEither失败时结果是E.left('error'),而成功时保留的是第一个计算的结果(环境值2),这正是 "tap(窥探)" 的语义。
tapReader(v2.16.0):f返回Reader<R2, _>,用于附带执行一个只读环境、不会失败的副作用计算。
6. 顺序组合:flatMap 系列与 do notation
6.1 flatMap 与 flatten
flatMap(v2.14.0,支持>export declare const flatMap: { <A, R2, E2, B>(f: (a: A) => ReaderEither<R2, E2, B>): <R1, E1>( ma: ReaderEither<R1, E1, A> ) => ReaderEither<R1 & R2, E1 | E2, B> <R1, E1, A, R2, E2, B>(ma: ReaderEither<R1, E1, A>, f: (a: A) => ReaderEither<R2, E2, B>): ReaderEither< R1 & R2, E1 | E2, B > }
它的类型揭示了组合规律:环境取交集R1 & R2(两边都需要满足),错误取并集E1 | E2(任一步都可能失败)。
flatMap的专用变体可以接入其他单子风格的计算:
flatMapEither(v2.15.0):f: (a: A) => Either<E2, B>。flatMapOption(v2.15.0):f: (a: A) => Option<B>,None时用onNone生成错误。flatMapNullable(v2.15.0):f: (a: A) => B | null | undefined,空值时用onNullable生成错误。flatMapReader(v2.16.0):f: (a: A) => Reader<R2, B>,把纯 Reader 接入链中。
flatten(v2.0.0)/flattenW(v2.11.0)则是"嵌套展开":flattenW接收ReaderEither<R1, E1, ReaderEither<R2, E2, A>>,返回ReaderEither<R1 & R2, E1 | E2, A>;flatten = flattenW(因为flattenW由flatMap(identity)实现,见 src/ReaderEither.ts)。测试 test/ReaderEither.ts 验证了flatten与flattenW。
6.2 do notation:Do / bind / bindTo / let / apS
do notation 让链式调用看起来像命令式代码。起点是Do(v2.9.0):ReaderEither<unknown, never, {}>,即"空环境、不失败、空记录"的初始值。
bindTo(v2.8.0):<N>(name: N) => <R, E, A>(fa) => ReaderEither<R, E, { readonly [K in N]: A }>,把当前值绑定到字段名。bind(v2.8.0):<N, A, R, E, B>(name, f: (a: A) => ReaderEither<R, E, B>) => (ma) => ...,把f的结果绑定为新字段,字段名不能与已有字段冲突(Exclude<N, keyof A>)。bindW(v2.8.0):bind的宽松版,环境与错误类型合并(R1 & R2、E1 | E2)。let(v2.13.0):<N, A, B>(name, f: (a: A) => B) => ...,用纯函数派生新字段(不进入错误通道)。apS(v2.8.0):<N, A, R, E, B>(name, fb: ReaderEither<R, E, B>) => (fa) => ...,把独立的 ReaderEither 结果绑定为新字段,适用于"并行"组合多个独立计算。apSW(v2.8.0):apS的宽松版。
字段类型均为readonly,组合后得到只读记录。ApT(v2.11.0)则是空元组ReaderEither<unknown, never, readonly []>,作为遍历空数组时的单位元。
6.3 应用式组合:ap / apFirst / apSecond
ap(v2.0.0):<R, E, A>(fa) => <B>(fab: ReaderEither<R, E, (a: A) => B>) => ReaderEither<R, E, B>。apW(v2.8.0):宽松版,环境取交集、错误取并集。apFirst(v2.0.0):保留第一个的结果。apSecond(v2.0.0):保留第二个的结果。apFirstW/apSecondW(v2.12.0):对应宽松版。
测试 test/ReaderEither.ts 验证了ap、apFirst、apFirstW、apSecond、apSecondW的 pipeable 用法。
7. 错误处理:alt / orElse / tapError / getOrElse
7.1 失败回退
alt(v2.0.0):<R, E, A>(that: () => ReaderEither<R, E, A>) => (fa) => ...,当fa失败时改用that()的结果(that惰性求值)。测试 test/ReaderEither.ts 验证:right('a')走alt后仍为right('a');left(1)走alt后变为right('b')。altW(v2.9.0):宽松版,环境、错误、返回类型全部合并。orElse(v2.0.0):<E1, R, E2, A>(onLeft: (e: E1) => ReaderEither<R, E2, A>) => (ma) => ...,失败时用错误值计算新的 ReaderEither。orElseW(v2.10.0):宽松版。orLeft(v2.11.0):<E1, R, E2>(onLeft: (e: E1) => Reader<R, E2>) => <A>(fa) => ...,失败时用错误值生成一个Reader作为新的错误通道。orLeftW(v2.16.6):宽松版,环境取交集。
7.2 窥探失败:tapError
tapError(v2.15.0):"effectfully peeks at the failure"——失败时执行副作用计算但保留原错误:
export declare const tapError: { <E1, R2, E2, _>(onLeft: (e: E1) => ReaderEither<R2, E2, _>): <R1, A>( self: ReaderEither<R1, E1, A> ) => ReaderEither<R1 & R2, E1 | E2, A> ... }适合在失败分支中记录日志、发送告警,之后链仍以原错误失败。
7.3 兜底收敛:getOrElse
getOrElse(v2.0.0):<E, R, A>(onLeft: (e: E) => Reader<R, A>) => (ma: ReaderEither<R, E, A>) => Reader<R, A>,把 ReaderEither 收敛为永不失败的 Reader:失败时用onLeft提供默认值,成功时取Right值。getOrElseW(v2.6.0)是宽松版,onLeft的返回值可以与成功值类型不同(合并为A | B)。
7.4 校验聚合:getApplicativeReaderValidation / getAltReaderValidation
默认的Applicative实例遇到多个错误时只返回第一个错误,默认的Alt实例只返回最后一个错误。若想聚合所有错误,需要提供Semigroup<E>来拼接错误:
getApplicativeReaderValidation<E>(S: Semigroup<E>): Applicative3C<URI, E>(v2.7.0):让ap通过Semigroup合并错误。getAltReaderValidation<E>(S: Semigroup<E>): Alt3C<URI, E>(v2.7.0):让alt通过Semigroup合并错误。
从源码看,二者分别基于E.getApplicativeValidation(S)(src/ReaderEither.ts)与ET.altValidation(R.Monad, S)(src/ReaderEither.ts)实现。这是实现"表单校验收集所有字段错误"等场景的标准手段。
8. 过滤与筛选:filterOrElse / fromPredicate / getCompactable / getFilterable
fromPredicate(v2.0.0):<E, A, B extends A>(refinement, onFalse) => <R = unknown>(a: A) => ReaderEither<R, E, B>,把纯值按谓词/类型守卫包装:通过则Right,否则Left(onFalse(a))。也支持只传Predicate的重载。filterOrElse(v2.0.0):对已有的 ReaderEither 施加谓词/类型守卫过滤,不满足时用onFalse生成错误。支持Refinement重载(类型收窄为B extends A)与Predicate重载。filterOrElseW(v2.9.0):宽松版,错误类型与原有错误合并为E1 | E2。getCompactable<E>(M: Monoid<E>): Compactable3C<'ReaderEither', E>(v2.10.0):提供compact/separate,用Monoid<E>决定空值时的错误。getFilterable<E>(M: Monoid<E>): Filterable3C<URI, E>(v2.10.0):在 Compactable 之上再提供filter/filterMap/partition/partitionMap。源码基于E.getFilterable(M)与E.getCompactable(M)组合实现(src/ReaderEither.ts)。
9. 提升(lifting)与 legacy 别名
9.1 lifting 分类
fromEitherK(v2.4.0):<E, A extends readonly unknown[], B>(f: (...a: A) => Either<E, B>) => <R = unknown>(...a: A) => ReaderEither<R, E, B>,把返回 Either 的普通函数提升为返回 ReaderEither 的函数。fromReaderK(v2.11.0):把返回 Reader 的函数提升进 ReaderEither。fromPredicate(v2.0.0):见上文。liftNullable(v2.15.0):<A, B, E>(f: (...a: A) => B | null | undefined, onNullable) => <R>(...a: A) => ReaderEither<R, E, NonNullable<B>>,把可能返回空值的函数提升为失败感知的函数。liftOption(v2.15.0):把返回Option<B>的函数提升为 ReaderEither,None时用onNone生成错误。
这些函数在源码中通过_.liftNullable(_FromEither)、_.liftOption(_FromEither)实现(src/ReaderEither.ts)。
9.2 legacy 分类(旧命名,全部为别名)
| 旧名 | 新名 | 引入版本 |
|---|---|---|
bimap | mapBoth | v2.0.0 |
chain/chainW | flatMap | v2.0.0 / v2.6.0 |
chainEitherK/chainEitherKW | flatMapEither | v2.4.0 / v2.6.1 |
chainFirst/chainFirstW | tap | v2.0.0 / v2.8.0 |
chainFirstEitherK/chainFirstEitherKW | tapEither | v2.12.0 |
chainFirstReaderK/chainFirstReaderKW | tapReader | v2.11.0 |
chainReaderK/chainReaderKW | flatMapReader | v2.11.0 |
mapLeft | mapError | v2.0.0 |
orElseFirst/orElseFirstW | tapError | v2.11.0 |
fromOptionK | liftOption | v2.10.0 |
chainOptionK/chainOptionKW | flatMapOption | v2.10.0 / v2.13.2 |
从源码看这些 legacy 导出几乎都是对新函数的直接赋值(例如export const chain: ... = flatMap,src/ReaderEither.ts),迁移到新命名零成本。
10. 实例(instances)与类型类体系
ReaderEither注册了一整套类型类实例,均可从 src/ReaderEither.ts 直接导入,供泛型代码按需取用:
| 实例 | 版本 | 组成 |
|---|---|---|
Functor | v2.7.0 | map |
Pointed | v2.10.0 | of |
Apply | v2.10.0 | map+ap |
Applicative | v2.7.0 | map+ap+of |
Chain | v2.10.0 | map+ap+flatMap |
Monad | v2.7.0 | map+ap+of+flatMap |
MonadThrow | v2.7.0 | Monad +throwError |
Alt | v2.7.0 | map+alt |
Bifunctor | v2.7.0 | mapBoth+mapError |
FromEither | v2.10.0 | fromEither |
FromReader | v2.11.0 | fromReader |
throwError(v2.7.0)即left的别名(src/ReaderEither.ts),MonadThrow由Monad加上throwError构成(src/ReaderEither.ts)。
11. 遍历与数组工具
traversing分类提供数组级操作(把ReaderEither数组聚合成一个 ReaderEither):
sequenceArray(v2.9.0):<R, E, A>(arr: readonly ReaderEither<R, E, A>[]) => ReaderEither<R, E, readonly A[]>,把一组 ReaderEither 倒转成包含结果数组的单个 ReaderEither;任一项失败则整体失败。traverseArray(v2.9.0):f: (a: A) => ReaderEither<R, E, B>,映射并收集。traverseArrayWithIndex(v2.9.0):带索引版本。traverseReadonlyArrayWithIndex(v2.11.0):ReadonlyArray版本的带索引遍历。traverseReadonlyNonEmptyArrayWithIndex(v2.11.0):ReadonlyNonEmptyArray版本,保证返回非空数组。
源码实现是组合Reader的遍历与Either的遍历:flow(R.traverseReadonlyNonEmptyArrayWithIndex(f), R.map(E.traverseReadonlyNonEmptyArrayWithIndex(SK)))(src/ReaderEither.ts),空数组时返回ApT。sequenceArray定义为traverseArray(identity)(src/ReaderEither.ts)。
12. 实用工具:local / swap / ap 家族
local(v2.0.0):<R2, R1>(f: (r2: R2) => R1) => <E, A>(ma: ReaderEither<R1, E, A>) => ReaderEither<R2, E, A>,类似Contravariant的contramap:在执行ma期间改变局部环境。适合"子作用域内覆盖配置"的场景,源码即R.local(src/ReaderEither.ts)。swap(v2.0.0):<R, E, A>(ma: ReaderEither<R, E, A>) => ReaderEither<R, A, E>,交换错误与成功通道。ap/apW/apFirst/apFirstW/apSecond/apSecondW:见 6.3 节。
13. zone of death(已废弃 API)
文档将以下 API 标记为废弃(@deprecated),新代码不应使用:
readerEither(v2.0.0,废弃):合并了Monad、Bifunctor、Alt、MonadThrow的"巨型实例"。官方建议改为按需传入小实例,例如"如果函数只需要Functor,就传RE.Functor而不是RE.readerEither"。getApplySemigroup(v2.0.0,废弃):改用Apply.ts的getApplySemigroup。getApplyMonoid(v2.0.0,废弃):改用Applicative.ts的getApplicativeMonoid。getSemigroup(v2.0.0,废弃):改用getApplySemigroup。getReaderValidation(v2.3.0,废弃):改用getApplicativeReaderValidation与getAltReaderValidation两个小实例组合。
14. 实战组合示例
把上述能力组合起来,一个典型的"读取配置 → 校验 → 业务计算 → 错误兜底"流程如下:
import * as E from 'fp-ts/Either' import * as RE from 'fp-ts/ReaderEither' import { pipe } from 'fp-ts/function' // 环境:应用配置 interface Config { readonly minLength: number readonly dbUrl: string } // 1. 读取配置并用 do notation 组装 const program = pipe( RE.Do, RE.bind('minLength', () => RE.asks((c: Config) => c.minLength)), RE.let('upper', (s) => s.toUpperCase()), // 纯派生 RE.bind('validated', ({ upper }) => RE.filterOrElse( (s: string) => s.length > 2, () => 'too-short' )(RE.right(upper)) // 校验失败进入 Left ) ) // 2. 调用时注入环境,得到 Either const result: E.Either<string, { readonly minLength: number; readonly upper: string; readonly validated: string }> = program({ minLength: 3, dbUrl: '...' }) // 3. 失败时兜底为默认值(收敛为 Reader) const fallback = pipe( program, RE.getOrElse((e) => `error: ${e}`) ) fallback({ minLength: 3, dbUrl: '...' })若需要聚合多个校验错误,可用RE.getApplicativeReaderValidation(S.semigroupString)构造 Applicative 后与apS组合,收集全部字段错误而非只取第一个。
结语
ReaderEither把依赖注入(Reader)与显式错误(Either)封装在一个纯函数类型中,是 fp-ts 中编写可测试、类型安全业务逻辑的主力工具。掌握其模型、构造器、do notation、错误处理与 Validation 聚合模式后,你可以在 src/ReaderEither.ts 与 test/ReaderEither.ts 中查阅每个 API 的实现与行为验证,并配合 docs/guides/do-notation.md 与 docs/guides/HKT.md 深入理解其背后的类型类机制。
- 开发工具
【免费下载链接】fp-ts
Functional programming in TypeScript
相关推荐
如何用 Flox 和 hogli start 快速启动 PostHog 本地开发环境
如何用 Flox 和 hogli start 快速启动 PostHog 本地开发环境 如果你的任务是在本机搭起一套可开发的 PostHog(注意:本流程面向"开
开发工具Newsroom数据集上的RLSeq2Seq应用:预训练到强化学习的完整流程指南
Newsroom数据集上的RLSeq2Seq应用:预训练到强化学习的完整流程指南 想要构建一个能够自动生成高质量新闻摘要的AI系统吗?RLSeq2Seq项目为你
开发工具视频修复终极指南:10分钟掌握untrunc开源工具拯救损坏视频
视频修复终极指南:10分钟掌握untrunc开源工具拯救损坏视频 你是否曾因为相机断电、存储卡故障或传输中断而丢失珍贵的视频回忆?当那些承载着重要时刻的MP4、
音视频视频处理
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考