fp-ts 入门指南:在 TypeScript 中实现类型化函数式编程
开发工具【免费下载链接】fp-tsFunctional programming in TypeScript项目地址https://gitcode.com/gh_mirrors/fp/fp-ts点击查看免费下载导读本文以 fp-ts 项目官方文档docs/index.md为主线系统讲解 fp-ts 的核心概念体系——纯函数、数据类型、类型类与高阶类型Higher Kinded Types。你将理解 fp-ts 如何在 TypeScript 中构建「用类型类驱动、以高阶抽象为骨架」的函数式编程方式掌握安装配置、第一个程序、为自定义数据类型编写类型类实例等实战能力并借助仓库源码理解URItoKind、Kind、HKT等底层机制的真实运行原理。fp-ts 是什么fp-ts是一个以「在 TypeScript 中提供类型化函数式编程能力」为目标的库。它把来自 Haskell、PureScript、Scala 等类型化函数式语言中最流行的数据类型data types、类型类type classes与抽象abstractions带到 TypeScript 中帮助开发者编写构建于高阶抽象之上的纯函数式应用与库。从 package.json 可以看到fp-ts 是一个纯 TypeScript 实现、零运行时依赖的库sideEffects: false可安全参与 tree-shaking当前仓库版本为 2.16.11采用 MIT 许可证。需要特别说明的免责声明fp-ts 项目本身不承担函数式编程教学职责——官方文档假设读者已经了解什么是函数式编程。本文聚焦于 fp-ts 库本身的结构与用法函数式编程基础概念如纯函数、范畴论只做必要铺垫。核心概念总览fp-ts 的目标是让开发者能够写出构建在高阶抽象之上的纯函数式应用。整体设计围绕三个支柱展开函数Functions函数式编程的核心是纯函数及其组合数据类型Data Types可实例化的实用部分用于解决具体问题类型类Type Classes理论基础描述「你能对数据做什么」其定律扎根于抽象代数与范畴论。三者互相配合数据类型通过类型类获得通用接口类型类通过高阶类型机制HKT才能抽象地操作任意数据类型。函数Functions函数式编程的一切围绕纯函数以及如何把它们组合成更大的结构展开。fp-ts 在 src/function.ts 中提供了若干通用工具函数涵盖组合、恒等函数、常量函数等场景。以最基础的组合工具为例// src/function.ts 中定义的恒等函数 export function identityA(a: A): A { return a }pipe是 fp-ts 中最常用的组合工具它把值从左到右依次送入一系列函数避免嵌套调用。flow则用于直接组合多个函数得到一个新函数。此外src/function.ts还提供了apply、unsafeCoerce以及针对函数类型本身的实例构造器例如getSemigroup、getMonoid、getRing它们说明「一元函数在其值域具备某种代数结构时也具备对应结构」。fp-ts 的每个通用工具函数都带有since版本标注与 JSDoc 示例完整的函数清单可查看 docs/modules/function.ts.md。数据类型Data Types数据类型是 fp-ts 的实用部分你可以用自己的数据实例化它们从而获得解决特定需求所需的性质与功能。// Option显式建模可能缺失的值 import { pipe } from fp-ts/function import * as O from fp-ts/Option const inverse (n: number): O.Optionnumber n 0 ? O.none : O.some(1 / n) const result pipe( inverse(2), O.map((n) n * 100), O.getOrElse(() 0) ) console.log(result) // 50Option表示一个要么存在some、要么缺失none的值。相比抛出异常或返回null它把缺失情况显式建模为类型的一部分再通过map、flatMap、match、getOrElse等组合子安全地处理。fp-ts 提供了数十种这样的数据类型见 src/index.ts 的导出清单按category分类Either、Task、TaskEither、Reader、State、These、IO、ReadonlyArray、ReadonlyRecord等。关键设计数据类型的共性通过类型类统一。所有数据类型共享同一套接口经由类型类因此「学会一个数据类型的用法就能举一反三套用到其他数据类型上」。特设多态与实例Instancefp-ts 中大量函数使用了特设多态ad hoc polymorphism一个函数只有单一实现却能处理不同类型参数。要让这一点成立通常需要为数据类型提供实例instance——即针对该数据类型特化的具体实现。以Functor为例其接口定义于 src/Functor.tsexport interface FunctorF { readonly URI: F readonly map: A, B(fa: HKTF, A, f: (a: A) B) HKTF, B }任何数据类型只要提供一个包含URI和map的对象例如src/Identity.ts中的Identity.Functor、src/Option.ts中的Option.Functor就获得了map能力。注意fp-ts 的数据类型默认不是栈安全的也没有 trampolining蹦床实现。不过对于遍历traverse和序列化sequence列表这类操作各数据类型内部提供了栈安全且经过优化的专门版本。类型类Type Classes类型类构成 fp-ts 的理论底座它们精确描述「你能对数据做什么」。为了保证可安全组合类型类建立在扎根于抽象代数与范畴论的**定律laws**之上。例如Functor必须满足两条定律见 src/Functor.ts 顶部文档同一律F.map(fa, a a) - fa复合律F.map(fa, a bc(ab(a))) - F.map(F.map(fa, ab), bc)仓库中以类型类命名的模块众多Functor、Applicative、Monad、Monoid、Semigroup、Eq、Ord、Foldable、Traversable、Bifunctor、Profunctor、Category等见 src/index.ts 中category model分组。它们按照定律层层叠加Monad建立在Applicative之上Applicative建立在Apply与Pointed之上Apply又建立在Functor之上——这保证了组合的安全性。高阶类型Higher Kinded Types高阶类型是 fp-ts 区别于其他函数式库的标志性特性。TypeScript 原生并不支持高阶类型fp-ts 的模拟方案基于论文Lightweight higher-kinded polymorphism类型反函数化type defunctionalization。整个机制的核心实现在 src/HKT.ts由三部分组成1.HKT家族抽象的类型构造器// * - * 构造器 export interface HKTURI, A { readonly _URI: URI readonly _A: A }HKT表示 kind 为* - *的类型构造器。fp-ts 为每种 kind 各定义了一个接口最多支持到四参见 src/HKT.ts 第 11-38 行接口对应的 kindHKTURI, A* - *HKT2URI, E, A* - * - *HKT3URI, R, E, A* - * - * - *HKT4URI, S, R, E, A* - * - * - * - *2.URItoKind家族类型层面的字典URItoKind是一个类型层面的映射type-level map把「URI 字符串」映射到具体数据类型并通过 TypeScript 的module augmentation模块扩充机制来填充// src/HKT.ts初始为空接口 export interface URItoKindA {} // 在 Identity 模块中扩充 declare module ./HKT { interface URItoKindA { readonly Identity: IdentityA // 把键 Identity 映射到类型 Identity } }src/HKT.ts依次定义了URItoKind、URItoKind2、URItoKind3、URItoKind4分别对应 1 到 4 个类型参数的类型构造器。3.URIS与Kind家族约束与投影URIS只是keyof URItoKindanysrc/HKT.ts 第 80 行作为Functor1等类型类接口的泛型约束KindF, A内部使用URItoKind能把抽象类型投影为具体类型若URI Identity则KindURI, number就是Identitynumber即number。export type KindURI extends URIS, A URI extends URIS ? URItoKindA[URI] : anyURIS2/Kind2、URIS3/Kind3、URIS4/Kind4依此类推src/HKT.ts 第 86-126 行覆盖Either二参、Reader环境 值、StateReaderTaskEither四参等更复杂的类型构造器。安装、配置与第一个程序安装fp-ts 是纯 TypeScript 库、无运行时依赖可通过主流包管理器安装# npm npm install fp-ts # yarn yarn add fp-ts # pnpm pnpm add fp-tsTypeScript 配置fp-ts要求开启 TypeScript 的strict模式。关闭它会导致大量组合子的类型推断失效// tsconfig.json { compilerOptions: { strict: true } }导入风格fp-ts 推荐按模块单独导入以保持较小的打包体积// 推荐按需导入具体模块 import * as E from fp-ts/Either import { pipe } from fp-ts/function // 不推荐用于生产环境一次性导入全部 import * as fp from fp-tsfp-ts 的入口聚合模块 src/index.ts 导出了全部子模块option、either、task、function、hkt、pipeable等但在生产环境中按需导入更有助于 tree-shaking。完整示例命令式 vs 函数式src/Option.ts 顶部提供了一个极具代表性的对比示例取列表头元素并求倒数命令式版本依赖异常处理函数式版本用Option组合子完成import * as O from fp-ts/Option import { pipe } from fp-ts/function const head A(as: ReadonlyArrayA): O.OptionA as.length 0 ? O.none : O.some(as[0]) const inverse (n: number): O.Optionnumber n 0 ? O.none : O.some(1 / n) const functional (as: ReadonlyArraynumber): string pipe( as, head, O.map((n) n * 2), O.flatMap(inverse), O.match( () no result, // onNone 分支 (head) Result is ${head} // onSome 分支 ) )实战为你的数据类型编写类型类实例理解了URItoKind、URIS、Kind三件套之后就可以为自定义数据类型注册类型类实例。完整推导参见官方指南 docs/guides/HKT.md下面从最简单的Identity开始。为 kind* - *的数据类型编写Functor实例// Identity.ts import { Functor1 } from fp-ts/Functor export const URI Identity export type URI typeof URI declare module fp-ts/HKT { interface URItoKindA { readonly Identity: IdentityA } } export type IdentityA A export const Functor: Functor1URI { URI, map: (ma, f) f(ma) }Functor1的定义src/Functor.ts 第 34-37 行export interface Functor1F extends URIS { readonly URI: F readonly map: A, B(fa: KindF, A, f: (a: A) B) KindF, B }为 kind* - * - *的数据类型编写实例二参类型构造器如Either使用另一套三元组URItoKind2、URIS2、Kind2// Either.ts import { Functor2 } from fp-ts/Functor export const URI Either export type URI typeof URI declare module fp-ts/HKT { interface URItoKind2E, A { readonly Either: EitherE, A } } export interface LeftE { readonly _tag: Left readonly left: E } export interface RightA { readonly _tag: Right readonly right: A } export type EitherE, A LeftE | RightA export const right A, E never(a: A): EitherE, A ({ _tag: Right, right: a }) export const Functor: Functor2URI { URI, map: (ma, f) (ma._tag Left ? ma : right(f(ma.right))) }Functor2的定义src/Functor.ts 第 43-46 行export interface Functor2F extends URIS2 { readonly URI: F readonly map: E, A, B(fa: Kind2F, E, A, f: (a: A) B) Kind2F, E, B }注意map中E保持自由——Either的map只作用于右侧的成功值左侧错误原样保留。编写抽象于类型类之上的泛型函数以lift为例它接受任意Functor实例把普通函数提升为作用于容器上的函数import { HKT } from fp-ts/HKT export function liftF(F: FunctorF): A, B(f: (a: A) B) (fa: HKTF, A) HKTF, B { return (f) (fa) F.map(fa, f) }但直接使用会类型检查失败const double (n: number): number n * 2 const doubleIdentity lift(identity)(double) // 报错Argument of type Functor1Identity is not assignable to parameter of type FunctorIdentity原因是Identity的实例是Functor1而非Functor。解决方案是为每种要支持的 kind 提供函数重载export function liftF extends URIS2( F: Functor2F ): A, B(f: (a: A) B) E(fa: Kind2F, E, A) Kind2F, E, B export function liftF extends URIS(F: Functor1F): A, B(f: (a: A) B) (fa: KindF, A) KindF, B export function liftF(F: FunctorF): A, B(f: (a: A) B) (fa: HKTF, A) HKTF, B export function liftF(F: FunctorF): A, B(f: (a: A) B) (fa: HKTF, A) HKTF, B { return (f) (fa) F.map(fa, f) }现在double可以同时提升到Identity与Eitherconst doubleIdentity lift(identity)(double) // (fa: Identitynumber) Identitynumber const doubleEither lift(either)(double) // E(fa: EitherE, number) EitherE, number从源码结构看fp-ts 内部正是大量采用这种「为每种 kind 提供重载 宽松签名的实现」模式例如 src/Functor.ts 中的map组合子用于 Functor 复合就针对Functor1/Functor2/Functor3的组合提供了十余个重载签名。命名约定与实例变体Code Conventionsfp-ts 的类型类接口与函数有一套严谨的后缀命名约定理解它们能大幅提升阅读与使用效率。官方约定详见 docs/guides/code-conventions.md。类型类接口数字与C后缀数字代表 kind类型构造器接受几个类型参数C代表 Constrained受约束的。Kind类型类类型反函数化说明全部FunctorFHKTF, A基础版本* - *Functor1FKindF, A如Identity* - * - *Functor2FKind2F, E, A如Either* - * - *Functor2CF, EKind2F, E, AFunctor2的变体E被固定* - * - * - *Functor3FKind3F, R, E, A如Reader* - * - * - *Functor3CF, R, EKind3F, R, E, AFunctor3的变体R与E均被固定受约束变体的典型场景是Validation只有为失败侧提供了Semigroup实例才能获得Functor实例src/Functor.ts 第 52-75 行中的Functor2C、Functor3C正是为此设计export interface Functor2CF extends URIS2, E { readonly URI: F readonly _E: E readonly map: A, B(fa: Kind2F, E, A, f: (a: A) B) Kind2F, E, B }函数后缀E / K / T / WE EffectmatchE这类析构函数的分支返回的是带效果的值如Task而非普通值。例如TaskOption.matchE的两个分支都返回Tasknumber见 docs/guides/code-conventions.md 中的示例。K KleisliKleisli 箭头指形如(a: A) FB的函数。fromEitherK、chainEitherK把(s: string) EitherE, B提升为作用在IOEither/TaskEither等 monad transformer 上的函数。T TuplesequenceT中的T表示「元组」名称借自对应的 Haskell 函数但在OptionT、EitherT、ReaderT、StateT中T通常表示Transformermonad 变换子。sequenceT在 src/Apply.ts 中实现把若干应用式值组合为包含结果元组的应用式值如sequenceTOption(O.some(1), O.some(2))得到O.some([1, 2])。W Widen拓宽以W结尾的函数能把错误聚合为联合类型针对Either系数据类型或把环境合并为交叉类型针对Reader系数据类型。例如chainW让错误类型从string | Error这样的联合中正确推断而不是因为Type string is not assignable to type Error而报错。模块结构规范每个定义数据结构的模块通常遵循固定结构见 docs/guides/code-conventions.mdURI定义与模块扩充declare module ./HKT数据结构定义伴随函数constructors、combinators实例函数私有以_前缀命名如 src/Identity.ts 中的_map、_ap类型类实例定义常量或函数。以 src/Identity.ts 为范本可以看到一个模块如何同时导出Functor、Pointed、Apply、Applicative、Chain、Monad、Foldable、Traversable、Alt、Comonad、ChainRec共 11 个实例并复用同一个_map实现。进一步学习路径完整的 API 参考浏览 docs/modules 目录下各模块文档如 docs/modules/Option.ts.md、docs/modules/Either.ts.md类型类实例编写详解docs/guides/HKT.md代码规范与命名约定docs/guides/code-conventions.mddo notation管道式绑定用法docs/guides/do-notation.md从 v1 升级到 v2 的迁移指南docs/guides/upgrade-to-v2.md更多学习资料docs/learning-resources.md。仓库中的 examples 目录提供了完整可运行的示例程序dtslint 目录则存放了用于验证类型签名正确性的类型级测试——对深入理解 fp-ts 的类型机制是很好的补充阅读材料。赞分享开发工具【免费下载链接】fp-tsFunctional programming in TypeScript项目地址https://gitcode.com/gh_mirrors/fp/fp-ts点击查看免费下载相关推荐深入理解fp-ts函数式编程在TypeScript中的实践指南深入理解fp ts函数式编程在TypeScript中的实践指南 前言 在当今前端开发领域函数式编程 FP 越来越受到开发者们的青睐。fp ts作为TypeS开发工具探索 fp-ts函数式编程在 TypeScript 中的力量探索 fp ts函数式编程在 TypeScript 中的力量 是一个由 Giacomo Canti 开发的开源库它为 TypeScript 提供了一套全面的开发工具vue-star-rating 自定义星形教程用 star-points 打造心形与钻石形评分vue star rating 自定义星形教程用 star points 打造心形与钻石形评分 vue star rating 是一款简单又高度可定制的 Vu上一篇【亲测免费】 探秘Python URX一款强大的机器人编程库下一篇深入解析Component从入门到精通的完整前端包管理指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考