fp-ts Store 模块完全指南:用 Comonad 理解带焦点的函数式状态
fp-ts Store 模块完全指南用 Comonad 理解带焦点的函数式状态【免费下载链接】fp-tsFunctional programming in TypeScript项目地址: https://gitcode.com/gh_mirrors/fp/fp-ts导读Store是 fp-ts 中基于Comonad余单子思想实现的经典数据结构它将「一个可读取当前状态的函数peek」与「一个当前焦点位置pos」打包在一起从而在纯函数式代码中表达带焦点的上下文。本文以 docs/modules/Store.ts.md 为主线结合 src/Store.ts 的源码实现与 test/Store.ts 的测试用例系统讲解Store的模型、全部实用函数seek/seeks/peeks/experiment等、Comonad 实例以及它与Comonad、Extend、Functor等抽象之间的关系。读完本文你将能够熟练创建Store、使用管道式 API 完成位置移动与局部取值并理解余单子编程的基本模式。一、Store 是什么模型与核心直觉1.1 Store 接口定义StoreS, A在 fp-ts 中定义于 src/Store.ts类型参数含义如下Sposition位置类型即状态空间的坐标类型Avalue取值类型即从位置读取到的值类型。export interface StoreS, A { readonly peek: (s: S) A readonly pos: S }接口只有两个字段字段类型含义peek(s: S) A从任意位置s读取对应的值只读查询函数posS当前焦点位置当前光标所在的坐标Store的核心直觉是它像一枚镜头或游标——peek描述了整个世界从任意坐标到值的映射pos标记了你当前注视着的位置。这使它天然适合表达光标文本编辑器位置 缓冲区读取、棋盘/网格坐标 格子内容查询等带焦点的场景。1.2 创建一个 Store由于Store只是一个普通接口直接构造对象字面量即可。测试用例 test/Store.ts 展示了典型构造方式——用S.size来自 src/string.ts 的size: (s: string) number作为peek即读取任意字符串的长度import * as _ from fp-ts/Store import * as S from fp-ts/string // peek 表示从任意字符串位置读到其长度pos 表示当前焦点在 a 处 const wa: _.Storestring, number { peek: S.size, pos: a }此时extract(wa)会执行wa.peek(wa.pos)即读取a的长度得到1。1.3 取回当前值extractextract是 Comonad 的对偶于 Monad 的return的关键操作签名见文档与源码 src/Store.tsexport declare const extract: E, A(wa: StoreE, A) A实现极其简单(wa) wa.peek(wa.pos)——对当前位置执行 peek取出焦点处的值。它不改变 Store 本身是纯读取。二、位置操作三件套seek / seeks / peeks这三者是Store使用频率最高的工具函数文档中分别标注为Reposition the focus...与Extract a value from a position which depends on the current position。2.1 seek把焦点移动到指定位置export declare function seekS(s: S): A(wa: StoreS, A) StoreS, A源码实现src/Store.tsreturn (wa) ({ peek: wa.peek, pos: s })seek保留原有peek仅把pos替换为目标位置s。例如测试 test/Store.ts 中焦点从a移到aa后extract结果为2aa的长度。2.2 seeks根据当前位置计算新焦点export declare function seeksS(f: EndomorphismS): A(wa: StoreS, A) StoreS, A实现src/Store.tsreturn (wa) ({ peek: wa.peek, pos: f(wa.pos) })其中f: EndomorphismS即(s: S) S的自同态函数定义见 src/Endomorphism.ts。seeks先对当前焦点施加f得到新位置再更新pos。测试用例中seeks((s) s a)把a变为aaextract得到2test/Store.ts。seekvsseeks的区分seek接收具体的坐标值seeks接收坐标变换函数。后者常用于相对移动例如光标右移一位可写为seeks((x) x 1)。2.3 peeks在不改变焦点的情况下窥探其他位置export declare function peeksS(f: EndomorphismS): A(wa: StoreS, A) A实现src/Store.tsreturn (wa) wa.peek(f(wa.pos))与seeks不同peeks不改变pos而是基于当前焦点计算出目标位置后直接peek取值返回A而非新的Store。它适合偷看相邻位置的值而不移动光标。测试中peeks((s) s a)返回2test/Store.ts且原 Store 的焦点保持不变。三、批量探测experimentexperiment是peeks的批量版本文档描述为Extract a collection of values from positions which depend on the current position。3.1 签名与多重重载experiment针对不同 Kind 结构提供了 5 个重载文档与 src/Store.ts 完全一致核心逻辑只有一行return (f) (wa) F.map(f(wa.pos), (s) wa.peek(s))它接收两个参数并柯里化一个Functor实例F支持Functor3/Functor3C/Functor2/Functor2C/Functor1/FunctorHKT各档一个函数f: (s: S) FS它根据当前焦点位置产生一批候选位置包在某个函子容器中如数组、Option、ReadonlyArray等。然后对每个候选位置执行wa.peek最终得到FA。3.2 实战示例测试 test/Store.ts 展示了与ReadonlyArray函子配合的用法pipe( wa, // Storestring, numberpeek S.sizepos a _.experiment(RA.Functor)((s) [s, s a]) // 候选位置[a, aa] ) // 结果[1, 2]即 [size(a), size(aa)]也可以换用Option、IO等任意 Functorexperiment会自动按对应容器聚合结果。这是以当前焦点为中心批量探测邻域的通用工具。四、Comonad 视角extend 与 duplicateStore是 fp-ts 中余单子的代表性实例理解extend/duplicate是掌握它的关键。4.1 extend把整片上下文的计算推广到每个位置export declare const extend: E, A, B(f: (wa: StoreE, A) B) (wa: StoreE, A) StoreE, B实现src/Store.tsexport const extend (f) (wa) ({ peek: (s) f({ peek: wa.peek, pos: s }), pos: wa.pos })extend接收一个消费整个 Store 得出一个值的函数f: (wa: StoreE, A) B返回新的 Store新 Store 的peek会在任意位置s处临时把焦点设为s再调用f计算该位置上的局部结果pos保持原焦点不变。这正是对偶于bindchain的上下文传播操作——它是后面会讲到的细胞自动机、平滑滤波等每个点由邻域决定算法的基石。测试中的用法test/Store.tspipe( wa, _.extend((wa) _.extract( pipe( wa, _.map((n) n 1) ) ) ) )4.2 duplicate制造带游标的光标export declare const duplicate: E, A(wa: StoreE, A) StoreE, StoreE, A源码实现极其简洁src/Store.tsexport const duplicate /*#__PURE__*/ extend(identity)duplicate就是extend(identity)——把内部每个位置上临时焦点化的 Store作为值得到一个StoreE, StoreE, A。测试验证了extract(extract(duplicate(wa))) 1test/Store.ts这恰好印证了余单子定律extract ∘ duplicate id在该实例上的成立。三者关系extend(f) map(f) ∘ duplicate这是 Extend 公理的一部分duplicate因此可以被视为extend的通用化版本。五、映射操作map 与 flap5.1 map把函数提升进 Store 上下文map是标准 Functor 操作文档说明mapcan be used to turn functions(a: A) Binto functions(fa: FA) FB。签名export declare const map: A, B(f: (a: A) B) E(fa: StoreE, A) StoreE, B实现src/Store.ts保持pos不变把f复合进peekexport const map (f) (fa) ({ peek: (s) f(fa.peek(s)), pos: fa.pos })5.2 flap把容器内的函数应用到固定值上export declare const flap: A(a: A) E, B(fab: StoreE, (a: A) B) StoreE, Bflapv2.10.0 加入把值a: A应用到容器内承载的函数上。fp-ts 中flap的通用实现定义于 src/Functor.tsreturn (a) (fab) F.map(fab, (f) f(a))而Store.flap是通过flap_(Functor)从Functor实例派生的src/Store.ts/*#__PURE__*/标记使其可被打包器做常量折叠。当你手里有一个StoreE, (a: A) B并想把它应用到固定参数a上时flap比手写map((f) f(a))更直观。六、类型体操URI、类型 lambda 与实例6.1 URI 与类型 lambda 注册Store是二元类型构造子两个类型参数S、A通过URI常量接入 HKT 系统src/Store.tsexport const URI Store export type URI typeof URI declare module ./HKT { interface URItoKind2E, A { readonly [URI]: StoreE, A } }URI常量运行时标识值为字符串StoreURI类型别名把字符串字面量类型Store暴露给类型层URItoKind2的declare module增强告诉 fp-ts 的 HKT 查找表Store对应StoreE, A这是experiment等泛型函数能对Store做类型推断的前提。6.2 Functor 与 Comonad 实例自 v2.7.0 起fp-ts 提供小而具体的实例src/Store.tsexport const Functor: Functor2URI { URI, map: _map } export const Comonad: Comonad2URI { URI, map: _map, extend: _extend, extract }其中_map/_extend是/* istanbul ignore next */标记的非柯里化适配层src/Store.ts把柯里化 API 转成实例所需的二元签名。抽象层级关系如下对应 src/Comonad.ts 与 src/Extend.ts 的接口定义Comonad2W extends Extend2W extends Functor2W ├── map : 来自 Functor ├── extend : 来自 Extend └── extract: Comonad 新增唯一取出操作Comonad2接口要求extract: E, A(wa: Kind2W, E, A) Asrc/Comonad.ts正好与Store的extract对应。七、zone of death被废弃的 store 实例文档最后一部分是zone of death废弃区store自 v2.0.0 起存在、现已标记deprecatedexport declare const store: Comonad2Store废弃原因源码注释 src/Store.ts 与文档一致应改用小而具体的实例。例如某个函数需要Comonad时传入S.Comonad而不是S.store其中S来自import S from fp-ts/Store。这符合 fp-ts v2 中期以来的 API 演进方针——用Functor、Comonad等最小实例按需组合避免一次性传递超大实例。新代码应直接使用Comonad。八、实例与定律验证从测试看正确性test/Store.ts 使用describe/it组织测试通过pipe管道式调用覆盖了全部核心 API被测函数测试位置关键断言maptest/Store.tsextract(map(n n1)(wa)) 2extendtest/Store.ts邻域求值后再extract得到2duplicatetest/Store.tsextract(extract(duplicate(wa))) 1余单子定律seektest/Store.ts焦点移到aa后extract 2seekstest/Store.ts变换(s) s a后extract 2peekstest/Store.ts不移动焦点窥探aa得2experimenttest/Store.ts与RA.Functor配合得[1, 2]这些用例同时验证了余单子的关键定律extract 左单位律extract ∘ duplicate idduplicate测试map/extend 与 extract 的相容性extract ∘ map(f) f ∘ extractextend 的复合extend(f) map(f) ∘ duplicate。借助pipe定义于 src/function.tsStore的 API 全部采用数据在管道尾部的柯里化形式可读性良好。九、完整实战用 Store 模拟一个带光标的小文本缓冲区综合以上 API构造一个字符串长度缓冲区 光标的完整示例import * as _ from fp-ts/Store import { pipe } from fp-ts/function import * as RA from fp-ts/ReadonlyArray import * as S from fp-ts/string // 1. 创建光标在 abc 开头 const buffer: _.Storestring, number { peek: S.size, pos: a } // 2. extract读取当前焦点处的长度 pipe(buffer, _.extract) // 1 // 3. seek直接跳到 hello pipe(buffer, _.seek(hello), _.extract) // 5 // 4. seeks相对移动向后追加一个字符 pipe(buffer, _.seeks((s) s !), _.extract) // 2 // 5. peeks不移动光标窥探右侧一个位置 pipe(buffer, _.peeks((s) s !)) // 2buffer.pos 仍为 a // 6. experiment批量探测当前位置及其邻域 pipe(buffer, _.experiment(RA.Functor)((s) [s, s a, s aa])) // [1, 2, 3] // 7. map把读取结果变换后仍保持焦点 pipe(buffer, _.map((n) length${n}), _.extract) // length1 // 8. extend在每个位置计算当前位置与其下一个位置长度之和 const sumWithNext pipe( buffer, _.extend((wa) _.extract(wa) _.peeks((s) s a)(wa)) ) pipe(sumWithNext, _.extract) // 1 2 3该示例完整串联了文档列出的全部核心函数extract、seek、seeks、peeks、experiment、map、extend可作为日常使用Store的速查样板。十、何时使用 Store适用场景小结结合文档内容与源码结构Store适合以下场景带焦点的只读上下文编辑器光标、地图/棋盘坐标、表格选中单元格等当前位置 全局查询能力的组合局部计算需要依赖邻域一维/二维元胞自动机、图像卷积、平滑滤波——extend让每个点的值由周围环境决定得以纯函数式表达与 Comonad 抽象协同需要extract/extend/duplicate三件套参与泛型组合的算法如 Conway 生命游戏可以建模为StoreCell, Cell上的迭代只想偷看不改状态peeks/experiment提供无副作用的位置探测。需要注意Store是纯只读结构seek/seeks产生的是新的 Store 而非原地修改若需要可写状态应配合其他状态管理模块使用。附录API 速查表分类名称签名要点版本Extractextract(wa: StoreE, A) A读取当前焦点值v2.6.2instancesComonadComonad2Store含 map/extend/extractv2.7.0instancesFunctorFunctor2Store仅含 mapv2.7.0mappingflap把值应用到容器内函数v2.10.0mappingmap在保持pos的同时变换peek结果v2.0.0modelStore{ peek: (s: S) A; pos: S }v2.0.0type lambdasURI/URI字符串Store及 HKT 注册v2.0.0utilsduplicateextend(identity)产生StoreE, StoreE, Av2.0.0utilsexperiment基于当前位置批量探测并聚合v2.0.0utilsextend将上下文 → 值的计算推广到每个位置v2.0.0utilspeeks不移动焦点窥探目标位置v2.0.0utilsseek焦点移到指定位置v2.0.0utilsseeks依据变换函数重定位焦点v2.0.0zone of death~~store~~已废弃改用Comonad等小实例v2.0.0版本说明以上版本号均取自当前仓库 src/Store.ts 与 docs/modules/Store.ts.md 中的since标注。延伸阅读Comonad 抽象定义了解Comonad2等接口的层级关系Extend 抽象定义extend运算的形式化约束Functor 模块flap的通用实现src/Functor.tsEndomorphism 定义seeks/peeks依赖的(s: S) S类型Store 源码 与 Store 测试本文全部结论的实现与验证依据。【免费下载链接】fp-tsFunctional programming in TypeScript项目地址: https://gitcode.com/gh_mirrors/fp/fp-ts创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考