多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Unison 类型更新实战:将数据构造函数重构为 Smart Constructor(以 `unique type Foo` 为例)

Unison 类型更新实战:将数据构造函数重构为 Smart Constructor(以 `unique type Foo` 为例) 编程语言编译器语言运行时开发工具【免费下载链接】unisonA friendly programming language from the future项目地址https://gitcode.com/gh_mirrors/un/unison点击查看免费下载本指南围绕unison-src/transcripts/idempotent/update-type-turn-constructor-into-smart-constructor.md这条 UCM 转录脚本展开讲解如何在不破坏代码库的前提下把 Unison 中一个公开的数据构造函数Bar降级为模块内部构造internal.Bar并提供一个同名的 Smart Constructor智能构造函数作为对外入口。读完本文你将掌握add/update命令的完整工作流、internal名称的作用域语义、Smart Constructor 的封装动机以及如何通过view与find.verbose验证重构结果。一、转录文档概述这是一个什么样的文档unison-src/transcripts/idempotent/目录是 Unison 项目用来做端到端回归测试的转录transcript文件集合。每条 transcript 都是一份可执行的 Markdown其中的代码块要么是unison语言片段会被加载进临时 scratch 文件并解析、类型检查要么是ucm命令片段会被实际送入 UCM 交互式终端执行而:added-by-ucm标注的代码块则是转录运行后 UCM 回填的真实输出。idempotent幂等前缀意味着这些转录在相同状态下重复运行应产生相同结果。本次关联文档update-type-turn-constructor-into-smart-constructor.md就是这类 transcript 中的一条它的文件名本身就是一句话需求——更新类型把构造函数变成 Smart Constructor。整篇文档仅由一个连续的操作序列构成本文会逐段拆解其中的每个命令、每段代码和每行输出并结合仓库源码解释其背后的实现机制。想对比阅读其他以update为主题的同目录转录可以参考 addupdatemessages.md、add-run.md、branch-squash.md 等它们共同刻画了 UCMupdate命令的各种行为边界。二、前置条件与初始状态2.1 合并内置库转录的第一步是builtins.merge builtins.merge lib.builtin该命令把 Unison 的内置库builtins例如Nat、等语言内置类型和函数合并到名为lib.builtin的命名空间中。Unison 是一种无保留字语言Nat、Text等基础类型与、这类运算符都是通过这种方式注入普通名称空间的普通定义而不是编译器硬编码的语法关键字。执行builtins.merge之后Nat类型及其上的()运算才在当前分支中可用后续示例代码中的n10才能被解析和类型检查。2.2 初始代码一个普通的unique type紧接着转录定义了一个一元数据类型和一个普通函数unique type Foo Bar Nat makeFoo : Nat - Foo makeFoo n Bar (n10)这里有两个要点值得解释unique typeUnison 支持两种数据类型声明unique type与普通type。unique修饰符表示这个类型在其内容之外还包含一个全局唯一标识符在v2哈希方案下唯一性是通过内容哈希加上类型级别标记共同决定的。它使得即使是结构上完全相同、只是名字不同的两个类型也被视为不同同时它保证了这个类型就是它自己从而允许我们对该类型做模式匹配而不会与同结构的其他类型混淆。makeFoo : Nat - Foo这是一个典型的辅助构造函数——它包装了Bar在构造时对输入做了n10的加工。注意在 Unison 中类型与其构造函数的全限定名是绑定在一起的type Foo Bar Nat隐式定义了Foo.Bar : Nat - Foo这个构造函数。所以此时Bar既是构造Foo的唯一途径也是Foo这个数据类型对外暴露的公开接口。2.3 将初始定义加入代码库 add Okay, Im searching the branch for code that needs to be updated... Done.UCM 的add命令把 scratch 文件中检测到的变更写入当前分支。转录输出中出现的 Im searching the branch for code that needs to be updated... 是 UCM 在add/update时的固定流程提示语它表示 UCM 正在扫描分支中所有依赖关系判断哪些定义需要同步更新例如makeFoo依赖类型Foo当Foo的定义变化时makeFoo是否受影响。这里的输出为 Done.说明首次add一切顺利没有需要额外处理的级联更新。2.4 中间状态小结此时代码库中类型Foo是公开可构造的Foo.Bar以Foo前缀限定可以直接构造FoomakeFoo是用户自定义的、对输入做加工的工厂函数尚未有任何封装或隐藏概念——任何代码都能直接Bar或Foo.Bar构造Foo。这正是本转录要改变的初始局面。三、核心操作把构造函数改为内部构造 Smart Constructor3.1 新代码的语义转录的关键一步是提交如下新代码然后执行updateunique type Foo internal.Bar Nat Foo.Bar : Nat - Foo Foo.Bar n internal.Bar n这段代码做了三件事构造函数改名类型定义从type Foo Bar Nat变为type Foo internal.Bar Nat。internal是 Unison 中一个具有特殊作用域语义的名字——凡是以internal.为前缀的构造函数都属于内部构造器internal constructor。它们在模式匹配中依然可用但不能直接以Foo.internal.Bar之外的形式被普通用户代码构造。同名的公开入口新定义了一个Foo.Bar : Nat - Foo其实现Foo.Bar n internal.Bar n直接转调内部构造器。类型声明保持unique type不变因此转录输出中才会有 (and 1 unchanged type) 的提示。于是重构后的Foo对外呈现为构造Foo的唯一公共途径是 Smart ConstructorFoo.Bar而类型真正的数据构造器internal.Bar被藏在了internal名字空间后面。任何想要直接构造Foo的代码都必须显式写出Foo.internal.Bar——这通常意味着它绕过了 Smart Constructor 可能施加的不变量检查。关于模块化语义的更深层讨论可参见 docs/type-declarations.markdown其中阐述了如何不暴露内部实现、保持数据不变量、自由变更内部实现而不破坏客户端代码这一 Unison 设计目标并探讨了internal、friend[Foo]、private[Foo]等候选机制的取舍。3.2 UCM 检测到的变更Loading changes detected in scratch.u. ~ Foo.Bar : Nat - Foo (and 1 unchanged type) ~ (modified) Run update to apply these changes to your codebase.:added-by-ucm是转录运行后由 UCM 自动回填的输出它展示了update执行前 UCM 的变更预扫描结果~ Foo.Bar : Nat - Foo波浪号~表示Foo.Bar这个**术语term**将被更新它是新加入的同名 Smart Constructor(and 1 unchanged type)类型Foo的声明体虽然包含构造函数名的修改但 UCM 判定其类型层面没有变化~ (modified)表示存在被修改的定义需要update来落盘。这里值得注意一个细节尽管Foo.Bar的签名Nat - Foo与原来Foo.Bar的签名完全相同UCM 依然将其标记为修改因为它的内容term 体变了——不再是隐式生成的裸构造函数而是一个调用internal.Bar的包装函数。这正是 Smart Constructor 重构在 UCM 变更检测层面的直接体现公开名字没变背后实现换了。3.3 执行update update Okay, Im searching the branch for code that needs to be updated... Thats done. Now Im making sure everything typechecks... Everything typechecks, so Im saving the results... Done.与add不同的是update会先扫描分支中所有引用旧定义的代码例如makeFoo引用了Bar尝试自动传播更新然后对整个代码库重新做类型检查全部通过后才写入结果。这条输出序列——searching the branch → making sure everything typechecks → saving the results——对应 UCMupdate命令的三阶段实现依赖扫描、全量类型检查、持久化。转录输出显示重构后makeFoo没有被标记为需要更新。原因在于虽然Bar变成了internal.Bar但makeFoo的代码是makeFoo n Bar (n10)。在 Unison 中Bar未限定的短名在Foo类型仍在作用域内时会被解析为Foo.Bar——也就是新的 Smart Constructor而非Foo.internal.Bar。因此makeFoo的语义在重构前后保持一致无需修改这也验证了Smart Constructor 替换数据构造器对既有调用方是透明的。四、重构结果的验证view与find.verbose4.1view Foo查看类型当前定义 view Foo type Foo internal.Bar Natview命令展示的是定义当前在代码库中的形态类型Foo的构造器确实已经是internal.Bar。注意这里显示的是定义本身不会自动展开Foo.Bar这个 Smart Constructor——想看它的实现需要用view Foo.Bar。4.2find.verbose查看完整命名空间与哈希 find.verbose 1. -- #oebc8v8v9lob5bnq7go1pjhfjbtnh8dmfhontua90t3mji0cl91t1dqaece9quofrk1vsbq6g0ukfigoi0vmvc01v8roceppejlgbs8 type Foo 2. -- #gl18p1lnbeari67ohdt9n46usnvsl59a6up1lhd9r808pqb7tt5edsf65o98bqcvb529mfm7q631ciuv2t5nqnde1i7b9t5mlu1drto Foo.Bar : Nat - Foo 3. -- #oebc8v8v9lob5bnq7go1pjhfjbtnh8dmfhontua90t3mji0cl91t1dqaece9quofrk1vsbq6g0ukfigoi0vmvc01v8roceppejlgbs8#0 Foo.internal.Bar : Nat - Foo 4. -- #td96hudai64mf0qgtusc70ehv98krs10jghdipjluc6cp4j8ac65msrt3tji18enpm2tm8d8h2qcf3parke19g7s17ipkd925m3061g makeFoo : Nat - Foofind.verbose会在名字之外额外显示每个定义的完整内容哈希#开头的十六进制串。这个输出是理解本转录结果的关键第 1 项type Foo其哈希#oebc8v8v...同时被第 3 项引用为#oebc8v8v...#0的前缀。#0是 Unison 内容寻址中对类型内部第 0 个构造器的引用方式——即Foo.internal.Bar这个构造器的身份由Foo类型哈希加构造器序号0共同决定。这也解释了为什么internal.Bar与Foo.Bar虽然名字相同前缀但哈希不同它们一个是类型声明的成员随类型哈希定位一个是独立的顶层术语。第 2 项Foo.Bar : Nat - Foo这就是我们新建的 Smart Constructor它的哈希#gl18p1ln...是独立的因为它是一个普通函数定义与类型哈希不同。同一名字Foo.Bar在两代Foo定义Barvsinternal.Bar之间对应着不同的哈希这正是 Unison 内容寻址历史的体现改名术语重命名会改变哈希而旧哈希依旧可以通过update前后的因果链追踪到。第 3 项Foo.internal.Bar : Nat - Foo内部构造器。它的类型签名与Foo.Bar完全相同都是Nat - Foo区别仅在于位置与权限internal.前缀表明它属于类型内部实现普通代码不应当直接调用它。第 4 项makeFoo如前所述它没有被修改——尽管它内部调用Bar但现在Bar解析到的是 Smart ConstructorFoo.Bar。从实现层面看internal前缀 构造器序号定位并非转录特有的魔法而是贯穿 Unison 编译器各层的既定设计在解析与类型检查阶段parser-typecheckerinternal.作为合法的构造器限定名被识别在核心表示层unison-core构造器通过Reference.DerivedId内容哈希 序号被唯一标识在哈希计算层unison-hashing-v2构造器的哈希始终绑定其所属类型的内容哈希。因此#oebc8v8v...#0这种类型哈希 构造器序号的引用形式是 Unison 全局名称解析的基础机制。五、为什么需要 Smart Constructor——设计动机结合 docs/type-declarations.markdown 的讨论本转录演示的重构回答了一个经典模块化问题如何让一个库暴露公共 API却不暴露其内部表示。直接暴露Foo.Bar : Nat - Foo时任何使用方都能绕过makeFoo的n10加工逻辑直接构造出不合规的Foo值。把构造函数降级为internal.Bar再以同名 Smart ConstructorFoo.Bar作为唯一公共入口就实现了三件事保持数据不变量invariant所有对外创建的Foo都必须经过Foo.Bar或在库内部经过internal.Bar库可以确保任何Foo值都满足既定约束自由的内部实现变更未来把internal.Bar换成别的表示比如internal.Baz Text只要保持Foo.Bar : Nat - Foo的签名不变客户端代码零改动命名空间层面的封装internal前缀在名字上就把内部实现与公共接口区分开阅读find.verbose输出即可一眼看出哪些名字是库作者希望使用者触碰的。需要注意的是Unison 中的internal构造器在模式匹配上仍然可用——也就是说case x of internal.Bar n - ...是合法的。它限制的是从库外部直接构造值这条路径而非观察已有值这条路径。这与某些语言中完全私有化构造函数的语义不同属于 Unison 封装但不隐藏模式匹配的设计取向。六、动手复现把本转录变成你自己的练习由于 transcript 文件本身是可执行的你完全可以把它当作一份交互式实验脚本在自己的 UCM 会话中逐步重放启动 UCM例如ucm或通过项目的 unison-cli-main 入口构建的二进制进入一个空的本地代码库依次执行builtins.merge lib.builtin、add把第一段unique type Foo Bar Nat与makeFoo加入代码库用第二段代码internal.BarFoo.BarSmart Constructor替换 scratch 内容执行update用view Foo与find.verbose核对结果确认与本文展示的输出一致。如果你想进一步验证 Smart Constructor 的效果可以在重构后尝试直接构造在 scratch 中输入Bar 42或Foo.Bar 42前者应解析到 Smart ConstructorFoo.Bar透明包装而Foo.internal.Bar 42虽然可以构造出值但会在名字上明确暴露你正在绕过公共接口的事实。你还可以把makeFoo的实现临时改为makeFoo n internal.Bar n再运行update观察 UCM 如何把这种直接使用内部构造器的调用识别为需要更新的依赖。需要说明的是internal.Bar在同一文件内仍然可直接调用本转录的Foo.Bar定义本身就是这么写的。internal 约束在实践中更多是约定俗成的封装纪律 名称空间的自我文档化它的严格强制语义如编译期拒绝外部直接构造属于 Unison 后续版本仍在演进的方向本转录展示的是当前可稳定依赖的行为。七、小结这条转录文档虽然只有短短 80 行却完整覆盖了一次把数据构造函数升级为 Smart Constructor的端到端重构流程用internal.前缀把原构造器从公共接口降级为类型内部实现用一个同名的顶层函数Foo.Bar提供带封装语义的构造入口用add/update完成代码库更新且因为 Unison 对短名Bar的解析自动指向新的Foo.Bar既有调用方makeFoo无需任何修改用view Foo与find.verbose从定义形态与内容哈希两个维度验证重构结果其中类型哈希 构造器序号#0的引用形式揭示了构造器身份与所属类型的内容寻址绑定关系。对于希望用 Unison 构建库或长期维护代码库的开发者这组操作是面向演化设计的最小可复现实战模板公开接口保持不变内部表示自由演化——而这正是 Unison 内容寻址 名称空间设计想要兑现的承诺。赞分享编程语言编译器语言运行时开发工具【免费下载链接】unisonA friendly programming language from the future项目地址https://gitcode.com/gh_mirrors/un/unison点击查看免费下载相关推荐Vote for the {{YEAR}} Steering CommitteeVote for the {{YEAR}} Steering Committee As is now customary, the third calendar编程语言编译器语言运行时开发工具F2 分组柱状图Dodge Column Chart实战从 JSX 配置到负值处理与源码原理F2 分组柱状图Dodge Column Chart实战从 JSX 配置到负值处理与源码原理 分组柱状图Dodge Column Chart是 F2编程语言编译器语言运行时开发工具Helm Chart 元数据类型校验实战以 chart-bad-type 为例理解 chart.metadata.typeHelm Chart 元数据类型校验实战以 chart bad type 为例理解 chart.metadata.type 导读 本篇基于 Helm 官方仓库云原生容器编排CLI运维上一篇终极Blender 3MF插件指南如何完美解决3D打印格式兼容性问题下一篇Blender 3MF插件3D打印工作流的终极免费解决方案创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表