Rust 模式匹配中的前导竖线(Leading Vert)语法:从 RFC 1925 提案到 or-patterns 的落地
Rust 模式匹配中的前导竖线(Leading Vert)语法:从 RFC 1925 提案到 or-patterns 的落地
本文以 Rust 官方 RFC 仓库中的 text/1925-optional-match-vert.md(Feature Name:match_vert_prefix)为核心,系统讲解"在 match 分支模式开头允许可选 | 前缀"这一语法提案的动机、语法设计与其最终落地路径:前导竖线后来通过 text/2535-or-patterns.md 中的 top_pat : '|'? pat<allow_top_alt> ; 规则正式进入 Rust 顶层模式语法。读完本文,你将理解前导 | 解决的代码对齐问题、它与其他 or-patterns 语法(text/2175-if-while-or-patterns.md)的关系,以及它在嵌套模式中为何被拒绝。
提案背景:让模式分支的每个 | 都能对齐
在 Rust 中,match 的每个分支可以用 | 连接多个备选模式,例如:
use E::*;
enum E { A, B, C, D }
// 这是当前合法的 Rust
match foo {
A | B | C | D => (),
}
RFC 1925 提出的目标非常简单:允许在第一个备选模式之前也写一个 |,即允许前导竖线:
match foo {
| A | B | C | D => (),
}
这一想法直接来自 F# 的使用体验。在 F# 中,联合类型(union,相当于 Rust 的枚举)的声明本身就采用"每个分支前都带 |"的风格,且以下三种写法完全等价:
// 普通联合类型
type IntOrBool = I of int | B of bool
// 为了风格一致,让每一行看起来都一样
type IntOrBool =
| I of int
| B of bool
// 允许折叠成一行
type IntOrBool = | I of int | B of bool
F# 的 match 语句同样延续这种风格——注意这里的每个 | 都是严格对齐的,而当前的 Rust 做不到这一点:
match foo with
| I -> ""
| B -> ""
RFC 作者在用过 F# 的这种风格后,认为 Rust 语言层面不支持前导 | 是一种限制,因此提出通过"对现有语法做直接扩展"来引入该能力。
核心动机:最大化 | 的对齐一致性
当前 Rust 对齐的尴尬
考虑一个多行排列的 or-pattern。在提案之前,Rust 只能做到以下两种"不一致"的写法之一:
use E::*;
enum E { A, B, C, D }
match foo {
// |
// V 这里不一致地缺少一个 `|`
A
| B
| C
| D => (),
}
match foo {
A |
B |
C |
D => (),
// ^ 同样不一致:这是序列的最后一项,
// 由于后面没有更多项,省略后续的 `|` 也可以认为是合理的
}
第一种写法首行缺少 |,后续行却有 |;第二种写法把 | 放在行尾,最后一行又特殊。两种风格都无法让每个备选模式前的 | 整齐对齐。
提案允许的写法
RFC 1925 提议的语法允许这样写:
use E::*;
enum E { A, B, C, D }
match foo {
| A
| B
| C
| D => (),
// ^ 通过配齐前导竖线获得了视觉一致性
}
每一行都以 | 开头,垂直方向完全对齐,这正是 F# 风格在 Rust 中的映射。
单行 match 的灵活性
前导 | 还给单行写法带来了风格自由。下面两种写法完全等价,不存在任何语义歧义,选择哪种只是风格偏好:
use E::*;
enum E { A, B, C, D }
// 带前导竖线
match foo {
| A | B | C | D => (),
}
// 当前已允许的写法
match foo {
A | B | C | D => (),
}
RFC 原文明确强调:这两种写法之间不应该有任何歧义,偏好应归结为风格选择。
多分支场景:所有等价写法一览
RFC 1925 给出了同一逻辑下四种风格等价的多分支 match,用于说明前导 | 不会改变任何语义,只影响排版:
use E::*;
enum E { A, B, C, D }
match foo {
A | B => println!("Give me A | B!"),
C | D => println!("Give me C | D!"),
}
match foo {
| A | B => println!("Give me A | B!"),
| C | D => println!("Give me C | D!"),
}
match foo {
| A
| B => println!("Give me A | B!"),
| C
| D => println!("Give me C | D!"),
}
match foo {
A | B =>
println!("Give me A | B!"),
C | D =>
println!("Give me C | D!"),
}
混合风格的边界情况
前导 | 允许与否是逐分支独立的。即使在一个分支前写 |、另一个分支前不写,也完全合法,只是视觉上略有不一致。RFC 用以下例子说明这一点:
use E::*;
enum E { A, B, C }
match foo {
| A
| B => {},
| C => {}
// ^ 按照上面的风格,每个分支的第一个元素前都可以放一个 `|`
}
match value {
| A
| B => {},
C => {}
// ^ 给 `A` 加了 `|` 却没给 `C` 加,看起来不太一致,
// 但这很难说非法——分支永远位于 `=>` 之后,
// 这不是 *语法* 层面需要过分关心的事
}
也就是说,RFC 作者认为这种对齐的不一致性属于风格问题,语法层面不需要禁止。
详细设计:语法上的一处最小改动
RFC 1925 的详细设计极其克制:只需要在 match 分支模式的语法产生式中,给第一个 pat 前加一个可选的 '|',语法中其余部分无需任何改动。
// 变更前
match_pat : pat [ '|' pat ] * [ "if" expr ] ? ;
// 变更后
match_pat : '|' ? pat [ '|' pat ] * [ "if" expr ] ? ;
其中 [ "if" expr ] ? 对应 match 分支的守卫条件(guard)。变更后语法仍保持上下文无关,解析器只需允许模式序列以单个前导 | 开头即可。RFC 作者坦言自己并不清楚具体实现细节,但明确断言除这一处产生式外,语法中的其他部分都不需要更新。
前导 | 在 if let、while let 等场景的延伸
虽然 RFC 1925 只针对 match 提出 match_pat 的改动,但这一"可选前导 |"的语法模式随后被后续 RFC 推广到了更多位置。例如 text/2175-if-while-or-patterns.md(RFC 2175)在为 if let、while let、let 与 for 引入顶层 or-patterns 时,其语法产生式同样包含了可选的 '|'?:
if_let_expr : "if" "let" '|'? pat [ '|' pat ] * '=' expr '{' block '}'
else_tail ? ;
并指出这些构造可以通过语法降级(syntax lowering)转换成 match 与 loop + match 表达式。
落地验证:RFC 2535 如何正式引入前导 |
仓库中的 text/2535-or-patterns.md(RFC 2535,or_patterns,2018-08-29)将 or-pattern 推广为可任意嵌套,并在 "Leading |" 小节中正面讨论了前导 | 的取舍。这里给出了一个关键设计决策:
模式语法的新增应写成
pat : .. | pat "|" pat ;还是pat : .. | "|"? pat "|" pat ;?RFC 2535 选择了前者,即不在嵌套模式内部允许前导|。
选择理由有四条(原文摘录):
- 若选择前者(不允许嵌套前导
|),未来可以再放宽为后者,但反过来不行——这是保守选择; - 有 OCaml 的先例(OCaml 拒绝
Some(| 1 | 2)这类嵌套前导竖线写法); - 对宏的收益存疑——宏并不需要产出前导交替;
- 模式内部的前导交替被认为是糟糕的风格。
与此同时,RFC 2535 明确指出允许前导 | 有一个显著优点:让 syn 等解析库或工具稍微更容易解析 Rust 语法。
更重要的是,RFC 2535 最终在顶层模式(top_pat)中正式引入了前导 |,这正是 RFC 1925 的核心诉求。其语法定义为:
top_pat : '|'? pat<allow_top_alt> ;
并将 match 分支的产生式改写为使用 top_pat:
match_arm : maybe_outer_attrs top_pat (IF expr_nostruct)? FAT_ARROW ;
同时,let 语句、if let、while let、for 循环的语法也都改用 top_pat:
let : LET top_pat maybe_ty_ascription maybe_init_expr ';' ;
expr_if_let : IF LET top_pat '=' expr_nostruct block (ELSE block_or_if)? ;
expr_while_let : maybe_label WHILE LET top_pat '=' expr_nostruct block ;
expr_for : maybe_label FOR top_pat IN expr_nostruct block ;
也就是说:RFC 1925 提议的"match 分支开头允许可选 |"最终以 RFC 2535 的 top_pat 形式落地,并且被统一推广到所有接受顶层模式的场景。RFC 2535 的实现说明中还特别强调,如果对 c(p | q) 这样的嵌套模式做纯 DNF 展开,可能会产生模式数量上的指数级膨胀(例如 (0 | 1, 0 | 1, 0 | 1, 0 | 1) 会展开成 16 个分支),因此实际实现更倾向于一步式案例分析(one-step case analysis),而不是简单的语法糖展开——这说明前导 | 只是语法层能力,其底层匹配语义仍由 or-pattern 的统一机制承担。
教学示例:官方推荐如何向新手解释
RFC 1925 的 "How We Teach This" 章节指出,加入前导 | 的教学示例非常直接:只需指出"前导竖线是被允许的"即可。RFC 给出了一个可直接用于文档与教程的完整示例:
use Letter::*;
enum Letter {
A,
B,
C,
D,
}
fn main() {
let a = Letter::A;
let b = Letter::B;
let c = Letter::C;
let d = Letter::D;
match a {
A => "A",
// 可以用 `|` 写备选分支。
B | C | D => "B, C, or D",
}
match b {
| A => "A",
// 前导 `|` 是被允许的。
| B
| C
| D => "B, C, or D",
}
}
对比上下两个 match:第一个分支没有前导 |,第二个分支的每个备选模式前都带 |,二者语义完全相同。这个示例同时覆盖了"单一模式分支"与"多模式 or 分支"两种形态,适合作为学习资料中的对照用例。
与其他 or-patterns 语法协同使用的注意事项
前导 | 只是 or-pattern 语法的一个排版层面扩展,它不改变 | 运算符本身的优先级与结合规则。RFC 2535 明确规定:在模式中 | 的优先级最低,@(绑定模式)比 | 绑定得更紧,因此 i @ p | q 被解析为 (i @ p) | q 而不是 i @ (p | q)——这也是在顶层模式中保持向后兼容的必要条件。若你想表达 i @ (p | q),必须显式加括号。这与前导 | 的取舍是同一套语法设计中的两面:前导 | 只允许出现在顶层(top_pat),嵌套 or-pattern(Some(A | B))内部则不允许前导竖线,与 OCaml 的约束保持一致。
另外需要注意,RFC 1925 中提到前导 | "对宏有好处"(Benefits to macros),但该小节在原文中仅以 "Needs filling in"(待补充)占位,未给出具体论证,属于原提案中未展开的部分;而 RFC 2535 后来对宏场景的判断也偏保守(宏不需要产出前导交替,且 pat 宏片段说明符按 pat<no_top_alt> 匹配)。因此,宏与前导 | 的交互并未成为该语法落地的主要驱动力。
仓库中的相关文档索引
如果你想继续追踪该语法的完整演进脉络,可以在本仓库中对照阅读以下文件:
- text/1925-optional-match-vert.md:本主题的原始提案,定义了
match_pat : '|' ? pat [ '|' pat ] * [ "if" expr ] ?的语法改动; - text/2535-or-patterns.md:or-patterns 的完整设计,其中 "Leading
|" 一节讨论了前导竖线的取舍,top_pat : '|'? pat<allow_top_alt> ;是该语法正式落地的规则; - text/2175-if-while-or-patterns.md:将顶层 or-pattern 与可选前导
|推广到if let、while let、let与for的 RFC; - lang_changes.md:Rust 语言子团队对语言变更(含语法变更)的 RFC 流程说明,前导
|正属于此类需要 RFC 的语言层改动。
小结
RFC 1925(match_vert_prefix)用一个极小的语法改动解决了真实的排版痛点:让 or-pattern 的每个备选模式前都能对齐一个 |,从而在 Rust 中复现 F# 风格的整洁 match 代码。虽然提案本身只修改了 match_pat 一条产生式,但它的核心思想——顶层模式允许可选前导 |——最终通过 RFC 2535 的 top_pat 规则正式落地,并统一覆盖 match、let、if let、while let 与 for 等所有接受顶层模式的场景;而嵌套模式内不允许前导 | 的保守决定,则保证了与 OCaml 先例及既有模式优先级语义的一致性。理解这一演进,有助于你在实际代码中选择一致、可读的分支排版风格,并正确使用 or-patterns 语法。