ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Roc 语言自定义相等性实战:为带 payload 的 nominal 类型实现 is_eq 方法并驱动 == 与 != 运算符

Roc 语言自定义相等性实战:为带 payload 的 nominal 类型实现 is_eq 方法并驱动 == 与 != 运算符 Roc 语言自定义相等性实战为带 payload 的 nominal 类型实现 is_eq 方法并驱动 与 ! 运算符【免费下载链接】rocA fast, friendly, functional language.项目地址: https://gitcode.com/GitHub_Trending/ro/roc导读本文以 Roc 编译器仓库中的 eval 快照测试 custom_wrapper_equality.md 为核心系统讲解如何在 Roc 中为“携带 payload载荷数据的 nominal 标签联合类型”定义自定义相等性通过类型声明中的关联方法is_eq让与!运算符不再依赖默认的结构化比较而是执行开发者定义的语义。读完本文你将掌握is_eq的签名规范、基于模式匹配的实现模式、/!在编译流水线解析 → 规范化 → 类型检查 → 求值中的完整传递路径以及如何用仓库自带的快照工具复现并验证这一行为。一、背景Roc 中相等性的两条路径Roc 是一门快速、友好、函数式的语言见仓库根目录 README.md。与许多语言不同Roc 的/!运算符并不总是执行“逐字节结构比较”编译器会在类型检查阶段为相等性选择两条语义路径之一这一点可以从规范化的 AST 节点定义中得到印证结构化相等structural equality对应src/canonicalize/Expression.zig中的e_structural_eq节点第 422–431 行。其注释明确说明This is not method dispatch. It represents the semantic case where equality is satisfied structurally rather than via a user-definedis_eqmethod.—— 即当类型没有用户自定义相等性时相等性按结构逐字段、逐标签满足方法派发相等method equality对应同一文件中的e_method_eq节点第 441–446 行包含lhs、rhs与negated三个字段。当类型声明了is_eq方法时会被编译为对该方法的调用!则通过对同一方法取反negated实现。也就是说声明了is_eq的类型其与!的语义完全由开发者掌控。本文要讲解的快照文件正是这一机制的端到端证据从源码、格式化输出到规范 IR 中的e-method-eq节点再到最终类型推断结果一应俱全。二、快照文件解剖一个 eval 快照包含什么关联文档位于test/snapshots/eval/custom_wrapper_equality.md属于 Roc 编译器仓库的eval 快照测试体系。根据 test/snapshots/README.md快照测试通过捕获编译各阶段词法分析、解析、规范化、类型检查等对指定 Roc 代码示例的输出来验证编译器行为并防止回归。每个普通快照文件由若干以#开头的段落组成段落内容本文件中的值METAINI 格式的元信息description与typedescriptionTest and ! operators with payload-carrying nominal types that have is_eq methodstypesnippetSOURCE被测的 Roc 源码定义UserId及其is_eq含两条expectEXPECTED期望的求值结果NIL无输出即所有expect通过PROBLEMS编译器诊断的规范 S-expressionNIL编译无任何诊断TOKENS词法分析产生的 token 流完整的 Zig 风格 token 序列PARSE解析产生的语法树Clojure 风格 S-expression完整 ASTFORMATTED格式化器对源码重新排版后的结果与 SOURCE 语义等价、制表符缩进CANONICALIZE规范化 IRcan-irClojure 风格含e-method-eq节点TYPES类型推断结果全部推断为UserId/UserId, UserId - Bool这里的typesnippet表示它是普通快照而非typereporting诊断渲染快照EXPECTED NIL与PROBLEMS NIL意味着这段代码在求值阶段零输出通过全部断言且编译期间未产生任何诊断报告关于NIL语义的说明同样见 test/snapshots/README.md。三、核心示例逐行解读为带 payload 的包装类型定义相等性下面是被测源码SOURCE段经格式化器重排后为FORMATTED段两者语义完全一致。它定义了一个名为UserId的 nominal 类型内部是一个携带I64payload 的标签Id并为其声明了自定义相等性方法is_eq# Define a UserId type that wraps an I64 with custom equality UserId : [Id(I64)].{ is_eq : UserId, UserId - Bool is_eq |a, b| match a { Id(id_a) match b { Id(id_b) id_a id_b } } } user1 : UserId user1 UserId.Id(100) user2 : UserId user2 UserId.Id(100) user3 : UserId user3 UserId.Id(200) # Test equality - same IDs should be equal expect user1 user2 # Test inequality - different IDs should not be equal expect user1 ! user33.1 类型声明的结构标签联合 关联方法类型声明UserId : [Id(I64)].{ ... }由两部分组成标签联合主体[Id(I64)]声明UserId有一个标签Id携带一个I64类型的载荷。在 PARSE 树中这一部分对应ty-tag-union→tags→ty-apply (ty (name Id)) (ty (name I64))即“把I64应用到标签Id上”花括号关联块.{ ... }为类型关联一个方法is_eq。PARSE 树中对应associated→s-type-anno类型注解与s-decl实现。这种写法正是“payload-carrying nominal type”的含义UserId不是一个裸数字而是一个包裹着I64的名义类型nominal type即使两个UserId内部都是100它们也是UserId的实例而不是I64。快照名称中的custom wrapper equality即指这种“包装类型自定义相等性”。3.2 is_eq 方法的签名与实现模式is_eq的类型注解是标准的 Roc 函数签名is_eq : UserId, UserId - Bool即“接收两个UserId返回Bool”。实现使用嵌套模式匹配对两个参数同时解构is_eq |a, b| match a { Id(id_a) match b { Id(id_b) id_a id_b } }解读外层match a解构第一个参数把 payload 绑定到id_a内层match b解构第二个参数把 payload 绑定到id_b内层分支最终比较id_a id_b此时id_a、id_b都是I64走内置数字相等比较见下文第五节。由于UserId只有一个标签Id两层匹配都是“穷尽”的不需要兜底分支。PARSE 树完整呈现了这一嵌套结构e-match中套e-match最内层是e-binop (op ) (e-ident (raw id_a)) (e-ident (raw id_b))。3.3 构造值与断言三个值通过命名空间式构造语法UserId.Id(100)创建user1与user2的 payload 相同100user3的 payload 不同200。最后两条断言验证语义expect user1 user2 # 期望相等 expect user1 ! user3 # 期望不等因为UserId声明了is_eq这两条expect并不做结构比较而是派发到is_eq方法user1 user2等价于调用UserId.is_eq(user1, user2)得到Trueuser1 ! user3则等价于对UserId.is_eq(user1, user3)的结果取反。EXPECTED NIL表明两条断言全部通过、无任何输出这就是“测试通过”的判定方式。四、从源码到规范 IRe-method-eq 的诞生快照的CANONICALIZE段展示了一段重要的中间表示——它证明了确实被转换成了对is_eq的方法派发而非结构化比较。该段的关键部分如下有删节完整内容见 custom_wrapper_equality.md(d-let (p-assign (ident custom_wrapper_equality.UserId.is_eq)) (e-lambda (args (p-assign (ident a)) (p-assign (ident b))) (e-match ... (value (e-method-eq (negated false) (lhs (e-lookup-local (p-assign (ident id_a)))) (rhs (e-lookup-local (p-assign (ident id_b)))))))) (annotation (ty-fn (effectful false) (ty-lookup (name UserId) (local)) (ty-lookup (name UserId) (local)) (ty-lookup (name Bool) (builtin))))) ... (s-expect (e-method-eq (negated false) (lhs (e-lookup-local (p-assign (ident user1)))) (rhs (e-lookup-local (p-assign (ident user2)))))) (s-expect (e-method-eq (negated true) (lhs (e-lookup-local (p-assign (ident user1)))) (rhs (e-lookup-local (p-assign (ident user3))))))可以观察到三点关键事实方法定义被整体规范化is_eq被收录为custom_wrapper_equality.UserId.is_eq模块名 类型名 方法名的完整限定名其 lambda 体的最内层 payload 比较被记为e-method-eq因为id_a/id_b的类型I64本身也走内置相等分发与!的对称实现expect user1 user2规范化为e-method-eq (negated false)而expect user1 ! user3规范化为e-method-eq (negated true)。!不是另一个运算符而是对同一相等性方法结果取反——这正是 src/canonicalize/Expression.zig 中e_method_eq结构体negated字段的运行时体现类型注解被保留规范 IR 中明确记录了is_eq的类型为(ty-fn (effectful false) UserId UserId Bool)即纯函数、无副作用接收两个本地UserId返回内建Bool。相应地TYPES段给出了类型推断的最终结论is_eq推断为UserId, UserId - Booluser1/user2/user3均推断为UserIdtype_decls中注册了 nominal 类型UserId。这意味着整个快照在类型层面完全闭合、可独立通过类型检查。五、求值端解释器如何执行 is_eq快照属于test/snapshots/eval/其求值由 Roc 解释器完成。根据 src/eval/README.md解释器的流水线为checked modules → post-check IRs → LIR → TRMC/TCE → ARC → Interpret在求值端最内层的id_a id_b两个I64会被编译为低层操作。仓库源码 src/eval/interpreter.zig 中可以看到相等性低层操作的实际分派实现第 7414 行附近.num_is_eq self.numCmpOp(args[0], args[1], arg_layout, .eq)—— 数值相等比较通过numCmpOp统一执行第 6301、6307 行附近.str_is_eq与.str_is_eq_static_small—— 字符串相等有专门的含小字符串优化路径。这解释了为什么is_eq的最内层可以直接写id_a id_b一旦 payload 被解构还原为内建类型如I64、Str就会落到编译器内置的、经过优化乃至 SIMD/静态分配优化的比较原语上而不是递归地再次寻找用户自定义方法。此外值得注意的边界事实有明确测试证据在 src/eval/test/eval_tests.zig 第 1334–1336 行.{ .name problem: F32.is_eq is intentionally unavailable, .source F32.is_eq(1.0.F32, 1.0.F32), .expected .{ .problem {} } }, .{ .name problem: F64.is_eq is intentionally unavailable, .source F64.is_eq(1.0.F64, 1.0.F64), .expected .{ .problem {} } }, .{ .name inspect: F32 opts in to receiver is_eq dispatch, .source 1.0.F32.is_eq(1.0.F32), .expected .{ .inspect_str True } },即对浮点类型F32/F64显式调用is_eq方法是有意禁止的会触发编译问题但通过接收者语法做相等性派发1.0.F32.is_eq(...)是允许的。这说明 Roc 对“哪些类型允许以方法形式调用is_eq”有精细控制自定义相等性并非无边界地开放。同一测试文件中还有一组数据驱动用例第 2059 行起验证了自定义is_eq与字面量模式匹配的交互例如from_numeral自定义字面量通过is_eq派发Code、MyNum、Tally、Scale等自定义类型均定义了is_eq : T, T - Bool并配合match (a, b)实现。这表明is_eq不仅驱动/!还参与模式匹配的字面量比较是 Roc 相等性机制更广泛的一部分。六、姊妹快照对比无 payload 与有 payload 的两种写法仓库中还有一个高度相关的姊妹快照 custom_type_equality.md它测试的是无 payload 的标签联合纯枚举自定义相等性Color : [Red, Green, Blue].{ is_eq : Color, Color - Bool is_eq |a, b| match a { Red match b { Red True Green False Blue False } Green match b { Red False Green True Blue False } Blue match b { Red False Green False Blue True } } }两者对比可以清晰地归纳出有 payload 与无 payload 的两种实现模式维度无 payloadcustom_type_equality.md有 payloadcustom_wrapper_equality.md本文主题类型主体[Red, Green, Blue]纯标签[Id(I64)]标签携带数据构造语法Color.Red无参构造UserId.Id(100)带实参构造解构方式match a { Red ... }直接分支match a { Id(id_a) ... }同时解出 payload相等判定逐标签组合手动映射到True/False解构后比较 payloadid_a id_b分支数量3×3 全组合1×1随标签数线性增长这恰好说明is_eq的设计意图把“如何定义相等”完全交给类型作者。无 payload 的枚举需要你显式枚举每种组合哪怕冗长而有 payload 的包装类型则通常解构出载荷后委托给内建比较代码更简洁。七、在本地复现与验证7.1 作为测试用例运行该快照属于编译器自身的测试资产可通过仓库文档记录的命令运行运行全部 eval 测试解释器 dev wasm 后端zig build run-test-eval如需覆盖 LLVM 后端追加-- --llvm命令来源src/eval/README.md其中每个数据驱动的TestCase会在所有启用后端上执行并要求Str.inspect输出字节级一致见 src/eval/README.md 的 Tests 一节。7.2 使用快照工具生成或更新快照工具src/snapshot_tool/README.md是编译器的“黄金文件”测试设施它运行编译器各阶段并与其基线输出比对任何差异都会导致测试失败从而捕获回归。针对单个快照文件test/snapshots/README.md 给出以下用法# 生成/更新全部快照 zig build run-snapshot-tool # 只处理指定快照文件 zig build run-snapshot-tool -- test/snapshots/eval/custom_wrapper_equality.md # 当编译器行为有意变化时依据 PROBLEMS 更新期望 zig build run-snapshot-tool -- test/snapshots/eval/custom_wrapper_equality.md --update-expected7.3 动手练习建议可以在自己的 Roc 源文件中验证本文示例并尝试以下变体以加深理解改写为元组解构把嵌套match a { ... match b { ... } }改写为match (a, b) { (Id(x), Id(y)) x y }src/eval/test/eval_tests.zig中多个用例采用此写法语义不变更换 payload 类型把I64换成Str比较逻辑自动走str_is_eq路径见 src/eval/interpreter.zig 第 6301 行附近体验低层原语分派移除is_eq再比较若不声明is_eq会退回e_structural_eq结构比较路径见 src/canonicalize/Expression.zig观察快照CANONICALIZE段中节点类型的变化。八、总结与边界通过 custom_wrapper_equality.md 这一个快照我们可以得到关于 Roc 自定义相等性的完整结论声明位置在 nominal 类型声明的.{ ... }关联块内签名契约is_eq : T, T - Bool纯函数、无副作用规范 IR 中effectful false可证实现自由度完全由类型作者定义常见模式是解构 payload 后委托内建运算符映射派发到is_eq本体!是对其结果取反e-method-eq的negated标志测试判定EXPECTED NIL表示全部expect通过PROBLEMS NIL表示无编译诊断。需要注意的边界均有仓库证据不应过度推断F32/F64的is_eq方法形式调用被有意禁止src/eval/test/eval_tests.zig解释器存在 1024 层调用深度上限但不受尾递归影响src/eval/README.md快照测试的NIL语义与普通诊断渲染快照不同test/snapshots/README.md。本文所有结论均基于仓库文档与源码未做任何外部性能或能力断言。【免费下载链接】rocA fast, friendly, functional language.项目地址: https://gitcode.com/GitHub_Trending/ro/roc创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表