effect-smol Optic 函数式光学:Lens、Prism 与 Traversal 完整指南
【免费下载链接】effect-smolCore libraries and experimental work for Effect v4项目地址: https://gitcode.com/GitHub_Trending/ef/effect-smol
如果你在用 TypeScript 处理嵌套不可变数据,一定被"改一个字段要重写整棵对象树"折磨过。effect-smol项目中的Optic 模块(函数式光学)正是解药:它让你像相机变焦一样,用Lens、Prism、Traversal等"光学元件"安全地聚焦、读取和更新嵌套结构中的任意部分——且全程不修改原数据。
🎯 什么是函数式光学?
把光学想象成一个可复用的"聚焦器":它描述了"在哪里看、在哪里改"。每个光学都支持三个核心操作:
| 操作 | 含义 | 类比 |
|---|---|---|
get | 读取聚焦到的值 | 用放大镜看细节 |
replace | 替换聚焦值 | 换掉那颗螺丝 |
modify | 用函数变换聚焦值 | 把螺丝拧紧一点 |
构建方式像搭积木:先聚焦company,再聚焦address,再聚焦street,层层下钻。定义一次,到处复用。
📖 官方文档:packages/effect/OPTIC.md 🔧 核心源码:packages/effect/src/Optic.ts
🧩 五种光学:一分钟认识家族成员
Optic 模块提供了五种光学类型,它们共享同一套组合方式:
| 光学 | 聚焦目标 | 聚焦后一定有值吗? | 典型场景 |
|---|---|---|---|
| Iso | 无损可逆的类型转换 | ✅ 必有 | 摄氏度 ↔ 华氏度 |
| Lens | 一定存在的字段 | ✅ 必有 | user.name |
| Prism | 联合类型的某一个分支 | ⚠️ 可能没有 | 匹配_tag: "A" |
| Optional | 可能存在的焦点 | ⚠️ 可能没有 | 可选字段、数组下标 |
| Traversal | 集合中的零或多个元素 | ⚠️ 多个 | 遍历数组所有正数 |
一句话记忆:Iso 最强、Lens 最常用、Prism 管联合、Traversal 管批量。
类型定义可以在源码中查看:
Iso接口:packages/effect/src/Optic.ts#L66Lens接口:packages/effect/src/Optic.ts#L144Prism接口:packages/effect/src/Optic.ts#L232Traversal接口:packages/effect/src/Optic.ts#L1033
⚡ 快速上手:三行代码聚焦字段
先看最小示例——聚焦对象S中的字段a:
import { Optic } from "effect" type S = { readonly a: number } const _a = Optic.id<S>().key("a") _a.get({ a: 1 }) // 1 读取 _a.replace(2, { a: 1 }) // { a: 2 } 替换 _a.modify((n) => n + 1)({ a: 1 }) // { a: 2 } 变换注意Optic.id<S>():它是"身份光学"(identity Iso),表示"先聚焦整个对象本身",是所有光学的起点。
嵌套数据:层层下钻到深层字段
假设要修改员工信息中「公司 → 地址 → 街道 → 名称」这个深埋的字段:
const _streetName = Optic.id<Employee>() .key("company") // 进入 company .key("address") // 进入 address .key("street") // 进入 street .key("name") // 锁定 name const capitalizeStreetName = _streetName.modify(String.capitalize)调用capitalizeStreetName(from)后,只有street.name的首字母被大写,其余数据原封不动,且原对象保持不可变。路径只定义一次,改数据结构时只需改这一个地方。
🔍 Lens 实战:安全处理可选字段
Lens 聚焦"一定存在"的字段,但 Optic 对可选数据也有声明式处理,关键是区分两种语义:
key("a"):值为undefined时保留该键 →{ a: undefined }optionalKey("a"):值为undefined时删除该键 →{}
const _a = Optic.id<S>().optionalKey("a") _a.replace(2, {}) // { a: 2 } 从"无"到"有" _a.replace(undefined, { a: 1 }) // {} 从"有"到"无"(键被移除)这套语义让"清除字段"这类操作变得干净利落,无需手写delete或解构技巧。
🔺 Prism 实战:安全聚焦联合类型的一个分支
联合类型(tagged union)是函数式编程的常客。直接用key访问某个分支的字段会踩空,而Prism通过tag方法聚焦特定分支:
type S = | { readonly _tag: "A"; readonly a: number } | { readonly _tag: "B"; readonly b: number } const _a = Optic.id<S>().tag("A").key("a") _a.replace(2, { _tag: "A", a: 1 }) // { _tag: 'A', a: 2 } 匹配,更新 _a.replace(2, { _tag: "B", b: 1 }) // { _tag: 'B', b: 1 } 不匹配,原样返回约定用_tag字段标识变体。不匹配时优雅地无操作,而不是抛错——这让联合类型的更新既安全又可预测。
🚂 Traversal 实战:批量处理集合元素
Traversal聚焦集合中的多个元素。配合forEach+ 条件判断,可以精准批处理:
const _positive = Optic.id<S>() .key("a") // 聚焦数组 .forEach((item) => item.check(Schema.isGreaterThan(0))) // 只保留正数 const addOne = _positive.modifyAll((n) => n + 1) addOne({ a: [1, -2, 3] }) // { a: [2, -2, 4] } 负数不受影响📌 技术细节:与其他光学库不同,这里的Traversal不是独立类型,而是被建模为"聚焦到ReadonlyArray<A>的Optional"。要对每个元素继续下钻,通过forEach传入的Iso<A, A>与后续光学组合即可。
🧬 进阶:从 Schema 一键生成光学
如果你有 Effect 的 Schema,可以直接生成光学起点,省去手写路径:
import { Schema } from "effect" const schema = Schema.Struct({ a: Schema.String, b: Schema.Number }) const _b = Schema.toIso(schema).key("b") // 自动获得类型安全的 LensSchema.Class也开箱即用,类实例可以直接作为光标的操作对象。相关测试见 packages/effect/test/schema/toIso.test.ts。
⚖️ 既然有 Immer,为什么还要 Optic?
Immer 让"写可变代码、得到不可变结果",日常够用。但当你面对深层嵌套 + 联合类型 + 可复用更新逻辑时,Optic 的优势很明显:
| 维度 | Immer | Optic |
|---|---|---|
| 路径导航 | 每次更新都手写一遍 | 定义一次 Lens,到处复用 |
| 可选字段 | 手动if守卫 | notUndefined()声明式表达 |
| 结构演化 | 散落各处的路径同步修改 | 改一处光学定义,全局生效 |
核心哲学是组合胜过嵌套:把小光学拼成大光学,代码保持扁平、可测试、可跨模块共享。
📋 选型速查表
不知道怎么选光学?对照这张表:
| 数据形状 | 用这个 |
|---|---|
| 一定存在的字段 | key |
可选字段(保留undefined) | key |
| 可选字段(清除时删键) | optionalKey |
| 联合类型的某个分支 | tag |
| Record / 数组下标 | at |
| 过滤并批量更新元素 | forEach+check/notUndefined |
另外,如果聚焦失败需要排查,用getResult显式拿到"成功/失败"结果,比静默跳过更易调试。
🚀 开始学习 Optic
| 资源 | 路径 |
|---|---|
| 官方完整文档(含全部示例) | packages/effect/OPTIC.md |
| 核心实现(类型 + 构造函数 + 操作) | packages/effect/src/Optic.ts |
| 单元测试(行为参考) | packages/effect/test/Optic.test.ts |
| Schema 生成光学测试 | packages/effect/test/schema/toIso.test.ts |
上手路线建议:先用key链式聚焦嵌套字段建立手感 → 再用tag处理联合类型 → 最后用forEach+check玩转批量更新。配合 packages/effect/OPTIC.md 中的完整示例,你就能在不可变数据的世界里自由"变焦"了。
【免费下载链接】effect-smolCore libraries and experimental work for Effect v4项目地址: https://gitcode.com/GitHub_Trending/ef/effect-smol
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考