ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

fp-ts ReaderIO 完全指南:用只读环境组合同步副作用

fp-ts ReaderIO 完全指南:用只读环境组合同步副作用 fp-ts ReaderIO 完全指南用只读环境组合同步副作用【免费下载链接】fp-tsFunctional programming in TypeScript项目地址: https://gitcode.com/gh_mirrors/fp/fp-ts本文围绕 fp-ts 2.x 中在 v2.13.0 引入的ReaderIO模块展开讲解如何将Reader的只读环境注入能力与IO的同步副作用能力组合为单一计算类型并覆盖其全部构造器、转换、组合子、do notation、实例与遍历工具。读完本文你将能熟练地用ReaderIO编写依赖配置环境、同时执行同步副作用日志、随机数、时间等的纯函数式 TypeScript 代码。ReaderIOR, A是 fp-ts 中一个非常实用的组合类型它表示一个读取类型为R的只读环境、执行同步副作用、最终产出类型为A的计算。它由 Reader monad 转换器src/ReaderT.ts以IO为内层 monad 实例化而来在依赖注入与同步副作用之间架起了一座简洁的桥梁。本文以 docs/modules/ReaderIO.ts.md 为骨架结合 src/ReaderIO.ts 源码与 test/ReaderIO.ts 测试展开深入讲解。ReaderIO 是什么一个类型两层语义类型模型在 src/ReaderIO.ts 中ReaderIO被定义为export interface ReaderIOR, A { (r: R): I.IOA }这是一个函数类型接收一个只读环境r: R返回一个IOA。而IOA本身又是一个() A的函数见 src/IO.ts。因此一个ReaderIOR, A在运行时表现为两层函数const effect: ReaderIOConfig, number (config) () config.retries调用方式先传环境得到IO再执行IO得到最终值。即effect(config)()。从源码结构看ReaderIO并没有自己重新实现组合逻辑而是将ReaderT应用于IO的实例I.Pointed、I.Functor、I.Apply、I.Monad。例如of定义在 src/ReaderIO.tsRT.of(I.Pointed)对应 src/ReaderT.ts 中(a) () F.of(a)map定义在 src/ReaderIO.tsRT.map(I.Functor)对应 src/ReaderT.ts 中(f) (fa) (r) F.map(fa(r), f)flatMap定义在 src/ReaderIO.tsdual(2, RT.flatMap(I.Monad))对应 src/ReaderT.ts 中(ma, f) (r) M.chain(ma(r), (a) f(a)(r))。可以看到flatMap的实现把同一个环境r同时传递给了前一个计算和后续函数产生的计算——这正是 Reader 语义中环境贯穿全程的核心体现。与 Reader、IO 的关系类型环境读取同步副作用表示ReaderR, A✅❌(r: R) AIOA❌✅() AReaderIOR, A✅✅(r: R) IOAReaderIO天然继承了这两个模块的语义并在FromReader、FromIO两个 typeclass 的支持下与它们互相转换详见下文conversions一节。构造器constructors进入 ReaderIO 世界本节对应 docs/modules/ReaderIO.ts.md 的 constructors 分类。ask读取整个环境export declare const ask: R() ReaderIOR, Rask不做任何计算直接把整个环境作为结果返回。它的实现是ask_(FromReader)src/ReaderIO.ts底层对应 src/FromReader.ts 中的F.fromReader(R.ask())而Reader.ask在 src/Reader.ts 中即() identity。测试用例验证test/ReaderIO.ts_.asknumber()(1)() // 1asks从环境中投影一个值export declare const asks: R, A(f: (r: R) A) ReaderIOR, Aasks接收一个投影函数f: (r: R) A只从环境中取出需要的部分不附带任何副作用。典型用法是读取配置对象的某个字段import * as RIO from fp-ts/ReaderIO interface Config { readonly apiUrl: string readonly retries: number } const getRetries: RIO.ReaderIOConfig, number RIO.asks((c) c.retries)of纯值提升export declare const of: R unknown, A never(a: A) ReaderIOR, Aof把一个纯值放入ReaderIO上下文忽略环境、无副作用。注意环境类型R默认是unknown因此在类型推断不足时无需显式指定环境。测试test/ReaderIO.ts_.of(1)({})(...) 1。asksReaderIO/asksReaderIOWeffectful 访问环境export declare const asksReaderIO: R, A(f: (r: R) ReaderIOR, A) ReaderIOR, A export declare const asksReaderIOW: R1, R2, A(f: (r1: R1) ReaderIOR2, A) ReaderIOR1 R2, A当从环境中取出配置后还需要接着做一次副作用计算时可以用asksReaderIO。asksReaderIOW是其宽松版本W后缀代表Widening类型拓宽允许投影函数返回的环境类型R2与输入环境R1不同最终合并为交叉类型R1 R2。在源码中asksReaderIO直接复用asksReaderIOWsrc/ReaderIO.ts实现来自R.asksReaderWsrc/Reader.ts 中的宽松版本。转换conversions与 Reader、IO 互通export declare const fromIO: A, R unknown(fa: I.IOA) ReaderIOR, A export declare const fromReader: R, A(fa: R.ReaderR, A) ReaderIOR, AfromIO把一个IOA提升为ReaderIOR, A——环境被忽略IO的副作用原样保留。实现为R.ofsrc/ReaderIO.ts即用 Reader 的of包裹(r) fa。fromReader把一个ReaderR, A提升为ReaderIOR, A——在 Reader 的结果外包一层惰性 IO。实现为RT.fromReader(I.Pointed)src/ReaderIO.ts对应 src/ReaderT.ts 的(ma) flow(ma, F.of)。这两个函数正是FromIO与FromReader两个实例见下文的方法也是flatMapIO、flatMapReader、tapIO、tapReader等组合子的底层依赖。测试验证test/ReaderIO.ts_.fromIO(() 1)({})(...) 1。映射mappingmap、as、asUnit、flapexport declare const map: A, B(f: (a: A) B) R(fa: ReaderIOR, A) ReaderIOR, Bmap将纯函数(a: A) B提升到ReaderIO上下文中变换计算结果而不触碰环境与副作用import { pipe } from fp-ts/function import * as RIO from fp-ts/ReaderIO const doubled pipe(RIO.asks((n: number) n), RIO.map((n) n * 2)) doubled(21)() // 42v2.16.0 起新增了as与asUnitsrc/ReaderIO.tsexport declare const as: { A(a: A): R, _(self: ReaderIOR, _) ReaderIOR, A R, _, A(self: ReaderIOR, _, a: A): ReaderIOR, A } export declare const asUnit: R, _(self: ReaderIOR, _) ReaderIOR, voidas把计算结果替换为指定常量常用于执行副作用后固定返回值asUnit把结果替换为void适合只关心副作用的场景。两者都支持>export declare const flap: A(a: A) E, B(fab: ReaderIOE, (a: A) B) ReaderIOE, B序列化sequencingflatMap 家族flatMapexport declare const flatMap: { A, R2, B(f: (a: A) ReaderIOR2, B): R1(ma: ReaderIOR1, A) ReaderIOR1 R2, B R1, A, R2, B(ma: ReaderIOR1, A, f: (a: A) ReaderIOR2, B): ReaderIOR1 R2, B }flatMap是 monadic 绑定的核心v2.14.0 引入支持>const f (a: string) _.of(a.length) pipe(_.of(foo), _.flatMap(f))({})() // 3 _.flatMap(_.of(foo), f)({})() // 3data-first 形式flatten/flattenWexport declare const flatten: R, A(mma: ReaderIOR, ReaderIOR, A) ReaderIOR, A export declare const flattenW: R1, R2, A(mma: ReaderIOR1, ReaderIOR2, A) ReaderIOR1 R2, Aflatten即flatMap(identity)的特化src/ReaderIO.ts用于拍平返回 ReaderIO 的 ReaderIO。flattenW允许内外层环境不同并合并。flatMapIO/flatMapReaderv2.16.0 引入的两个跨类型绑定组合子export declare const flatMapIO: { A, B(f: (a: A) I.IOB): R(self: ReaderIOR, A) ReaderIOR, B R, A, B(self: ReaderIOR, A, f: (a: A) I.IOB): ReaderIOR, B } export declare const flatMapReader: { A, R2, B(f: (a: A) R.ReaderR2, B): R1(self: ReaderIOR1, A) ReaderIOR1 R2, B R1, A, R2, B(self: ReaderIOR1, A, f: (a: A) R.ReaderR2, B): ReaderIOR1 R2, B }flatMapIO后续步骤是纯IO环境类型保持不变R实现为_.flatMapIO(_FromIO, _FlatMap)src/ReaderIO.tsflatMapReader后续步骤是纯Reader环境类型合并为R1 R2实现为_.flatMapReader(_FromReader, _FlatMap)src/ReaderIO.ts。它们让你无需先把IO/Reader手动fromIO/fromReader提升直接链式衔接。测试test/ReaderIO.tspipe(_.of(1), _.flatMapIO(() I.of(2)))(undefined)() // 2 _.flatMapReader(_.of(1), () R.of(2))(undefined)() // 2组合子combinatorstap 系列与 localtap/tapIO/tapReader三者语义一致按顺序执行后续计算但只保留第一个计算的结果side-effectful 观察/记录即把副作用附着在计算链条上而不改变主数据流。export declare const tap: { R1, A, R2, _(self: ReaderIOR1, A, f: (a: A) ReaderIOR2, _): ReaderIOR1 R2, A A, R2, _(f: (a: A) ReaderIOR2, _): R1(self: ReaderIOR1, A) ReaderIOR2 R1, A } export declare const tapIO: { A, _(f: (a: A) I.IO_): R(self: ReaderIOR, A) ReaderIOR, A R, A, _(self: ReaderIOR, A, f: (a: A) I.IO_): ReaderIOR, A } export declare const tapReader: { R2, A, _(f: (a: A) R.ReaderR2, _): R1(self: ReaderIOR1, A) ReaderIOR1 R2, A R1, A, R2, _(self: ReaderIOR1, A, f: (a: A) R.ReaderR2, _): ReaderIOR1 R2, A }文档中给出的tapIO示例非常直观docs/modules/ReaderIO.ts.md源码注释见 src/ReaderIO.tsimport { pipe } from fp-ts/function import * as RIO from fp-ts/ReaderIO import * as Console from fp-ts/Console // 向 stdout 输出 Hello, fp-ts但结果仍然是环境中的字符串 const effect pipe( RIO.askstring(), RIO.tapIO((value) Console.log(Hello, ${value})) ) async function test() { assert.deepStrictEqual(effect(fp-ts)(), fp-ts) }tap在 v2.15.0 引入tapIO/tapReader在 v2.16.0 引入。测试同样验证了保留第一个结果的语义test/ReaderIO.ts、test/ReaderIO.ts。local修改局部环境export declare const local: R2, R1(f: (r2: R2) R1) A(ma: ReaderIOR1, A) ReaderIOR2, Alocal在子计算执行期间临时改变环境类似Contravariant的contramap执行完毕后环境恢复原样。这在为某个子任务注入子配置的场景中非常有用。实现直接复用R.localsrc/ReaderIO.ts。测试test/ReaderIO.tspipe( _.asks((n: number) n 1), _.local(S.size) // 先把 aaa 变成 3再执行 asks )(aaa)() // 4do notation像命令式一样顺序组合ReaderIO提供了完整的 do notation 支持对应 docs/modules/ReaderIO.ts.md其模式与 fp-ts 全局的 do notation 指南docs/guides/do-notation.md一致DoReaderIOunknown, {}即of({})src/ReaderIO.tsdo 块的起点bindTo(name)把当前结果存入记录的name字段bind(name, f)用当前记录计算下一步并存入name字段bindWbind的宽松版本允许合并环境类型apS(name, fb)用 applicative 方式并行绑定一个独立计算apSWapS的宽松版本。测试中的经典用法test/ReaderIO.tspipe( _.of(1), _.bindTo(a), _.bind(b, () _.of(b)) )(undefined)() // { a: 1, b: b } pipe(_.of(1), _.bindTo(a), _.apS(b, _.of(b)))(undefined)() // { a: 1, b: b }配合环境与副作用的完整示例import { pipe } from fp-ts/function import * as RIO from fp-ts/ReaderIO import * as Console from fp-ts/Console interface Config { readonly baseUrl: string readonly token: string } const program: RIO.ReaderIOConfig, { readonly url: string; readonly message: string } pipe( RIO.Do, RIO.bind(url, () RIO.asks((c: Config) ${c.baseUrl}/api?token${c.token})), RIO.bind(message, ({ url }) RIO.tapIO(() Console.log(请求地址${url}), RIO.of(done))), RIO.map(({ url }) ({ url, message: done })) )实例instances完整的 typeclass 图谱ReaderIO注册了 9 个实例src/ReaderIO.ts并通过 src/HKT.ts 的URItoKind2模块增强声明src/ReaderIO.ts接入 fp-ts 的 Higher-Kinded Type 体系实例类型提供的操作FunctorFunctor2ReaderIOmapPointedPointed2ReaderIOofApplyApply2ReaderIOmap、apApplicativeApplicative2ReaderIOmap、ap、ofChainchainable.Chain2ReaderIOmap、ap、chainMonadMonad2ReaderIOmap、of、ap、chainMonadIOMonadIO2ReaderIO上述全部 fromIOFromIOFromIO2ReaderIOfromIOFromReaderFromReader2ReaderIOfromReader其中URI常量与类型别名src/ReaderIO.ts是 fp-ts 类型级编程的标识符export const URI ReaderIO export type URI typeof URI这些实例使ReaderIO可以无缝用于 fp-ts 提供的各种泛型工具如sequence、traverse、monoid 组合等也可以被其他 monad transformer 内嵌。提升liftingfromIOK / fromReaderKexport declare const fromIOK: A extends readonly unknown[], B( f: (...a: A) I.IOB ) R unknown(...a: A) ReaderIOR, B export declare const fromReaderK: A extends readonly unknown[], R, B( f: (...a: A) R.ReaderR, B ) (...a: A) ReaderIOR, BfromIOK把接收若干参数、返回IO的函数整体提升为接收同样参数、返回ReaderIO的函数即flow(f, F.fromIO)见 src/FromIO.tsfromReaderK同理提升返回Reader的函数即flow(f, F.fromReader)见 src/FromReader.ts。这类函数也被称为K 风格Kleisli 风格提升常用于把现成的纯 IO/Reader 函数接入 ReaderIO 管线。测试test/ReaderIO.tsconst f _.fromIOK((s: string) I.of(s.length)) pipe(_.of(a), _.chain(f))({})() // 1遍历traversing批量执行 ReaderIOReaderIO提供了一套基于Applicative的数组遍历工具对应 docs/modules/ReaderIO.ts.mdexport declare const sequenceArray: R, A(arr: readonly ReaderIOR, A[]) ReaderIOR, readonly A[] export declare const traverseArray: A, R, B(f: (a: A) ReaderIOR, B) (as: readonly A[]) ReaderIOR, readonly B[] export declare const traverseArrayWithIndex: A, R, B(f: (index: number, a: A) ReaderIOR, B) (as: readonly A[]) ReaderIOR, readonly B[] export declare const traverseReadonlyArrayWithIndex: A, R, B(f: (index: number, a: A) ReaderIOR, B) (as: readonly A[]) ReaderIOR, readonly B[] export declare const traverseReadonlyNonEmptyArrayWithIndex: A, R, B(f: (index: number, a: A) ReaderIOR, B) (as: ReadonlyNonEmptyArrayA) ReaderIOR, ReadonlyNonEmptyArrayBsequenceArray等价于ReadonlyArray#sequence(Applicative)把ReaderIO数组翻转为单个ReaderIO返回数组实现为traverseArray(identity)src/ReaderIO.tstraverseArray等价于ReadonlyArray#traverse(Applicative)traverseArrayWithIndex/traverseReadonlyArrayWithIndex提供索引参数traverseReadonlyNonEmptyArrayWithIndex面向非空数组保持非空结构。从源码看src/ReaderIO.ts数组遍历基于R.traverseReadonlyNonEmptyArrayWithIndex(f)与I.traverseReadonlyNonEmptyArrayWithIndex(SK)的组合实现空数组直接返回ApTof([])。测试验证了遍历与副作用执行顺序test/ReaderIO.tsconst f _.traverseReadonlyArrayWithIndex((i, a: string) _.of(a i)) pipe(input, f)(undefined)() // [a0, b1] pipe([append(1), append(2)], _.sequenceArray)(undefined)() // [1, 2]副作用按序执行工具utilsap 家族与 ApTexport declare const ap: R, A(fa: ReaderIOR, A) B(fab: ReaderIOR, (a: A) B) ReaderIOR, B export declare const apW: R2, A(fa: ReaderIOR2, A) R1, B(fab: ReaderIOR1, (a: A) B) ReaderIOR1 R2, B export declare const apFirst: E, B(second: ReaderIOE, B) A(first: ReaderIOE, A) ReaderIOE, A export declare const apSecond: E, B(second: ReaderIOE, B) A(first: ReaderIOE, A) ReaderIOE, B export declare const ApT: ReaderIOunknown, readonly []apapplicative 应用实现为RT.ap(I.Apply)src/ReaderIO.ts对应 src/ReaderT.ts 的(fa) (fab) (r) F.ap(fab(r), fa(r))——同一环境r同时传给函数与参数apWap的宽松版本允许两侧环境不同并合并apFirst按序执行两个计算保留第一个结果apSecond按序执行两个计算保留第二个结果测试见 test/ReaderIO.tsApTof([])空数组起点遍历实现的基石src/ReaderIO.ts。legacy 兼容层chain 系列别名为兼容 v2.13.0 引入模块时的命名习惯ReaderIO保留了完整的 legacy 别名层src/ReaderIO.ts全部指向现代名称见下表legacy 名称指向说明chainflatMap绑定chainWflatMap宽松绑定chainFirsttap保留第一个结果chainFirstWtap宽松版本chainIOKflatMapIO绑定纯 IO 步骤chainFirstIOKtapIOIO 副作用观察chainReaderKflatMapReader绑定纯 Reader 步骤chainReaderKWflatMapReader宽松版本chainFirstReaderKtapReaderReader 副作用观察chainFirstReaderKWtapReader宽松版本新代码建议直接使用flatMap/tap/flatMapIO/tapIO/flatMapReader/tapReader等现代 API。实践建议与常见模式依赖注入把Config作为环境R所有需要配置的函数返回ReaderIOConfig, A在程序入口统一注入program(config)()。同步副作用隔离日志、计时、随机数等同步副作用放进IO通过fromIO、flatMapIO、tapIO接入保持主逻辑纯函数化。do notation 组织多步流程用DobindapS让依赖多个环境字段的流程线性可读apS适合彼此独立的分支。W 化环境合并当组合的不同计算依赖不同环境子集时优先选用bindW、apSW、apW、flattenW、asksReaderIOW等W后缀函数让最终环境类型自动求交。测试友好ReaderIO是惰性的、同步的测试中可直接effect(config)()断言返回值参考 test/ReaderIO.ts 的用例风格无需 mock 异步调度。小结ReaderIO是 fp-ts 中只读环境 同步副作用组合的标准答案它以 src/ReaderT.ts 转换器为骨架、IO为内层 monad提供了从构造ask/asks/of/asksReaderIO、转换fromIO/fromReader、映射map/as/asUnit/flap、序列化flatMap/flatten/flatMapIO/flatMapReader、观察tap/tapIO/tapReader、do notation、遍历到完整 typeclass 实例的一整套 API。其全部行为均可由 src/ReaderIO.ts 源码与 test/ReaderIO.ts 测试印证环境贯穿每次flatMap、副作用严格延迟到(config)()才执行、W后缀统一做环境类型合并。掌握它你就能在纯函数式风格下优雅地处理依赖配置且伴随同步 I/O的现实问题。【免费下载链接】fp-tsFunctional programming in TypeScript项目地址: https://gitcode.com/gh_mirrors/fp/fp-ts创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表