【免费下载链接】roc
A fast, friendly, functional language.
导读
本文以 list_count_if_empty.md 这份 REPL 快照测试文件为核心,剖析 Roc 语言标准库中List.count_if函数在空列表输入下的行为语义:对空列表调用count_if会返回0,且编译与求值全程无任何诊断报告。文章将结合src/build/roc/Builtin.roc中的count_if实现与fold底层原理,说明该结果为何成立,并横向对比同一主题下的多份快照测试,同时讲解 REPL 快照的文件格式与验证方式,帮助读者理解 Roc 语言如何用快照测试锁定列表函数的边界行为。
一、快照测试文件到底在验证什么
先看本仓库中这份关联文档的完整内容(list_count_if_empty.md):
# META ~~~ini description=List.count_if on empty list returns 0 type=repl ~~~ # SOURCE ~~~roc » List.count_if([], |x| x > 2) ~~~ # OUTPUT 0 # PROBLEMS NIL这份文件属于 test/snapshots 目录下的 REPL 快照测试,由四个标准区块组成:
| 区块 | 内容 | 含义 |
|---|---|---|
META | description+type=repl | 声明该快照为 REPL 求值测试,并给出一句话行为描述 |
SOURCE | 以»开头的 REPL 输入 | 用户在 REPL 提示符后输入的表达式:List.count_if([], \|x\| x > 2) |
OUTPUT | 0 | REPL 求值后应输出的结果 |
PROBLEMS | NIL | 编译/类型检查阶段应产生的诊断报告集合,NIL表示没有任何报错或警告 |
这条测试验证的核心语义是:对空列表执行List.count_if,无论谓词如何,结果都是0。PROBLEMS为NIL则进一步说明:对空列表调用count_if完全合法,不产生任何类型错误或诊断信息。
关于快照测试的整体设计思路,可参见 test/snapshots/README.md:快照测试通过捕获源码在各编译阶段(分词、解析、规范化、类型检查等)的输出,来验证编译器行为并检测回归。普通快照(type=file、snippet、expr、repl等)的PROBLEMS区块保存的是reporting.Report的规范 S 表达式序列化,不含任何渲染器相关的细节;NIL即表示编译没有产生任何报告。
二、List.count_if 的源码实现:fold 驱动的计数器
count_if是 Roc 语言内建标准库函数,定义在 src/build/roc/Builtin.roc 中,其文档注释与完整实现如下:
## Run the given function on each item of a list, and return the ## number of items for which the function returned `Bool.True`. ## ```roc ## expect [1, -2, -3].count_if(I64.is_negative) == 2 ## expect [1, 2, 3].count_if(|num| num > 1) == 2 ## ``` count_if : List(a), (a -> Bool) -> U64 count_if = |list, predicate| List.fold( list, 0, |acc, item| if predicate(item) { acc + 1 } else { acc }, )从源码结构可以提炼出count_if的三个关键事实:
- 类型签名:
List(a), (a -> Bool) -> U64。它接受一个任意元素类型的列表和一个返回Bool的谓词函数,最终返回U64(无符号 64 位整数)。这也解释了为什么结果输出是0而非0u64或类似带类型标记的形式——返回值类型就是U64。 - 实现策略:
count_if完全基于List.fold构建。它以0作为累加器初值,对列表逐项应用step函数:若predicate(item)为真则累加器加 1,否则保持原值。因此返回值本质上是"谓词返回True的元素个数"。 - 与
keep_if的关联:count_if可以理解为List.keep_if的"计数版"——只统计而不收集元素。若需要保留符合条件的元素本身,应使用keep_if;若只关心数量,count_if是更直接的选择。
空列表结果的底层依据:fold 的初值即结果
为什么空列表必然返回0?关键在于List.fold本身的实现。在 src/build/roc/Builtin.roc 中:
fold : List(item), state, (state, item -> state) -> state fold = |list, init, step| { var $state = init for item in list { $state = step($state, item) } $state }fold用for item in list遍历列表并反复应用step。当列表为空时,循环体一次都不会执行,$state始终等于init,最终直接返回初值0。这从源码层面证实了快照断言List.count_if([], |x| x > 2) == 0的必然性:它不依赖谓词|x| x > 2的具体逻辑,纯粹是 fold 折叠语义在空容器上的自然结果——这也是函数式语言中"空容器上聚合操作返回幺元(identity)"这一普遍规律的体现。
三、同一语义簇下的横向对比:四份快照互为印证
仓库中与count_if相关的快照共有四份,分别锁定不同输入形态下的行为,本文档正是其中针对空列表的边界用例。将它们放在一起对比,可以完整还原count_if的语义全貌:
| 快照文件 | SOURCE 输入 | OUTPUT | 覆盖场景 |
|---|---|---|---|
| list_count_if.md | List.count_if([1, 2, 3, 4, 5], \|x\| x > 2) | 3 | 常规场景:部分元素满足谓词 |
| list_count_if_all_match.md | List.count_if([1, 2, 3, 4, 5], \|x\| x > 0) | 5 | 全部匹配:结果等于列表长度 |
| list_count_if_none_match.md | List.count_if([1, 2, 3], \|x\| x > 10) | 0 | 无一匹配:结果归零 |
| list_count_if_empty.md | List.count_if([], \|x\| x > 2) | 0 | 空列表:循环不执行,返回初值 |
对照Builtin.roc中文档注释给出的expect示例([1, -2, -3].count_if(I64.is_negative) == 2),可以归纳出count_if的三个可验证结论:
- 计数上界 = 列表长度:当所有元素都满足谓词时(
all_match),返回值等于列表长度5; - 计数下界 = 0:当没有元素满足谓词(
none_match)或列表本身为空(empty)时,返回值都是0; - 空列表是平凡情形:对空列表,谓词永远不会被调用,结果不依赖谓词内容,恒为初值
0。
值得留意的是,none_match与empty两个场景输出同为0,但成因不同:前者是"遍历了所有元素但无一命中",后者是"根本没有元素可遍历"。这两份快照合在一起,恰好把count_if的两个边界收敛点都钉死了,防止未来编译器或标准库重构时在这两条路径上引入回归。
四、REPL 快照的运行与调试方式
本仓库的快照测试通过 Zig 构建系统驱动,具体命令记录在 test/snapshots/README.md 中:
# 生成/更新全部快照 zig build run-snapshot-tool # 只更新指定的某个快照(例如本文分析的这份) zig build run-snapshot-tool -- test/snapshots/repl/list_count_if_empty.md # 用编译器的实际报告更新 PROBLEMS 区块 zig build run-snapshot-tool -- <file_path> --update-expected # 调试 REPL 求值过程(输出解释器逐步追踪信息) zig build run-snapshot-tool -- <repl_snapshot.md> --trace-eval针对 REPL 类型的快照,--trace-eval是一个实用的调试开关,它开启解释器逐步追踪,可以观察表达式List.count_if([], |x| x > 2)在求值过程中的每一步内部状态:
- 该开关只对
type=repl的快照生效; - 一次只能搭配单个快照文件使用;
- Debug 构建默认开启追踪;Release 构建需要追加
-Dtrace-eval=true编译选项才能启用。
当这份快照的OUTPUT或PROBLEMS与真实求值结果不一致时,快照工具会报告差异,从而暴露编译器或标准库的回归。这正是快照测试的核心价值:把"count_if作用于空列表返回 0 且无诊断"这类语义约定固化成可自动比对的标准答案。
五、实践小结:从快照反推语言语义
回到本文核心的这份文档,可以用一句话概括它的技术结论:在 Roc 语言中,List.count_if对空列表的求值结果为0,且该调用完全合法、不产生任何编译诊断。这个结论的完整证据链是:
- 快照断言层:
list_count_if_empty.md的OUTPUT为0、PROBLEMS为NIL; - 标准库实现层:
count_if基于List.fold(Builtin.roc),初值为0; - 折叠语义层:
fold对空列表不执行循环体,直接返回初值(Builtin.roc); - 行为矩阵层:与
all_match(返回 5)、none_match(返回 0)两份快照共同构成完整的边界覆盖。
对读者而言,这份快照不仅是一个测试用例,更是一份"可执行的语言语义说明书":当你需要确认 Roc 标准库某个函数在边界输入下的行为时,test/snapshots/repl 目录下的同名快照就是最直接、最权威的答案来源。同类列表函数(如list_keep_if_empty、list_fold_until_empty、list_take_first_empty、list_drop_first_empty等)都遵循同一套"常规场景 + 全匹配 + 零匹配 + 空列表"的快照组织模式,读者可自行对照阅读,借此系统掌握 Roc 列表 API 的边界语义。
【免费下载链接】roc
A fast, friendly, functional language.
相关推荐
Roc 语言 REPL 快照测试精读:List.take_first 取 0 个元素的空列表边界语义
Roc 语言 REPL 快照测试精读:List.take_first 取 0 个元素的空列表边界语义 本篇以 Roc 语言仓库中的 REPL 快照测试 test
Roc 语言 List.drop_last 越界删除语义解析:REPL 快照测试如何锁定"删除数超过列表长度时返回空列表"的行为
Roc 语言 List.drop_last 越界删除语义解析:REPL 快照测试如何锁定"删除数超过列表长度时返回空列表"的行为 本篇技术指南以 Roc 编译器
Roc 语言 `abs_diff` 方法全类型解析:语义、边界与 REPL 快照验证
Roc 语言 abs_diff 方法全类型解析:语义、边界与 REPL 快照验证 abs_diff 是 Roc 标准库中一个覆盖全部数值类型(U8~U128、I
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考