fp-ts 中的 Traced 余单子(Comonad):基于 `P => A` 函数的环境读取与增量构建
2026/9/23 13:14:54 网站建设 项目流程
  • 开发工具

【免费下载链接】fp-ts

Functional programming in TypeScript

项目地址:https://gitcode.com/gh_mirrors/fp/fp-ts
点击查看免费下载

导读

Traced是 fp-ts 中一种以"纯函数读取环境"为形态的类型构造器:Traced<P, A>本质上就是(p: P) => A,它把"从某个可叠加的上下文P中读出值A"封装成可组合的一等公民。本文以 Traced.ts 模块文档 为核心,结合 Traced 源码 与 Traced 测试用例 中的ProjectBuilder实例,完整讲解Traced的模型、Functor/Comonad实例,以及censorlistenlistenstracks四个工具函数的实现原理与实战用法。读完本文,你将能够用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 中的URItoKind2Traced才能参与Functor2Comonad2C等带 kind 的泛型实例构造。

1.2 为什么要用"函数"做数据类型?

Traced的妙处在于:函数本身就是一种数据结构。一个Traced<P, A>尚未被"消费"时,它只是一个闭包;只有当调用它并传入具体的P时,值才会被计算出来。这带来两个直接优势:

  1. 延迟求值:构建复杂的Traced链并不会立即执行任何计算;
  2. 可组合的上下文叠加:借助Monoid<P>,多个Traced的环境可以像"覆盖配置"一样逐层合并。

模块文档在 docs/modules/Traced.ts.md 中将Traced归于model分类,并标注 "Added in v2.0.0",说明它是 fp-ts 2.x 的基石抽象之一。


二、实例:FunctorgetComonad

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):

  1. 同一律map(fa, a => a) <-> fa
  2. 复合律map(fa, bc ∘ ab) <-> map(map(fa, ab), bc)

由于Tracedmap本质是函数复合,这两条定律天然成立。

2.2getComonadTraced的余单子结构

模块文档的核心签名:

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接口,并向上提供Functor2Cmap

从源码结构看,Monoid<P>emptyconcatTraced余单子语义的基石: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 一族:mapflap

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,从而让Functor2Comonad2C等带 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.MonoidAnyemptyfalseconcat是逻辑或(见 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,恰好匹配extendf: (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 }

这里展示了TracedComonad的组合威力:每个extend层都是对环境的增量改写,tracks还能根据前一层的结果自动推导下一层的位置偏移,最终由extractM.empty(默认环境)上一次成型。

7.4 与listen/listens/censor搭配

  • listenC.extract(_.listen(buildProject('myproject')))返回[Project, Settings],可同时拿到构建结果与所用配置;
  • listenspipe(buildProject('myproject'), _.listens((s) => s.settingsTravis))返回[Project, boolean],取出的辅助值是"当前是否启用 Travis";
  • censor:在读取前改写配置,如测试中取反settingsHasLibrary

这四个工具函数让Traced既能"读",也能"看"(审计当前位置)、"改"(覆盖配置)、"追"(按相对位置跳转),构成一套完整的环境操作 DSL。


八、小结与适用场景

API类型作用引入版本
Traced<P, A>模型(p: P) => A,环境读取函数v2.0.0
Functor实例对读取结果mapv2.7.0
getComonad(monoid)实例构造器基于Monoid<P>构造Comonad2Cv2.0.0
mapmapping柯里化映射v2.0.0
flapmapping值翻转应用v2.10.0
listenutils取出当前位置v2.0.0
listensutils取出位置的派生值v2.0.0
censorutils改写当前位置v2.0.0
tracksutils按相对位置提取值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

项目地址:https://gitcode.com/gh_mirrors/fp/fp-ts
点击查看免费下载
上一篇:终极指南:如何高效优化Kubeshark大规模集群流量处理性能
下一篇:djLint 项目使用教程

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询