fp-ts State 模块完全指南用纯函数建模与执行状态计算【免费下载链接】fp-tsFunctional programming in TypeScript项目地址: https://gitcode.com/gh_mirrors/fp/fp-ts导读本文以 fp-ts 仓库中的 State 模块文档 为骨架结合 State 源码实现 与 测试用例系统讲解 fp-ts 中Statemonad 的类型模型、全部构造器与组合子、do notation、数组遍历工具以及实例instances体系。读完本文你将理解StateS, A为什么是状态传递函数而非可变状态容器掌握get / put / modify / gets的底层语义、evaluate / execute的运行方式以及如何借助bind / bindTo / apS / let写出可读性强的命令式风格状态逻辑并能将其嵌入StateReaderTaskEither等组合 monad。1. State 是什么从类型签名看本质fp-ts 在 src/State.ts 中给出的核心模型极其精简export interface StateS, A { (s: S): [A, S] }也就是说StateS, A本身就是一个函数它接收当前状态s: S返回一个二元组[A, S]其中A是本次计算产生的结果valueS是计算完成后的新状态。这个模型有三个关键推论状态是显式传递的没有任何全局可变变量状态作为参数流入、作为返回值流出天然满足引用透明性State是惰性的构造State只是产生一个函数只有真正调用它传入初始状态时计算才发生结果是纯函数同样的s一定得到同样的[A, S]方便测试与推理。从类型层次看State是一个二元类型构造器* - * - *因此在 HKT 系统中属于URIS2范畴。fp-ts 通过 src/State.ts 的模块扩充把State注册进URItoKind2从而让Functor2、Monad2等类型类实例能够以Kind2State, S, A的形式工作declare module ./HKT { interface URItoKind2E, A { readonly [URI]: StateE, A } }其中URI State见 src/State.tsURI类型别名export type URI typeof URI让 State 作为字面量类型参与类型推导。一个贯穿全模块的惯例fp-ts 中StateS, A的纯状态传递通常与S作为首个类型参数、A作为结果类型参数保持一致阅读签名时不要被E错误或R环境等其他模块的字母干扰。2. 构造器constructors五种进入 State 世界的方式文档将构造器分为五类其实现均可从 src/State.ts 中直接核对且全部有对应的测试覆盖test/State.ts。2.1get读取当前状态export declare const get: S() StateS, S实现() (s) [s, s]。它把当前状态同时作为结果和新状态返回即读取操作本身不改变状态。测试_.get()(1)得到[1, 1]。2.2put整体替换状态export declare const put: S(s: S) StateS, void实现(s) () [undefined, s]。把状态无条件设置为传入的s结果值为undefined即void。测试_.put(2)(1)得到[undefined, 2]——初始状态1被丢弃状态变为2。2.3modify按函数变换状态export declare const modify: S(f: (s: S) S) StateS, void实现(f) (s) [undefined, f(s)]。它接收一个EndomorphismS即S - S的自映射函数把f应用到当前状态结果值同样为undefined。测试_.modify(U.double)(1)得到[undefined, 2]。2.4gets从状态投影出结果export declare const gets: S, A(f: (s: S) A) StateS, A实现(f) (s) [f(s), s]。它用f从当前状态计算出一个结果值但状态保持不变。这非常适合读取状态的某个字段场景例如gets((s: Config) s.timeout)。测试_.gets(U.double)(1)得到[2, 1]。2.5of把纯值提升进 Stateexport declare const of: S, A(a: A) StateS, A实现(a) (s) [a, s]。它不触碰状态只把值a作为结果返回状态原样传递。这是Pointed/Applicative/Monad实例的基础对应pure/return的概念。记忆口诀get读状态、put写状态、modify变换状态、gets读状态并投影、of只带值不动状态。3. 运行 Stateevaluate 与 execute构造出的State必须喂一个初始状态才能执行。fp-ts 提供两个运行工具utils自 v2.8.0 起成为推荐 APIexport declare const evaluate: S(s: S) A(ma: StateS, A) A export declare const execute: S(s: S) A(ma: StateS, A) Sevaluate(initial)(ma)运行计算丢弃最终状态只取结果A。源码为ma(s)[0]src/State.tsexecute(initial)(ma)运行计算丢弃结果只取最终状态S。源码为ma(s)[1]src/State.ts。两者都是柯里化风格先传初始状态再传计算与pipe组合时非常自然。测试示例test/State.tsimport { pipe } from ./function import * as _ from ./State pipe(_.ofnumber, string(a), _.evaluate(0)) // a pipe(_.ofnumber, string(a), _.execute(0)) // 0如果只关心状态流转的最终结果execute是首选如果只关心计算结果用evaluate。在 v2.8.0 之前这两个职责由evalState/execState承担见本文第 8 节它们现已被标记为deprecated。4. 组合子与 sequencing把 State 串起来4.1map在结果上变换export declare const map: A, B(f: (a: A) B) E(fa: StateE, A) StateE, B实现src/State.ts为先运行fa(s1)得到[a, s2]再把f应用到a上状态s2原样向后传递。map永远不修改状态只修改结果。测试pipe(x, _.map(U.double))(0)得到[-2, 1]。4.2ap把装着函数的状态应用到装着参数的状态export declare const ap: E, A(fa: StateE, A) B(fab: StateE, (a: A) B) StateE, B实现src/State.ts展示了对状态串行传递的精确控制export const ap (fa) (fab) (s1) { const [f, s2] fab(s1) const [a, s3] fa(s2) return [f(a), s3] }注意执行顺序先运行fab函数方再把得到的新状态s2喂给fa参数方最后状态为s3。这意味着ap下状态按从左到右、先函数后参数的顺序流动。测试pipe(_.of(U.double), _.ap(_.of(1)))(0)得到[2, 0]。4.3flatMap按前一步结果决定后续计算export declare const flatMap: { A, S, B(f: (a: A) StateS, B): (ma: StateS, A) StateS, B S, A, B(ma: StateS, A, f: (a: A) StateS, B): StateS, B }flatMap是 monad 的核心运算。实现src/State.tsexport const flatMap dual(2, (ma, f) (s1) { const [a, s2] ma(s1) return f(a)(s2) })它先运行ma得到结果a和新状态s2然后调用f(a)得到下一个 State并用s2驱动它。注意flatMap使用dual(2, ...)声明因此既支持管道式pipe(ma, flatMap(f))也支持直接调用式flatMap(ma, f)——测试中两种写法均有覆盖test/State.ts。4.4flatten消除一层嵌套export declare const flatten: E, A(mma: StateE, StateE, A) StateE, A实现为flatMap(identity)src/State.ts即把内层 State 直接当作结果返回。测试pipe(_.of(_.of(a)), _.flatten)(0)得到[a, 0]。4.5tap执行副作用式计算但保留原结果export declare const tap: { S, A, _(self: StateS, A, f: (a: A) StateS, _): StateS, A A, S, _(f: (a: A) StateS, _): (self: StateS, A) StateS, A }tap自 v2.15.0 加入语义是顺序组合两个计算用第一个的结果决定第二个但最终只保留第一个的结果。它非常适合记录日志把中间值写进状态再继续这类场景。实现为dual(2, chainable.tap(Chain))src/State.ts由 Chain 模块 的通用tap派生。测试对比很直观test/State.ts设x (s) [s - 1, s 1]、f (_n) (s) [s - 1, s 1]则pipe(x, _.flatMap(f))(0)→[0, 2]结果来自第二个计算f的[s-1, s1]此时s2pipe(x, _.tap(f))(0)→[-1, 2]结果保留第一个计算的s-1 -1状态仍是第二个计算产生的2。4.6apFirst/apSecond只保留一侧结果export declare const apFirst: E, B(second: StateE, B) A(first: StateE, A) StateE, A export declare const apSecond: E, B(second: StateE, B) A(first: StateE, A) StateE, BapFirst(second)(first)依次运行first、second保留第一个的结果先做再丢apSecond(second)(first)依次运行first、second保留第二个的结果。两者的状态都会按顺序累计。测试test/State.ts中pipe(_.of(a), _.apFirst(_.of(b)))(0)得到[a, 0]而_.apSecond版本得到[b, 0]。4.7flap把值应用到函数容器export declare const flap: A(a: A) E, B(fab: StateE, (a: A) B) StateE, Bflap是Functor的反方向变体map是函数在前、容器在后f: A B应用到fa: FAflap则是值在前、装着函数的容器在后把值a塞进fab里的函数。实现为flap_(Functor)src/State.ts由 Functor 模块 的通用flap基于Functor实例派生自 v2.10.0 加入。5. 实例instancesState 在各类型类中的身份fp-ts 推崇小而专的实例State模块导出了 7 个类型类实例全部定义在 src/State.ts实例类型自版本组成FunctorFunctor2Statev2.7.0URImapPointedPointed2Statev2.10.0URIofApplyApply2Statev2.10.0URImapapApplicativeApplicative2Statev2.7.0URImapapofChainchainable.Chain2Statev2.10.0URImapapchain: flatMapMonadMonad2Statev2.7.0URImapapofchain: flatMapFromStateFromState2Statev2.11.0URIfromState: identity它们之间的依赖关系符合类型类层次Functor ⊂ Pointed ⊂ Apply ⊂ Applicative ⊂ Chain ⊂ MonadApply含FunctorApplicative含PointedChain含ApplyMonad含Applicative与Chain。Monad的chain直接复用flatMap因此flatMap与chain在 State 上语义完全一致。FromState值得特别说明它的fromState实现是identitysrc/State.ts意思是State 本身就是 State无需任何转换。这个实例的真正价值不在 State 自身而在于让其他 monad 通过 FromState 模块 的能力把State提升进自身。例如 StateReaderTaskEither 就实现了fromState: ST.fromState(RTE.Pointed)从而获得get / put / modify / gets / fromStateK / chainStateK等全套状态工具——这是 State 在 fp-ts 生态中最重要的生态位之一。在函数式编程实践中应遵循 fp-ts 的推荐按需传入最小的实例。如果一个函数只需要map就声明它接受Functor2State并传入S.Functor而不是要求整个Monad。6. do notation让状态计算接近命令式代码v2.8.0 起 fp-ts 为State提供了 do notation 工具用于把嵌套的flatMap展开成先绑定、再计算的可读写法。它们全部基于既有实例派生src/State.ts6.1bindTo把值命名成对象字段export declare const bindTo: N(name: N) E, A(fa: StateE, A) StateE, { readonly [K in N]: A }由bindTo_(Functor)派生。例如pipe(_.of(1), _.bindTo(a))产生状态值{ a: 1 }。6.2bind顺序绑定依赖计算export declare const bind: N, A, E, B( name: ExcludeN, keyof A, f: (a: A) StateE, B ) (ma: StateE, A) StateE, { readonly [K in N | keyof A]: K extends keyof A ? A[K] : B }由chainable.bind(Chain)派生。f可以读取前面已经绑定的字段新字段name被追加到结果对象上。6.3apS用 Applicative 追加字段export declare const apS: N, A, E, B( name: ExcludeN, keyof A, fb: StateE, B ) (fa: StateE, A) StateE, { readonly [K in N | keyof A]: K extends keyof A ? A[K] : B }由apS_(Apply)派生。与bind的差别在于apS的第二个计算fb不依赖前面字段因此可以用Apply而非Chain实现代价是丢失了顺序依赖能力。6.4let用纯函数追加字段export declare const let: N, A, B( name: ExcludeN, keyof A, f: (a: A) B ) E(fa: StateE, A) StateE, { readonly [K in N | keyof A]: K extends keyof A ? A[K] : B }由let__(Functor)派生自 v2.13.0 加入。f是纯函数(a: A) B不产生任何状态效果只做结果变换。6.5 完整示例test/State.ts 给出了 do notation 的端到端验证import { pipe } from ./function import * as _ from ./State pipe( _.of(1), _.bindTo(a), // { a: 1 } _.bind(b, () _.of(b)), // { a: 1, b: b } (b 依赖 a可用 bind) _.apS(c, _.of(c)), // { a: 1, b: b, c: c } (c 不依赖前面可用 apS) _.let(d, ({ a, b }) [a, b]) // 纯函数追加 d )(state) // [{ a: 1, b: b, c: c, d: [1, b] }, state]注意do notation 中的状态state从头到尾未被修改因为of/bindTo/bind/apS/let都不改变状态。若中途需要写状态可插入modify或put组合。7. 遍历traversing对数组批量执行 State 计算State模块提供 5 个数组遍历工具全部按从左到右、状态顺序传递的方式工作自 v2.9.0 / v2.11.0 分两批加入函数等价语义入参返回traverseArrayReadonlyArray#traverse(Applicative)(a: A) StateS, B 数组StateS, readonly B[]traverseArrayWithIndexReadonlyArray#traverseWithIndex(Applicative)(index, a) StateS, B 数组StateS, readonly B[]traverseReadonlyArrayWithIndex同上新版推荐同左同左traverseReadonlyNonEmptyArrayWithIndexReadonlyNonEmptyArray#traverseWithIndex(Applicative)(index, a) StateS, B 非空数组StateS, ReadonlyNonEmptyArrayBsequenceArrayReadonlyArray#sequence(Applicative)readonly StateS, A[]StateS, readonly A[]实现细节src/State.tstraverseReadonlyNonEmptyArrayWithIndex是核心实现用一个显式循环逐元素运行f(i, as[i])前一步产出的新状态作为下一步的输入最后返回[bs, out]结果数组 最终状态traverseReadonlyArrayWithIndex在其上包装空数组直接返回of([])非空则委托给非空版本traverseArrayWithIndex与traverseArray分别是上述函数的别名/投影traverseArray忽略索引(_, a) f(a)sequenceArray实现为traverseArray(identity)——不逐个变换元素只是把一组 State 依次串行执行并收集结果。一个经典场景模拟累计器式遍历。测试test/State.ts中定义了append(n) (s) [n, [...s, n]]即把 n 同时作为结果返回、并追加到状态数组于是pipe([append(1), append(2)], _.sequenceArray)([]) // [[1, 2], [1, 2]]结果数组是[1, 2]最终状态累计数组也是[1, 2]——状态确实沿数组元素依次流动。8. legacy 与 zone of death历史 API 的迁移指引8.1 legacychain与chainFirstexport declare const chain: S, A, B(f: (a: A) StateS, B) (ma: StateS, A) StateS, B export declare const chainFirst: S, A, B(f: (a: A) StateS, B) (ma: StateS, A) StateS, A在 src/State.ts 中它们分别是flatMap与tap的直接别名chain flatMap、chainFirst tap。文档明确标注为 legacy新代码应使用flatMap/tap但旧代码迁移成本为零API 完全兼容。8.2 zone of death三个已废弃 APIzone of death死亡区是 fp-ts 对deprecatedAPI 的分类名称State 模块中有三个export declare const evalState: S, A(ma: StateS, A, s: S) A // 用 evaluate 代替 export declare const execState: S, A(ma: StateS, A, s: S) S // 用 execute 代替 export declare const state: Monad2State // 用具体实例代替evalState/execState的实现在 src/State.ts分别是ma(s)[0]与ma(s)[1]功能与evaluate/execute完全相同只是参数顺序不同(ma, s)而非柯里化。迁移只需把evalState(ma, s)改为evaluate(s)(ma)statev2.0.0 加入是Monad实例的旧导出export const state: Monad2URI Monad见 src/State.ts。文档明确建议不要传整个state实例而是按函数需要的最小实例传递——需要Functor就传S.Functor需要Monad才传S.Monad。这既减少不必要的约束也让类型推导更精确。迁移速查旧 API新 APIevalState(ma, s)evaluate(s)(ma)execState(ma, s)execute(s)(ma)chain(f)flatMap(f)chainFirst(f)tap(f)state整体实例Functor/Pointed/Apply/Applicative/Chain/Monad按需选取9. 在组合 monad 中使用 State生态联动State单独使用很直观但 fp-ts 中更常见的做法是把它嵌入其他 monad。FromState类型类src/FromState.ts定义了从 State 提升的抽象契约export interface FromStateF { readonly URI: F readonly fromState: S, A(fa: StateS, A) HKT2F, S, A }凡是实现了FromState的 monad都自动获得一组通用的状态工具FromState.get/put/modify/gets/fromStateK/chainStateK见 src/FromState.ts。State自身的FromState实例以identity实现即零成本提升。实际使用中StateReaderTaskEither 暴露了完整的FromState工具集get、put、modify、gets、fromStateK、chainStateK让你在同时携带状态S 环境R 错误E 异步Task的计算中依然能像使用纯State一样读写状态。此时单看 StateT 状态变换器 的模型StateTM, S, A (s: S) HKTM, [A, S]就能理解其本质把返回二元组泛化为返回任意 monadM包裹的二元组。选型建议如果只有纯同步状态读写需求直接使用State一旦需要错误处理、依赖注入或异步优先考虑StateReaderTaskEither这类组合 monad并通过fromStateK把已有的纯State函数直接提升进去复用。10. 小结State 模块 API 全景结合 State 模块文档 与 源码State 模块的全部公开 API 可归纳为六个分区modelStateS, A接口——(s: S) [A, S]constructorsof、get、put、modify、getsmappingmap、flapsequencing / combinatorsflatMap、flatten、tap、ap、apFirst、apSecondutils / do notationevaluate、execute、bindTo、bind、apS、lettraversingtraverseArray、traverseArrayWithIndex、traverseReadonlyArrayWithIndex、traverseReadonlyNonEmptyArrayWithIndex、sequenceArray外加instancesFunctor/Pointed/Apply/Applicative/Chain/Monad/FromState、type lambdasURI以及 legacy / zone of death 中的旧 APIchain、chainFirst、evalState、execState、state。编写新代码时的推荐实践是用bindTo/bind/apS/let组织计算、用get/put/modify/gets读写状态、用evaluate/execute触发运行、用tap在保留结果的同时执行状态副作用并始终按最小实例约束传递类型类从而获得最清晰、最可维护的函数式状态管理代码。【免费下载链接】fp-tsFunctional programming in TypeScript项目地址: https://gitcode.com/gh_mirrors/fp/fp-ts创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
