news 2026/9/23 22:16:36

fp-ts Store 模块完全指南:用 Comonad 理解带焦点的函数式状态

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 实例,以及它与ComonadExtendFunctor等抽象之间的关系。读完本文,你将能够熟练创建Store、使用管道式 API 完成位置移动与局部取值,并理解余单子编程的基本模式。


一、Store 是什么:模型与核心直觉

1.1 Store 接口定义

Store<S, A>在 fp-ts 中定义于 src/Store.ts,类型参数含义如下:

  • Sposition(位置)类型,即状态空间的坐标类型;
  • Avalue(取值)类型,即从位置读取到的值类型。
export interface Store<S, 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: _.Store<string, number> = { peek: S.size, pos: 'a' }

此时extract(wa)会执行wa.peek(wa.pos),即读取'a'的长度,得到1

1.3 取回当前值:extract

extract是 Comonad 的"对偶于 Monad 的return"的关键操作,签名见文档与源码 src/Store.ts:

export declare const extract: <E, A>(wa: Store<E, 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 seek<S>(s: S): <A>(wa: Store<S, A>) => Store<S, A>

源码实现(src/Store.ts):

return (wa) => ({ peek: wa.peek, pos: s })

seek保留原有peek,仅把pos替换为目标位置s。例如测试 test/Store.ts 中,焦点从'a'移到'aa'extract结果为2'aa'的长度)。

2.2 seeks:根据当前位置计算新焦点

export declare function seeks<S>(f: Endomorphism<S>): <A>(wa: Store<S, A>) => Store<S, A>

实现(src/Store.ts):

return (wa) => ({ peek: wa.peek, pos: f(wa.pos) })

其中f: Endomorphism<S>(s: S) => S的自同态函数(定义见 src/Endomorphism.ts)。seeks先对当前焦点施加f得到新位置,再更新pos。测试用例中seeks((s) => s + 'a')'a'变为'aa'extract得到2(test/Store.ts)。

seekvsseeks的区分seek接收具体的坐标值seeks接收坐标变换函数。后者常用于"相对移动",例如光标右移一位可写为seeks((x) => x + 1)

2.3 peeks:在不改变焦点的情况下窥探其他位置

export declare function peeks<S>(f: Endomorphism<S>): <A>(wa: Store<S, A>) => A

实现(src/Store.ts):

return (wa) => wa.peek(f(wa.pos))

seeks不同,peeks不改变pos,而是基于当前焦点计算出目标位置后直接peek取值,返回A而非新的Store。它适合"偷看"相邻位置的值而不移动光标。测试中peeks((s) => s + 'a')返回2(test/Store.ts),且原 Store 的焦点保持不变。


三、批量探测:experiment

experimentpeeks的批量版本,文档描述为"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))

它接收两个参数并柯里化:

  1. 一个Functor实例F(支持Functor3/Functor3C/Functor2/Functor2C/Functor1/FunctorHKT各档);
  2. 一个函数f: (s: S) => F<S>,它根据当前焦点位置产生一批候选位置(包在某个函子容器中,如数组、OptionReadonlyArray等)。

然后对每个候选位置执行wa.peek,最终得到F<A>

3.2 实战示例

测试 test/Store.ts 展示了与ReadonlyArray函子配合的用法:

pipe( wa, // Store<string, number>,peek = S.size,pos = 'a' _.experiment(RA.Functor)((s) => [s, s + 'a']) // 候选位置:['a', 'aa'] ) // 结果:[1, 2],即 [size('a'), size('aa')]

也可以换用OptionIO等任意 Functor,experiment会自动按对应容器聚合结果。这是"以当前焦点为中心批量探测邻域"的通用工具。


四、Comonad 视角:extend 与 duplicate

Store是 fp-ts 中余单子的代表性实例,理解extend/duplicate是掌握它的关键。

4.1 extend:把"整片上下文"的计算推广到每个位置

export declare const extend: <E, A, B>(f: (wa: Store<E, A>) => B) => (wa: Store<E, A>) => Store<E, B>

实现(src/Store.ts):

export const extend = (f) => (wa) => ({ peek: (s) => f({ peek: wa.peek, pos: s }), pos: wa.pos })

extend接收一个"消费整个 Store 得出一个值"的函数f: (wa: Store<E, A>) => B,返回新的 Store:新 Store 的peek会在任意位置s处临时把焦点设为s,再调用f计算该位置上的"局部结果";pos保持原焦点不变。这正是对偶于bindchain的"上下文传播"操作——它是后面会讲到的细胞自动机、平滑滤波等"每个点由邻域决定"算法的基石。

测试中的用法(test/Store.ts):

pipe( wa, _.extend((wa) => _.extract( pipe( wa, _.map((n) => n + 1) ) ) ) )

4.2 duplicate:制造"带游标的光标"

export declare const duplicate: <E, A>(wa: Store<E, A>) => Store<E, Store<E, A>>

源码实现极其简洁(src/Store.ts):

export const duplicate = /*#__PURE__*/ extend(identity)

duplicate就是extend(identity)——把内部每个位置上"临时焦点化的 Store"作为值,得到一个Store<E, Store<E, A>>。测试验证了extract(extract(duplicate(wa))) === 1(test/Store.ts),这恰好印证了余单子定律extract ∘ duplicate = id在该实例上的成立。

三者关系:extend(f) = map(f) ∘ duplicate,这是 Extend 公理的一部分,duplicate因此可以被视为extend的"通用化"版本。


五、映射操作:map 与 flap

5.1 map:把函数提升进 Store 上下文

map是标准 Functor 操作,文档说明:"mapcan be used to turn functions(a: A) => Binto functions(fa: F<A>) => F<B>"。签名:

export declare const map: <A, B>(f: (a: A) => B) => <E>(fa: Store<E, A>) => Store<E, B>

实现(src/Store.ts)保持pos不变,把f复合进peek

export 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: Store<E, (a: A) => B>) => Store<E, B>

flap(v2.10.0 加入)把a: A应用到容器内承载的函数上。fp-ts 中flap的通用实现定义于 src/Functor.ts:

return (a) => (fab) => F.map(fab, (f) => f(a))

Store.flap是通过flap_(Functor)Functor实例派生的(src/Store.ts),/*#__PURE__*/标记使其可被打包器做常量折叠。当你手里有一个Store<E, (a: A) => B>并想把它应用到固定参数a上时,flap比手写map((f) => f(a))更直观。


六、类型体操:URI、类型 lambda 与实例

6.1 URI 与类型 lambda 注册

Store是二元类型构造子(两个类型参数SA),通过URI常量接入 HKT 系统(src/Store.ts):

export const URI = 'Store' export type URI = typeof URI declare module './HKT' { interface URItoKind2<E, A> { readonly [URI]: Store<E, A> } }
  • URI常量:运行时标识,值为字符串'Store'
  • URI类型别名:把字符串字面量类型'Store'暴露给类型层;
  • URItoKind2declare module增强:告诉 fp-ts 的 HKT 查找表"'Store'对应Store<E, A>",这是experiment等泛型函数能对Store做类型推断的前提。

6.2 Functor 与 Comonad 实例

自 v2.7.0 起,fp-ts 提供小而具体的实例(src/Store.ts):

export const Functor: Functor2<URI> = { URI, map: _map } export const Comonad: Comonad2<URI> = { URI, map: _map, extend: _extend, extract }

其中_map/_extend/* istanbul ignore next */标记的"非柯里化"适配层(src/Store.ts),把柯里化 API 转成实例所需的二元签名。

抽象层级关系如下(对应 src/Comonad.ts 与 src/Extend.ts 的接口定义):

Comonad2<W> extends Extend2<W> extends Functor2<W> ├── map : 来自 Functor ├── extend : 来自 Extend └── extract: Comonad 新增(唯一"取出"操作)

Comonad2接口要求extract: <E, A>(wa: Kind2<W, E, A>) => A(src/Comonad.ts),正好与Storeextract对应。


七、zone of death:被废弃的 store 实例

文档最后一部分是"zone of death"(废弃区),store自 v2.0.0 起存在、现已标记@deprecated

export declare const store: Comonad2<'Store'>

废弃原因(源码注释 src/Store.ts 与文档一致):应改用小而具体的实例。例如某个函数需要Comonad时,传入S.Comonad而不是S.store(其中S来自import S from 'fp-ts/Store')。这符合 fp-ts v2 中期以来的 API 演进方针——用FunctorComonad等最小实例按需组合,避免一次性传递"超大实例"。新代码应直接使用Comonad


八、实例与定律验证:从测试看正确性

test/Store.ts 使用describe/it组织测试,通过pipe管道式调用覆盖了全部核心 API:

被测函数测试位置关键断言
maptest/Store.tsextract(map(n => n+1)(wa)) === 2
extendtest/Store.ts邻域求值后再extract得到2
duplicatetest/Store.tsextract(extract(duplicate(wa))) === 1(余单子定律)
seektest/Store.ts焦点移到'aa'extract === 2
seekstest/Store.ts变换(s) => s + 'a'extract === 2
peekstest/Store.ts不移动焦点,窥探'aa'2
experimenttest/Store.tsRA.Functor配合得[1, 2]

这些用例同时验证了余单子的关键定律:

  • extract 左单位律extract ∘ duplicate = idduplicate测试);
  • map/extend 与 extract 的相容性extract ∘ map(f) = f ∘ extract
  • extend 的复合extend(f) = map(f) ∘ duplicate

借助pipe(定义于 src/function.ts),Store的 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: _.Store<string, 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 + '!')) // 2(buffer.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) // 'length=1' // 8. extend:在每个位置计算"当前位置与其下一个位置长度之和" const sumWithNext = pipe( buffer, _.extend((wa) => _.extract(wa) + _.peeks((s) => s + 'a')(wa)) ) pipe(sumWithNext, _.extract) // 1 + 2 = 3

该示例完整串联了文档列出的全部核心函数(extractseekseekspeeksexperimentmapextend),可作为日常使用Store的速查样板。


十、何时使用 Store:适用场景小结

结合文档内容与源码结构,Store适合以下场景:

  • 带焦点的只读上下文:编辑器光标、地图/棋盘坐标、表格选中单元格等"当前位置 + 全局查询能力"的组合;
  • 局部计算需要依赖邻域:一维/二维元胞自动机、图像卷积、平滑滤波——extend让"每个点的值由周围环境决定"得以纯函数式表达;
  • 与 Comonad 抽象协同:需要extract/extend/duplicate三件套参与泛型组合的算法(如 Conway 生命游戏可以建模为Store<Cell, Cell>上的迭代);
  • 只想"偷看"不改状态peeks/experiment提供无副作用的位置探测。

需要注意:Store纯只读结构,seek/seeks产生的是新的 Store 而非原地修改;若需要可写状态,应配合其他状态管理模块使用。


附录:API 速查表

分类名称签名要点版本
Extractextract(wa: Store<E, A>) => A,读取当前焦点值v2.6.2
instancesComonadComonad2<'Store'>,含 map/extend/extractv2.7.0
instancesFunctorFunctor2<'Store'>,仅含 mapv2.7.0
mappingflap把值应用到容器内函数v2.10.0
mappingmap在保持pos的同时变换peek结果v2.0.0
modelStore{ peek: (s: S) => A; pos: S }v2.0.0
type lambdasURI/URI字符串'Store'及 HKT 注册v2.0.0
utilsduplicateextend(identity),产生Store<E, Store<E, A>>v2.0.0
utilsexperiment基于当前位置批量探测并聚合v2.0.0
utilsextend将"上下文 → 值"的计算推广到每个位置v2.0.0
utilspeeks不移动焦点窥探目标位置v2.0.0
utilsseek焦点移到指定位置v2.0.0
utilsseeks依据变换函数重定位焦点v2.0.0
zone of death~~store~~已废弃,改用Comonad等小实例v2.0.0

版本说明:以上版本号均取自当前仓库 src/Store.ts 与 docs/modules/Store.ts.md 中的@since标注。


延伸阅读

  • Comonad 抽象定义:了解Comonad2等接口的层级关系;
  • Extend 抽象定义:extend运算的形式化约束;
  • Functor 模块:flap的通用实现(src/Functor.ts);
  • Endomorphism 定义:seeks/peeks依赖的(s: S) => S类型;
  • Store 源码 与 Store 测试:本文全部结论的实现与验证依据。

【免费下载链接】fp-tsFunctional programming in TypeScript项目地址: https://gitcode.com/gh_mirrors/fp/fp-ts

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/23 22:15:24

同样用汇写,为什么别人的初稿比你好 —— 使用细节决定效果

两个学生同时用汇写写毕业论文&#xff0c;生成的初稿质量可能差很多。差别不在工具&#xff0c;在使用细节。汇写&#xff08;https://www.huixielunwen.com/tool/graduationThesis&#xff09;对所有人都是同一个系统&#xff0c;但输入的信息质量不同&#xff0c;输出质量自…

作者头像 李华
网站建设 2026/9/23 22:10:41

YOLO红细胞目标检测:标签格式转换与训练调参实战指南

简介&#xff1a;面向目标检测入门与进阶人群的YOLO红细胞检测数据集&#xff0c;使用真实场景下的高质量图像构建&#xff0c;包含1000张已标注图片&#xff0c;场景覆盖多种光照和背景条件&#xff0c;可支撑医学影像分析、细胞识别等方向的课程设计、毕业设计或算法预研。数…

作者头像 李华
网站建设 2026/9/23 22:08:14

Qt跨平台GUI开发实战:拼图项目详解三端构建与资源管理

简介&#xff1a;本资源是一份面向C初学者与高校计算机专业学生的课程设计实践项目&#xff0c;聚焦Qt跨平台GUI开发能力训练&#xff0c;完整实现经典拼图游戏功能&#xff0c;适用于期末大作业提交、Qt入门实战及图形界面编程能力提升。压缩包共181个文件&#xff0c;包含17个…

作者头像 李华
网站建设 2026/9/23 22:04:03

示波器串行解码实战:CAN/LIN总线故障诊断从入门到精通

入行头几年&#xff0c;我吃过一次大亏。售后车间送来一台偶发通信故障的车&#xff0c;故障码指向CAN网络&#xff0c;但用万用表量CAN_H对地电压、CAN_L对地电压、终端电阻&#xff0c;全部正常。反复试车就是不复现&#xff0c;客户催得急&#xff0c;最后只能让车先走。后来…

作者头像 李华
网站建设 2026/9/23 22:02:38

比特币多因子LSTM交易策略工程实践

简介&#xff1a;本资源是一份基于LSTM的比特币多因子量化交易策略完整实现&#xff0c;面向计算机、人工智能、金融工程等专业的学生与初学者&#xff0c;解决加密资产预测建模与策略回测落地难题&#xff0c;适用于课程设计、毕设开发及算法进阶学习。压缩包共12个文件&#…

作者头像 李华
网站建设 2026/9/23 22:02:16

微信小程序开发实战:案例3.8 模块化详解与不同模块背景颜色区分

前言在微信小程序的开发过程中&#xff0c;随着项目功能的增加&#xff0c;代码量也会随之膨胀。为了提高代码的可维护性和复用性&#xff0c;模块化&#xff08;Modularization&#xff09; 是必不可少的手段。微信小程序原生支持 CommonJS 规范&#xff0c;允许我们将通用的变…

作者头像 李华