- 开发工具
【免费下载链接】fp-ts
Functional programming in TypeScript
导读
Traced是 fp-ts 中一种以"纯函数读取环境"为形态的类型构造器:Traced<P, A>本质上就是(p: P) => A,它把"从某个可叠加的上下文P中读出值A"封装成可组合的一等公民。本文以 Traced.ts 模块文档 为核心,结合 Traced 源码 与 Traced 测试用例 中的ProjectBuilder实例,完整讲解Traced的模型、Functor/Comonad实例,以及censor、listen、listens、tracks四个工具函数的实现原理与实战用法。读完本文,你将能够用Traced实现"增量式、可组合的构建器"(如逐层叠加配置的工程脚手架生成器),并理解余单子extract/extend在其中的作用。
一、Traced是什么:把"读取环境"变成类型
1.1 模型定义
在 fp-ts 中,Traced的模型极其简单,src/Traced.ts#L17-L19 中只有一行:
export interface Traced<P, A> { (p: P): A }也就是说,一个Traced<P, A>就是一个接受P返回A的普通函数。类型参数:
P(Position):"位置"或"环境"类型,是读取的输入;A:读取到的值类型。
从源码结构看,Traced属于二元类型构造器(两个类型参数),因此在 src/Traced.ts#L104-L108 中通过URItoKind2注册进 fp-ts 的高阶类型(HKT)体系:
declare module './HKT' { interface URItoKind2<E, A> { readonly [URI]: Traced<E, A> } }配合 src/HKT.ts 中的URItoKind2,Traced才能参与Functor2、Comonad2C等带 kind 的泛型实例构造。
1.2 为什么要用"函数"做数据类型?
Traced的妙处在于:函数本身就是一种数据结构。一个Traced<P, A>尚未被"消费"时,它只是一个闭包;只有当调用它并传入具体的P时,值才会被计算出来。这带来两个直接优势:
- 延迟求值:构建复杂的
Traced链并不会立即执行任何计算; - 可组合的上下文叠加:借助
Monoid<P>,多个Traced的环境可以像"覆盖配置"一样逐层合并。
模块文档在 docs/modules/Traced.ts.md 中将Traced归于model分类,并标注 "Added in v2.0.0",说明它是 fp-ts 2.x 的基石抽象之一。
二、实例:Functor与getComonad
2.1Functor:对读取结果做映射
模块文档中的Functor实例签名:
export declare const Functor: Functor2<'Traced'>对应的实现位于 src/Traced.ts#L114-L117:
export const Functor: Functor2<URI> = { URI, map: _map }map的行为非常直观——先读取再变换(src/Traced.ts#L90):
export const map: <A, B>(f: (a: A) => B) => <E>(fa: Traced<E, A>) => Traced<E, B> = (f) => (fa) => (p) => f(fa(p))即map(f)(wa) = (p) => f(wa(p)):它不改变环境P,只对读取到的A做变换。测试 test/Traced.ts#L55-L58 验证了这一点:
const wa = buildProject('myproject') U.deepStrictEqual(pipe(wa, _.map(getProjectName))(M.empty), 'myproject')Functor实例需要满足两条定律(见 src/Functor.ts#L7-L10):
- 同一律:
map(fa, a => a) <-> fa - 复合律:
map(fa, bc ∘ ab) <-> map(map(fa, ab), bc)
由于Traced的map本质是函数复合,这两条定律天然成立。
2.2getComonad:Traced的余单子结构
模块文档的核心签名:
export declare function getComonad<P>(monoid: Monoid<P>): Comonad2C<URI, P>它依赖一个Monoid<P>,才能为Traced<P, _>构造Comonad2C实例。实现见 src/Traced.ts#L62-L78:
export function getComonad<P>(monoid: Monoid<P>): Comonad2C<URI, P> { function extend<A, B>(wa: Traced<P, A>, f: (wa: Traced<P, A>) => B): Traced<P, B> { return (p1) => f((p2) => wa(monoid.concat(p1, p2))) } function extract<A>(wa: Traced<P, A>): A { return wa(monoid.empty) } return { URI, _E: undefined as any, map: _map, extend, extract } }这里有两个关键操作:
extract:把Traced<P, A>退化为A。做法是以monoid.empty(幺元)作为"默认位置"调用函数,即wa(monoid.empty)。这对应 src/Comonad.ts#L15-L17 中Comonad接口对extract的定义:extract: <A>(wa: HKT<W, A>) => A。extend:给定wa: Traced<P, A>和f: (wa: Traced<P, A>) => B,返回一个新的Traced<P, B>。它的实现是:在新位置p1上,把f应用到"以p1为基准、用monoid.concat(p1, p2)偏移出的新读取函数"上。extend继承自 src/Extend.ts#L39-L41 的Extend2C接口,并向上提供Functor2C的map。
从源码结构看,Monoid<P>的empty和concat是Traced余单子语义的基石:empty定义了"起始位置",concat定义了"从当前位置偏移到相邻位置"的方式。
2.3 类型变量说明
getComonad<P>返回Comonad2C<URI, P>,其中C表示"部分应用"(Curried):P已被固定,剩下的A作为自由类型参数。这在 src/Comonad.ts#L39-L41 中有完整定义:
export interface Comonad2C<W extends URIS2, E> extends Extend2C<W, E> { readonly extract: <A>(wa: Kind2<W, E, A>) => A }三、mapping 一族:map与flap
3.1map(映射)
模块文档对map的说明是标准的 Functor 语义:
mapcan be used to turn functions(a: A) => Binto functions(fa: F<A>) => F<B>whose argument and return types use the type constructorFto represent some computational context.
签名(Added in v2.0.0):
export declare const map: <A, B>(f: (a: A) => B) => <E>(fa: Traced<E, A>) => Traced<E, B>注意这里的E就是模型里的P(位置)。map是柯里化的,可直接配合pipe使用,例如 test/Traced.ts#L56-L57 中pipe(wa, _.map(getProjectName))。
3.2flap(翻转应用)
flap(Added in v2.10.0)是一个"函数值翻转"工具:
export declare const flap: <A>(a: A) => <E, B>(fab: Traced<E, (a: A) => B>) => Traced<E, B>其实现只是基于Functor实例的派生(src/Traced.ts#L123):
export const flap = /*#__PURE__*/ flap_(Functor)而flap_的通用实现见 src/Functor.ts#L153-L155:
return (a) => (fab) => F.map(fab, (f) => f(a))即:先准备好一个值a: A,再拿到一个"环境读取函数Traced<E, A => B>",最后得到Traced<E, B>。语义上等价于"把a应用到环境中的函数上"。
四、utils:位置(Position)操作四件套
这四个工具函数是本模块最具实用价值的部分,全部 "Added in v2.0.0",实现在 src/Traced.ts#L27-L56。
4.1listen:取出当前位置
Get the current position
export declare function listen<P, A>(wa: Traced<P, A>): Traced<P, [A, P]>实现:
export function listen<P, A>(wa: Traced<P, A>): Traced<P, [A, P]> { return (e) => [wa(e), e] }它把读取结果与当前位置打包成元组[A, P],相当于"审计":不仅知道读到了什么,还知道是从哪个位置读的。测试 test/Traced.ts#L93-L107 展示了C.extract(_.listen(buildProject('myproject')))的结果是[Project, Settings]二元组。
4.2listens:取出依赖位置的派生值
Get a value which depends on the current position
export declare function listens<P, B>(f: (p: P) => B): <A>(wa: Traced<P, A>) => Traced<P, [A, B]>实现:
export function listens<P, B>(f: (p: P) => B): <A>(wa: Traced<P, A>) => Traced<P, [A, B]> { return (wa) => (e) => [wa(e), f(e)] }与listen的区别在于:它不直接返回原始位置P,而是先用f: (p: P) => B对位置做一次变换,再与读取结果打包。例如 test/Traced.ts#L109-L127 中_.listens((settings) => settings.settingsTravis)取出的辅助值就是"当前是否开启 Travis"这个布尔派生量。
4.3censor:改写当前位置
Apply a function to the current position
export declare function censor<P>(f: (p: P) => P): <A>(wa: Traced<P, A>) => Traced<P, A>实现:
export function censor<P>(f: (p: P) => P): <A>(wa: Traced<P, A>) => Traced<P, A> { return (wa) => (e) => wa(f(e)) }censor不改变结果类型A,而是在读取前用f对位置做改写——这正是"覆盖配置"的原语。测试 test/Traced.ts#L129-L147 中_.censor((settings) => ({ ...settings, settingsHasLibrary: !settings.settingsHasLibrary }))把settingsHasLibrary取反,最终C.extract得到settingsHasLibrary: true。
4.4tracks:按相对位置提取值
Extracts a value at a relative position which depends on the current value.
export declare function tracks<P, A>(M: Monoid<P>, f: (a: A) => P): (wa: Traced<P, A>) => A实现(src/Traced.ts#L27-L29,源码注释标注了 "TODO: curry in v3",即 v3 将调整柯里化顺序):
export function tracks<P, A>(M: Monoid<P>, f: (a: A) => P): (wa: Traced<P, A>) => A { return (wa) => wa(f(wa(M.empty))) }tracks是最有意思的一个:它先在默认位置M.empty上读取wa得到a,再用f(a)计算出相对偏移量,最后在偏移后的位置wa(f(wa(M.empty)))上取值。因此它被称为"追踪"——根据当前值决定下一步去哪个相对位置读取。
测试 test/Traced.ts#L77-L91 给出了经典用法:travisB = _.tracks(M, (project) => ({ ...M.empty, settingsTravis: project.projectGitHub })),即"如果项目启用了 GitHub,就顺带开启 Travis"。将其与gitHubB通过C.extend组合后,projectTravis正确变为true。
五、type lambdas:URI与类型注册
模块文档的 type lambdas 部分包含两个条目(均 "Added in v2.0.0"):
export declare const URI: 'Traced' export type URI = typeof URI对应 src/Traced.ts#L96-L102。URI是一个字面量类型标签'Traced',配合 src/Traced.ts#L104-L108 的模块扩展,将Traced<E, A>注册进URItoKind2,从而让Functor2、Comonad2C等带 kind 的抽象可以实例化到Traced上。这是 fp-ts 中所有高阶类型(HKT)参与泛型编程的标准机制。
六、zone of death:已废弃的traced
模块文档最后一部分是 "zone of death"(废弃区):
export declare const traced: Functor2<'Traced'>注释明确写着 "UseFunctorinstead."(改用Functor),实现在 src/Traced.ts#L136:
export const traced: Functor2<URI> = Functor从 src/Traced.ts#L129-L135 可以看到它带有@deprecated标记。也就是说,traced只是Functor的别名,为 v2.0.0 的早期用户保留兼容性,新代码应直接使用Functor。这也解释了模块文档中Functor实例为何标注 "Added in v2.7.0"(traced在 v2.0.0 引入、v2.7.0 被正式实例替代)。
七、实战:用Traced构建可组合的增量构建器
测试 test/Traced.ts 提供了一个非常直观的实战模型(Adapted from "Comonadic builders"),我们把它完整展开,展示Traced的完整工作流。
7.1 定义环境与单子
import * as B from 'fp-ts/boolean' import { Monoid, struct } from 'fp-ts/Monoid' import * as _ from 'fp-ts/Traced' interface Settings { readonly settingsHasLibrary: boolean readonly settingsGitHub: boolean readonly settingsTravis: boolean } const M: Monoid<Settings> = struct({ settingsHasLibrary: B.MonoidAny, settingsGitHub: B.MonoidAny, settingsTravis: B.MonoidAny }) const C = _.getComonad(M)这里用 src/Monoid.ts#L140-L151 的struct把三个布尔Monoid合成一个结构体Monoid<Settings>。B.MonoidAny的empty是false、concat是逻辑或(见 src/boolean.ts 中MonoidAny相关定义),因此M.empty就是"全默认关闭"的配置。
7.2 定义构建器
interface Project { readonly projectName: string readonly projectHasLibrary: boolean readonly projectGitHub: boolean readonly projectTravis: boolean } interface ProjectBuilder extends _.Traced<Settings, Project> {} const buildProject = (projectName: string): ProjectBuilder => (settings) => ({ projectName, projectHasLibrary: settings.settingsHasLibrary, projectGitHub: settings.settingsGitHub, projectTravis: settings.settingsTravis })ProjectBuilder就是一个Traced<Settings, Project>:给它一份Settings,它返回一个Project。
7.3 用extend叠加特性
const hasLibraryB = (wa: ProjectBuilder): Project => { const p = { ...M.empty, settingsHasLibrary: true } return wa(p) } const gitHubB = (wa: ProjectBuilder): Project => { const p = { ...M.empty, settingsGitHub: true } return wa(p) }这些"特性函数"接收一个ProjectBuilder并返回新的Project,恰好匹配extend的f: (wa) => B形状。于是可以通过C.extend逐层叠加:
// extract:以默认配置构建 C.extract(buildProject('myproject')) // => { projectName: 'myproject', projectHasLibrary: false, projectGitHub: false, projectTravis: false } // extend 一层:开启 library C.extract(C.extend(buildProject('myproject'), hasLibraryB)) // => projectHasLibrary: true // 两层叠加 + tracks 自动联动 const travisB = _.tracks(M, (project) => ({ ...M.empty, settingsTravis: project.projectGitHub })) C.extract(C.extend(C.extend(buildProject('github-travis'), gitHubB), travisB)) // => { projectName: 'github-travis', projectGitHub: true, projectTravis: true }这里展示了Traced与Comonad的组合威力:每个extend层都是对环境的增量改写,tracks还能根据前一层的结果自动推导下一层的位置偏移,最终由extract在M.empty(默认环境)上一次成型。
7.4 与listen/listens/censor搭配
listen:C.extract(_.listen(buildProject('myproject')))返回[Project, Settings],可同时拿到构建结果与所用配置;listens:pipe(buildProject('myproject'), _.listens((s) => s.settingsTravis))返回[Project, boolean],取出的辅助值是"当前是否启用 Travis";censor:在读取前改写配置,如测试中取反settingsHasLibrary。
这四个工具函数让Traced既能"读",也能"看"(审计当前位置)、"改"(覆盖配置)、"追"(按相对位置跳转),构成一套完整的环境操作 DSL。
八、小结与适用场景
| API | 类型 | 作用 | 引入版本 |
|---|---|---|---|
Traced<P, A> | 模型 | (p: P) => A,环境读取函数 | v2.0.0 |
Functor | 实例 | 对读取结果map | v2.7.0 |
getComonad(monoid) | 实例构造器 | 基于Monoid<P>构造Comonad2C | v2.0.0 |
map | mapping | 柯里化映射 | v2.0.0 |
flap | mapping | 值翻转应用 | v2.10.0 |
listen | utils | 取出当前位置 | v2.0.0 |
listens | utils | 取出位置的派生值 | v2.0.0 |
censor | utils | 改写当前位置 | v2.0.0 |
tracks | utils | 按相对位置提取值 | v2.0.0 |
traced | 废弃 | 用Functor替代 | v2.0.0(废弃) |
适用场景包括:配置驱动的增量构建(如测试中的工程脚手架生成)、带上下文的审计日志(listen/listens)、基于默认值逐层覆盖的设置系统(censor+extract),以及任何"从可叠加环境中读取派生数据"的领域。需要特别说明的是,getComonad强依赖Monoid<P>:只有P具备幺元与可结合拼接运算,extract(读默认位置)和extend(位置偏移)才能成立。
如果你想进一步深入,可以继续阅读 Traced 源码、模块文档 与配套的 测试用例,并结合 Comonad 接口定义 与 Monoid 构造工具 理解其抽象层级。
- 开发工具
【免费下载链接】fp-ts
Functional programming in TypeScript
相关推荐
如何快速分析GPS轨迹数据?GPXSee终极免费工具完整指南
如何快速分析GPS轨迹数据?GPXSee终极免费工具完整指南 GPS轨迹分析是户外运动爱好者和专业地理工作者的必备技能,但面对众多GPS文件格式,你是否感到困惑
桌面应用GIS探索 fp-ts:函数式编程在 TypeScript 中的力量
探索 fp ts:函数式编程在 TypeScript 中的力量 是一个由 Giacomo Canti 开发的开源库,它为 TypeScript 提供了一套全面的
开发工具LeeGo高级组件:Union Brick实现复杂界面组合的终极方案
LeeGo高级组件:Union Brick实现复杂界面组合的终极方案 LeeGo是一个声明式、可配置且高度可重用的Swift UI开发框架,让iOS界面开发像搭
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考