【免费下载链接】roc
A fast, friendly, functional language.
本文以仓库中的 REPL 快照测试 test/snapshots/try_map_err.md 为核心线索,结合内置标准库 src/build/roc/Builtin.roc 中Try类型的真实实现,系统讲解Try.map_err与Try.map_err!的语义、实现原理、类型签名及实战用法。读完本文,你将理解 Roc 中结果类型(Try)错误分支变换的完整行为——包括 effectful 版本map_err!的差异、与map_ok/on_err/catch等近亲函数的取舍,以及快照测试如何为这些语义提供机器可验证的保障。
一、背景:Try 类型在 Roc 中的地位
Roc 使用Try表示一个可能成功也可能失败的计算结果。在 Builtin.roc 中,Try被定义为一个带有Ok、Err两个变体的普通标记联合体,并挂载了一批方法:
Try(ok, err) := [Ok(ok), Err(err)].{ parser_for : _ encoder_for : _ ... }它本质上就是Result/Either的 Roc 版本:Try.Ok(value)表示成功并携带成功值,Try.Err(error)表示失败并携带错误值。围绕这个类型,内置库提供了is_ok/is_err判定、map_ok/map_err/map_both单边或双边变换、on_err恢复、catch折叠、ok_or/err_or兜底等一系列方法。而本文主角map_err,正是"只对错误分支做变换"这一职责的承担者。
二、快照内容逐条解读
test/snapshots/try_map_err.md 是一个type=repl的 REPL 快照,它在 SOURCE 段中依次求值 4 个表达式,并在 OUTPUT 段固化每一次的输出:
» Try.map_err(Try.Err(50), |err_code| err_code + 1) » Try.map_err!(Try.Err(50), |err_code| err_code + 1) » Try.map_err(Try.Ok("hello"), |_| "world") » Try.map_err!(Try.Ok("hello"), |_| "world")对应输出:
Err(51.0) --- Err(51.0) --- Ok("hello") --- Ok("hello")2.1 Err 分支被变换,Ok 分支原样保留
- 表达式 1 与 2:输入是
Try.Err(50),经过|err_code| err_code + 1变换后输出Err(51.0)——错误值 50 被加 1 变成 51,并仍然包裹在Err中。无论是否带!,结果一致。 - 表达式 3 与 4:输入是
Try.Ok("hello"),变换函数|_| "world"根本没有被调用,输出保持Ok("hello")不变。这正是map_err的核心语义:只作用于Err分支,对Ok分支不做任何事(连变换函数都不会执行)。
2.2 输出51.0背后的类型推断细节
值得注意的一个细节是:输入字面量是50,输出却打印为51.0。这是因为在 REPL 环境中,未标注类型的数字字面量默认按浮点类型(F64)推断,50 + 1的结果以51.0的浮点形式显示。这说明map_err本身不关心错误值的具体数值类型——err_code + 1中+运算符决定了错误类型为数值类型,而 REPL 的默认字面量推断决定了它是浮点。若想得到整数结果,需像内置库文档示例那样显式标注,例如Try.Err(50.I64)。
2.3 PROBLEMS 段的含义
快照末尾的PROBLEMS段为NIL。按照 test/snapshots/README.md 的说明,普通快照(含repl类型)的PROBLEMS段保存编译器诊断的规范 S-expression 序列化,NIL表示编译过程没有产生任何报告——即这 4 个表达式在类型检查、求值等各阶段全部顺利通过,没有任何错误或警告。这本身就是对Try.map_err/Try.map_err!类型签名正确性的一次自动化验证。
三、map_err 的源码实现
Try.map_err的完整定义位于 Builtin.roc:
## If the result is `Err`, transforms the value it holds by running a conversion ## function on it. Then returns a new `Err` holding the transformed value. If ## the result is `Ok`, this has no effect. Use [Try.map_ok] to transform an `Ok`. ## ```roc ## expect [].last().map_err(|_| ProvidedListIsEmpty) == Err(ProvidedListIsEmpty) ## ## expect [4].last().map_err(|_| ProvidedListIsEmpty) == Ok(4.0) ## ``` map_err : Try(ok, a), (a -> b) -> Try(ok, b) map_err = |try, transform| match try { Err(a) => Err(transform(a)) Ok(ok) => Ok(ok) }3.1 类型签名解读
map_err : Try(ok, a), (a -> b) -> Try(ok, b)- 第一个参数是
Try(ok, a):ok是成功值类型,a是错误值类型; - 第二个参数是变换函数
(a -> b):只接收错误值、返回新错误值; - 返回值是
Try(ok, b):成功值类型ok保持不变,错误值类型从a变为b。
类型签名已经"剧透"了语义:ok同时出现在输入和输出的同一位置,意味着Ok分支的类型和值都不变;只有错误分支的类型a -> b发生迁移。这解释了快照中Ok("hello")原样返回、而Err(50)变为Err(51)的行为。
3.2 实现逻辑:一个简单的 match
实现部分是一个直白的match:
- 命中
Err(a)时,调用transform(a)得到新错误值,再包回Err; - 命中
Ok(ok)时,不调用 transform,直接把ok原样包回Ok。
这个模式与map_ok(Builtin.roc)镜像对称:map_ok命中Ok(a)时变换、命中Err时原样返回。两者合起来,正是"分别只动一边"的精确语义。
3.3 一个贴近实战的官方示例
Builtin 文档注释中给出了用List.last()配合map_err的例子:
expect [].last().map_err(|_| ProvidedListIsEmpty) == Err(ProvidedListIsEmpty) expect [4].last().map_err(|_| ProvidedListIsEmpty) == Ok(4.0)List.last()本身返回Try(空列表时为Err),通过map_err(|_| ProvidedListIsEmpty)可以把底层的"无元素"错误翻译成业务自定义的错误标签ProvidedListIsEmpty,而成功分支的Ok(4.0)则被原样透传。这是错误规范化(error normalization)的典型用法:在错误跨越模块边界之前,把内部错误转换为调用方理解的领域错误。
四、effectful 版本 map_err!:带副作用的错误变换
与普通版本并排定义的是带!后缀的 effectful 版本(Builtin.roc):
## Like [Try.map_err], but the transform function is effectful. If the argument is ## an `Err`, the effect is run and its return value is wrapped in a new `Err`. If ## the result is `Ok`, the effect is not run and the `Ok` is returned unchanged. ## ```roc ## # Log the failure to the database only when the request errored. ## request.map_err!(|e| SQL.execute!("INSERT INTO errors (message) VALUES (?)", [e.message])) ## ``` map_err! : Try(ok, a), (a => b) => Try(ok, b) map_err! = |try, transform!| match try { Err(a) => Err(transform!(a)) Ok(ok) => Ok(ok) }4.1 签名差异:->变成=>
普通版参数类型是(a -> b)(纯函数),而map_err!是(a => b)(effectful 函数,即可能执行 IO、数据库操作、日志等能力的函数)。在 Roc 的能力系统(capabilities)中,=>表示该函数体内可以使用能力。整个函数类型因此也以=>连接:Try(ok, a), (a => b) => Try(ok, b)。
4.2 语义差异:仅在 Err 分支执行副作用
实现逻辑与普通版完全同构:
Err(a)分支执行transform!(a),效果运行、结果包回Err;Ok(ok)分支不执行效果,原样返回。
文档注释给出的应用场景非常具体:只在请求出错时把失败信息写入数据库——
request.map_err!(|e| SQL.execute!("INSERT INTO errors (message) VALUES (?)", [e.message]))这里SQL.execute!是一个 effectful 操作;用map_err!可以保证:成功时不产生任何数据库写入,失败时把错误信息落库,同时错误值仍以Try的形式继续传递。这种"失败时才记账/审计/上报"的模式,正是map_err!相对于普通map_err的价值所在。
快照中表达式 2 与 4 验证了map_err!与map_err在行为上的一致性:Try.Err(50)变换后是Err(51.0),Try.Ok("hello")原样返回——即使变换函数被声明为 effectful,Ok分支依然不会被触碰。
五、与相邻 API 的取舍:map_ok、map_both、on_err、catch
理解map_err最好的方式,是把它放进Try家族的"分工表"中看。以下函数都定义在 Builtin.roc 的同一区域(L5323-L5519),相互之间有明确的分工:
| 函数 | 签名(简化) | 语义 |
|---|---|---|
map_ok | Try(a, err), (a -> b) -> Try(b, err) | 只变换Ok分支,Err原样返回 |
map_err | Try(ok, a), (a -> b) -> Try(ok, b) | 只变换Err分支,Ok原样返回 |
map_both | Try(a, b), (a -> c), (b -> d) -> Try(c, d) | 两边各自变换,命中哪个分支就运行哪个函数 |
on_err | Try(ok, a), (a -> Try(ok, b)) -> Try(ok, b) | 出错时运行恢复函数,可返回新的Try(恢复本身可失败) |
catch | Try(ok, err), (err -> a), (ok -> a) -> a | 把结果折叠成普通值,两边都必须返回同一类型 |
源码注释里给出了精确的指引:
- 想让
Ok与Err各自映射为不同类型、结果仍是Try→ 用 map_both; - 出错时想"恢复"而非"变换",且恢复过程还可能再次失败 → 用 on_err;
- 想把
Try折叠成一个普通值(如日志字符串、HTTP 状态码)→ 用 catch; - 只想把错误值转换一下、不恢复也不折叠 → 用
map_err。
另外,ok_or 和 err_or 提供了"出错/成功时给默认值"的兜底;但源码注释特别提醒:ok_or这类函数"会掩盖错误的发生、增加调试难度,应谨慎使用,优先用?转发错误或用match显式处理"。map_err由于保留了Try结构(错误依然在Err里、类型变为新错误类型),是比"吃掉错误"更安全的选择。
六、仓库中的真实调用:map_err 如何被编译器自身使用
map_err并非仅供应用层使用,Roc 编译器自身的代码也大量使用它,这为理解它的实战形态提供了绝佳样本。
在 Builtin.roc 中,十六进制数字解析错误被映射为结构化错误标签:
hex_digit_value(byte).map_err(|_| InvalidHex({ index, byte }))同一文件的 L23214 处,JSON 字符串中的非法\uXXXX转义被映射为:
.map_err(|_| InvalidJson("invalid hex digit in \\uXXXX escape"))这两处的共同模式是:底层解析函数返回的底层错误,在向上层传播前被map_err转换为带上下文的领域错误——InvalidHex携带了出错位置{ index, byte },InvalidJson携带了人类可读的消息。这就是map_err在实际编译器中承担的错误上下文增强(error enrichment)职责。
在解释器测试 src/eval/test/eval_issue_tests.zig#L1474 中,也能看到同样的链式用法:
request.headers.find_first(|h| h.name == "x-foo").map_err(|_| NotFound)find_first查找失败时返回Err,通过map_err(|_| NotFound)统一转换为NotFound错误。可见在 Roc 生态中,map_err是"把低层失败翻译成高层语义错误"的标准工具。
七、快照测试如何锁定这些语义
try_map_err.md属于仓库的 REPL 快照体系。根据 test/snapshots/README.md:
Snapshot tests provide comprehensive validation of the compilation pipeline by showing how source code is transformed through each stage: tokenization, parsing, canonicalization, and type checking etc.
快照文件包含META(元信息)、SOURCE(被测代码)、OUTPUT(期望输出)与PROBLEMS(期望诊断)四个区段;type=repl表示该快照通过 REPL 解释器实际求值并捕获输出。try_map_err.md的OUTPUT段精确锁定了map_err/map_err!在四种输入组合下的结果,任何对语义的破坏(比如错误分支不调用变换函数、或错误分支意外调用纯变换以外的效果)都会导致快照失配,从而在 CI 中暴露回归。
对开发者而言,可以按 README 提供的方式手动复现与调试这份快照:
# 更新/生成指定快照 zig build run-snapshot-tool -- test/snapshots/try_map_err.md # 从 PROBLEMS 更新期望值(本快照为 NIL,无需) zig build run-snapshot-tool -- test/snapshots/try_map_err.md --update-expected # 跟踪 REPL 求值过程(仅适用于 type=repl 快照) zig build run-snapshot-tool -- test/snapshots/try_map_err.md --trace-eval注意 README 的提示:--trace-eval只适用于 REPL 快照且一次只能处理单个文件;trace 输出在 debug 构建中默认开启,release 构建需以-Dtrace-eval=true开启。
八、实践要点小结
map_err只动错误分支:Err(a)会被变换为Err(f(a)),Ok(ok)原样返回且变换函数不会执行——快照中 4 个表达式是这一语义的最直观验证。map_err!是带能力的版本:变换函数以=>声明,可在错误分支执行日志、写库等效果;Ok分支依然不触发任何效果。适合"失败时才审计/上报"的场景。- 错误类型可以安全更换:签名
Try(ok, a), (a -> b) -> Try(ok, b)表明错误类型可在变换中改变,常用于把底层错误转换为领域错误(参考编译器内部的InvalidHex/InvalidJson/NotFound用法)。 - 与恢复、折叠区分开:要"恢复失败"用
on_err;要把结果折叠成普通值用catch;要两边都变换用map_both;要兜底默认值用ok_or/err_or(但需警惕其掩盖错误的副作用)。 - REPL 数字字面量默认按浮点推断:
Err(50)经+1后显示为Err(51.0),需要整数语义时应显式标注类型如50.I64。
围绕 try_map_err.md 这份快照,从 Builtin.roc 的类型定义到解释器测试 eval_issue_tests.zig,整个仓库形成了一条完整的证据链:map_err语义既有文档注释的权威说明,又有 REPL 快照的机器验证,还有编译器内部的实际调用样本。掌握了它,你就能在 Roc 中写出既清晰又安全的错误处理链。
【免费下载链接】roc
A fast, friendly, functional language.
相关推荐
从源码到运行:AndroidScreencast编译与部署完全指南
从源码到运行:AndroidScreencast编译与部署完全指南 AndroidScreencast是一款强大的开源工具,让你能够在电脑上查看和控制Andro
Roc 语言 List.chunks_of 列表分块全解析:从 REPL 快照测试到 Builtin 源码实现
Roc 语言 List.chunks_of 列表分块全解析:从 REPL 快照测试到 Builtin 源码实现 导读 List.chunks_of 是 Roc
Roc 语言 List.map2 深度实战:从 REPL 快照到 Builtin 源码,掌握成对映射的完整语义
Roc 语言 List.map2 深度实战:从 REPL 快照到 Builtin 源码,掌握成对映射的完整语义 本篇技术指南以 Roc 语言测试快照 test/
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考