Rust 中的“解析而非验证”模式
Rusty thoughts on "Parse, don't validate"
Alexis King 提出的“解析而非验证”理念在 Rust 中有着独特的实践方式。文章探讨了如何利用 Rust 的类型系统,将运行时验证转化为编译期保证。通过引入 NonEmpty 类型、AbsPathBuf 以及 NonZero 等概念,开发者可以在数据解析阶段就确立不变量,从而消除后续代码中对空值或无效状态的冗余检查。这种从 Vec 到 NonEmpty、从 PathBuf 到 AbsPathBuf 的类型转换,不仅提升了代码的清晰度,还避免了潜在的运行时错误,真正实现了“解析即验证”的优雅设计。
一旦解析器将 mode 转换为 Mode 枚举,就不再需要任何进一步的验证。
HN 评论区
34- articulatepang
我更喜欢一条稍微更通用的原则:让非法状态无法表示(Make Illegal States Unrepresentable,简称 MISU)。“解析而非验证”原则只是 MISU 的一个特例。
两者的区别在于:即使没有发生类似解析的转换,MISU 依然适用。例如,假设你有一个变量表示网络连接的状态,它可以是 Disconnected(断开),或者 Connected to some IP address(连接到某个 IP 地址)(这里做了简化)。
那么一种实现方式可能是:
struct {
connected: bool,
peer_ip: int32
}
问题在于,这允许我们表示一种非法或无意义的状态:虽然已断开连接,但 peer_ip 字段里还残留着一些旧的垃圾数据。更糟糕的是,我们可能会写成:
struct {
connected: bool,
peer_ip: Option<int32>
}
这样就会出现 connected = true 但 peer_ip = None 的情况。
解决方案是使用和类型(sum type):
type connection =
Disconnected
| Connected of int32
(抱歉使用了虚构的语法;希望熟悉 Rust 的人能看懂。)
“让非法状态无法表示”这一原则适用于程序的各个层面,包括模块或函数之间的每一个接口,解析输入只是其中之一。
- Fluorescence
我不确定这种类型是否真是个好建议:
pub struct NonEmpty<T> {
pub head: T,
pub tail: Vec<T>,
}
你不得不手动实现各种 trait 来支持切片和迭代的易用性,而且如果需要以 Vec 的形式传递所有权,还会付出昂贵的重新分配代价。
我预期的写法是:
pub struct NonEmpty<T> {
v: Vec<T>,
}
构造函数负责强制约束不变式,然后为 [T] 实现 Deref 和 DerefMut,从而获得正常的 len/is_empty/索引/迭代功能,可以以 &[T] 的形式传递给其他函数,也可以修改值(这不会破坏不变式)。
至于如何在保持不变式的前提下修改长度,则看开发者自己的选择了,例如:
- 添加 .into_vec() 方法用于解包/修改/重新包装
- 添加你自定义的保持不变式的修改器
- Supermancho
“解析而非验证”可以重述为:
“期望解析器完成类型验证”
这显然只是把问题从类型层面转移到了其他地方。相比之下,值验证是一个更简单的问题,可以作为独立的应用特定检查来处理。
- jelder
这篇文章很棒。Alexis King 实际上曾表示,如果她早知道“解析而非验证”会如此流行,她本会用一种比 Haskell 更广泛使用的语言来写这篇文章。
- jph
这是一篇关于 Rust 类型系统优势的好文章。如果你喜欢这篇内容,你可能会好奇如何构建自己的解析能力。我推荐 Rust 的 Winnow 和 Nom 这两个 crate,以及 Rust 的 From 和 Into 这两个 trait。