Roc 语言 List.starts_with 前缀判断完全指南:从 REPL 快照测试到内置实现
2026/9/19 11:02:54 网站建设 项目流程

Roc 语言 List.starts_with 前缀判断完全指南:从 REPL 快照测试到内置实现

【免费下载链接】rocA fast, friendly, functional language.项目地址: https://gitcode.com/GitHub_Trending/ro/roc

导读

本文以 Roc 编译器仓库中的 REPL 快照测试 list_starts_with.md 为切入点,系统讲解List.starts_with的语义、边界情况与底层实现。通过阅读本文,你将掌握列表前缀判断的完整行为规则(含空前缀、空列表、类型约束等边界),理解 Roc 标准库中该函数的源码实现与其背后List.take_firstsublist的调用链,并学会读懂和运行test/snapshots/repl/目录下的快照测试来验证行为。

一、快照文件是什么:REPL 测试的标准载体

在 Roc 编译器仓库中,test/snapshots/repl/目录存放的是针对 REPL(交互式命令行)行为的快照测试。每个文件是一个标准化的四段式文档,list_starts_with.md是其中的典型代表:

# META ~~~ini description=List.starts_with returns True when the first list begins with the second type=repl ~~~ # SOURCE ~~~roc » List.starts_with([1, 2, 3, 4], [1, 2]) ~~~ # OUTPUT True # PROBLEMS NIL

四个段落各有明确职责:

段落作用本文件取值
META声明测试的描述与类型description描述被测行为;type=repl表明这是 REPL 快照,区别于编译型快照
SOURCE»提示符后输入 REPL 表达式List.starts_with([1, 2, 3, 4], [1, 2])
OUTPUT期望的 REPL 求值结果True
PROBLEMS期望的编译/检查诊断,NIL表示无错误NIL

这段测试的意图非常清晰:当第一个列表以第二个列表作为前缀时,List.starts_with返回True[1, 2]恰好是[1, 2, 3, 4]的起始片段,因此输出为True,且没有任何类型或运行时诊断(PROBLEMS = NIL)。

二、边界情况:空前缀与不匹配前缀

单个快照只覆盖了一条路径,但仓库在test/snapshots/repl/下配套了覆盖边界情况的兄弟快照,组合起来完整刻画了List.starts_with的行为。这是理解该函数语义的关键。

2.1 空前缀恒为真

list_starts_with_empty_prefix.md 验证了空前缀的语义:

# META ~~~ini description=List.starts_with with an empty prefix is always True (every list starts with the empty list) type=repl ~~~ # SOURCE ~~~roc » List.starts_with([1, 2, 3], []) ~~~ # OUTPUT True # PROBLEMS NIL

结论:任何列表都以空列表为前缀List.starts_with([1, 2, 3], [])返回True

2.2 前缀不匹配返回 False

list_starts_with_no_match.md 验证了不匹配场景:

# SOURCE ~~~roc » List.starts_with([1, 2, 3], [9, 9]) ~~~ # OUTPUT False # PROBLEMS NIL

注意这里前缀[9, 9]与列表首元素1不同,即便两者长度相同(均为 2),比较结果仍为False。前缀判断是逐元素的,不只看长度。

2.3 字符串版本对照

str_starts_with.md 展示了同一语义在字符串类型上的体现,且演示了多表达式 REPL 输出:

# SOURCE ~~~roc » Str.starts_with("hello world", "hello") » Str.starts_with("hello world", "world") » Str.starts_with("", "") » Str.starts_with("hello", "") » Str.starts_with("hi", "hello") ~~~ # OUTPUT True --- False --- True --- True --- False # PROBLEMS NIL

多行 REPL 输出以---分隔每个表达式的结果,规律与列表版本完全一致:空前缀恒真、空串互比恒真、前缀不匹配为假。

三、源码实现:Builtin.roc 中的定义

List.starts_with的实现位于 src/build/roc/Builtin.roc,它是编译进 Roc 程序的 builtin 定义:

## Returns `Bool.True` if the first list starts with the second list. ## ## If the second list is empty, this always returns `Bool.True`; every list ## is considered to "start with" an empty list. ## ## If the first list is empty, this only returns `Bool.True` if the second list is empty. starts_with : List(a), List(a) -> Bool where [a.is_eq : a, a -> Bool] starts_with = |list, prefix| prefix == List.take_first(list, List.len(prefix))

实现只有一行,但信息量很大:

  • List.len(prefix)决定取多少元素:先计算前缀长度n,再用List.take_first(list, n)截取主列表的前n个元素,最后与prefix做相等比较。
  • 空前缀恒真的来源:当prefix为空时,List.len(prefix) == 0take_first(list, 0)返回空列表,[] == []恒成立,因此返回True——这解释了第二节的边界行为。
  • 空主列表的规则:若list为空而prefix非空,take_first返回空列表,与prefix不相等,返回False;只有当两边都为空时才为True。源码注释精确地描述了这一规则。

紧邻其后的ends_with是它的镜像实现,思路完全对称:

ends_with : List(a), List(a) -> Bool where [a.is_eq : a, a -> Bool] ends_with = |list, suffix| suffix == List.take_last(list, List.len(suffix))

四、类型约束:为什么需要 is_eq

注意starts_with的类型签名末尾的约束:

where [a.is_eq : a, a -> Bool]

这是 Roc 基于能力的约束系统(capability/where 子句)。List(a)的元素类型a必须实现is_eq能力(即元素之间可判等),函数才能编译。这意味着:

  • 如果元素类型具备相等性(例如函数类型、无Eq实现的抽象类型),调用List.starts_with会在编译期报错,而不是运行期崩溃;
  • [a.is_eq]属于静态约束,编译器在类型检查阶段即可解析(相关约束解析逻辑可在 src/canonicalize/BuiltinLowLevel.zig 与 src/postcheck/boxy/lower.zig 中看到starts_with相关内置函数的处理痕迹)。

五、底层调用链:take_first 与 sublist

List.starts_with的核心依赖是List.take_first,其定义同样在 src/build/roc/Builtin.roc:

take_first : List(a), U64 -> List(a) take_first = |list, n| { List.sublist(list, { len: n, start: 0 }) }

take_first又被实现为List.sublist(list, { len: n, start: 0 })——即从下标 0 开始、长度为n的子列表。sublist才是真正触碰列表内部表示的低层操作。

从编译器后端代码可以印证这条调用链如何被翻译到不同目标平台:

  • 解释器src/eval/interpreter.zig中 list_take_first 的分派 调用evalListTakeFirst,与list_take_last并列,说明解释执行starts_with时实际执行的就是列表截取加相等比较;
  • Wasm 后端src/backend/wasm/WasmCodeGen.zig中 .list_take_first 的代码生成分支;
  • 开发用 LIR 后端src/backend/dev/LirCodeGen.zig中 .list_take_first 的生成逻辑。

也就是说,List.starts_with在编译后并非一个独立的专用指令,而是"截取 + 判等"的复合操作,这让它的复杂度与List.sublist相当,属于 O(prefix 长度) 级别的比较。

六、Str.starts_with:同一语义的不同实现路径

列表版本通过take_first + ==在 Roc 层组合实现;而Str.starts_with则是编译器内置的低层函数(builtin),直接映射到 Zig 运行时:

在 src/eval/interpreter.zig 中,解释器对str_starts_with指令的处理是:

.str_starts_with => blk: { const result = builtins.str.startsWith(valueToRocStr(args[0]), valueToRocStr(args[1])); const val = try self.alloc(ll.ret_layout); val.write(u8, if (result) 1 else 0); break :blk val; },

字符串前缀判断直接调用builtins.str.startsWith(底层 Zig 实现,声明于 src/base/LowLevel.zig 的str_starts_with),结果写入一个字节(u8,0/1)作为 Roc 的Bool返回。对比之下,字符串是字节序列,直接按字节前缀比较远比构造子串再判等高效,这正是 Roc 为Str提供独立低层内置函数的原因。

七、如何运行与验证这些快照

test/snapshots/repl/*.md快照由仓库中的快照工具驱动执行,工具入口位于 src/snapshot_tool/main.zig。从该文件的用法提示(main.zig#L677)可以看到针对单个 REPL 快照的调试方式:

roc snapshot --trace-eval <path_to_single_repl_snapshot.md>

该模式会逐步回放单个type=repl快照的求值过程,并核对OUTPUTPROBLEMS是否与文件中记录的期望一致。由于--trace-eval仅适用于 REPL 快照(main.zig#L3360 中明确检查type=repl),像list_starts_with.md这样的文件正是它的目标输入。常规做法是直接对整个test/snapshots/repl/目录运行快照测试,任何与预期输出不一致的变更都会导致测试失败——这正是"快照"二字的含义:把 REPL 的稳定行为固化为可回归的文档。

八、实战建议与相关 API

综合快照与源码,List.starts_with的使用要点可归纳为:

  1. 参数顺序:第一参数是完整列表,第二参数是待匹配的前缀,结果反映"第一个列表以第二个列表开头";
  2. 空前缀是恒真边界:过滤用户输入前缀时,空字符串/空列表会导致所有项匹配,需注意业务含义;
  3. 元素需要可判等:调用前确认元素类型满足a.is_eq约束,否则编译失败;
  4. ends_with成对出现:后缀判断语义完全对称,实现基于take_last
  5. 字符串场景优先用Str.starts_with:它在编译器中直接映射到底层字节比较,语义与列表版本一致但走专用低层指令。

当你在 REPL 中验证这些行为时,仓库中的四个快照文件就是最权威的参考:主用例 list_starts_with.md、空前缀 list_starts_with_empty_prefix.md、不匹配 list_starts_with_no_match.md 以及字符串版本 str_starts_with.md,它们共同构成了对该 API 语义的完整、可回归的行为契约。

【免费下载链接】rocA fast, friendly, functional language.项目地址: https://gitcode.com/GitHub_Trending/ro/roc

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询