news 2026/9/20 23:03:50

Roc 语言 Try.map_err 与 Try.map_err! 完整解析:从 REPL 快照到 Builtin 源码实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Roc 语言 Try.map_err 与 Try.map_err! 完整解析:从 REPL 快照到 Builtin 源码实现

【免费下载链接】roc

A fast, friendly, functional language.

项目地址:https://gitcode.com/GitHub_Trending/ro/roc
点击查看免费下载

本文以仓库中的 REPL 快照测试 test/snapshots/try_map_err.md 为核心线索,结合内置标准库 src/build/roc/Builtin.roc 中Try类型的真实实现,系统讲解Try.map_errTry.map_err!的语义、实现原理、类型签名及实战用法。读完本文,你将理解 Roc 中结果类型(Try)错误分支变换的完整行为——包括 effectful 版本map_err!的差异、与map_ok/on_err/catch等近亲函数的取舍,以及快照测试如何为这些语义提供机器可验证的保障。

一、背景:Try 类型在 Roc 中的地位

Roc 使用Try表示一个可能成功也可能失败的计算结果。在 Builtin.roc 中,Try被定义为一个带有OkErr两个变体的普通标记联合体,并挂载了一批方法:

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_okTry(a, err), (a -> b) -> Try(b, err)只变换Ok分支,Err原样返回
map_errTry(ok, a), (a -> b) -> Try(ok, b)只变换Err分支,Ok原样返回
map_bothTry(a, b), (a -> c), (b -> d) -> Try(c, d)两边各自变换,命中哪个分支就运行哪个函数
on_errTry(ok, a), (a -> Try(ok, b)) -> Try(ok, b)出错时运行恢复函数,可返回新的Try(恢复本身可失败)
catchTry(ok, err), (err -> a), (ok -> a) -> a把结果折叠成普通值,两边都必须返回同一类型

源码注释里给出了精确的指引:

  • 想让OkErr各自映射为不同类型、结果仍是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.mdOUTPUT段精确锁定了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开启。

八、实践要点小结

  1. map_err只动错误分支Err(a)会被变换为Err(f(a))Ok(ok)原样返回且变换函数不会执行——快照中 4 个表达式是这一语义的最直观验证。
  2. map_err!是带能力的版本:变换函数以=>声明,可在错误分支执行日志、写库等效果;Ok分支依然不触发任何效果。适合"失败时才审计/上报"的场景。
  3. 错误类型可以安全更换:签名Try(ok, a), (a -> b) -> Try(ok, b)表明错误类型可在变换中改变,常用于把底层错误转换为领域错误(参考编译器内部的InvalidHex/InvalidJson/NotFound用法)。
  4. 与恢复、折叠区分开:要"恢复失败"用on_err;要把结果折叠成普通值用catch;要两边都变换用map_both;要兜底默认值用ok_or/err_or(但需警惕其掩盖错误的副作用)。
  5. 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.

项目地址:https://gitcode.com/GitHub_Trending/ro/roc
点击查看免费下载
上一篇:mini-vue组件错误边界:捕获子组件异常
下一篇:amis 辅助类 Visibility 完全指南:用 visible / invisible 控制元素显示与隐藏

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/20 23:01:30

Python+飞书机器人:自动化消息推送实战指南

“发个通知而已,能有多麻烦?”说实话,我以前也这么想。直到有一天,我负责的一个数据同步任务在凌晨2点挂了,群里安安静静,第二天早上才被人发现。那一刻我才意识到:手动发通知这事,看…

作者头像 李华
网站建设 2026/9/20 23:01:14

淘宝评论情感分析:Python机器学习从分词到LIME

简介:面向毕业设计、课程大作业及机器学习入门实践,这是一份基于机器学习的电商淘宝商品评论情感分析项目源码与数据包。项目从Selenium模拟登录爬取淘宝评论入手,依次完成数据清理、jieba精确模式分词、词语索引与词向量构造,并对…

作者头像 李华
网站建设 2026/9/20 23:01:01

Hermes Agent 实战笔记:跟一条消息走完工具调用循环的每一轮

Hermes Agent 实战笔记:跟一条消息走完工具调用循环的每一轮 【免费下载链接】hermes-agent The agent that grows with you 项目地址: https://gitcode.com/GitHub_Trending/he/hermes-agent Hermes Agent 是一个可接多种模型后端的 AI 代理框架&#xff08…

作者头像 李华