设计解析:pack、each 与 pack expansion 的完整机制)
Carbon 语言变长参数Variadics设计解析pack、each 与 pack expansion 的完整机制【免费下载链接】carbon-langCarbon Languages main repository: documents, design, implementation, and related tools. (NOTE: Carbon Language is experimental; see README)项目地址: https://gitcode.com/GitHub_Trending/ca/carbon-lang本篇指南基于 Carbon 语言仓库中的官方设计文档 proposals/p002240-variadics.md提案 #2240及其配套设计正文 docs/design/variadics.md系统讲解 Carbon 如何定义、实现并类型检查可接收任意数量参数的泛型变长函数。读完本文你将掌握each名称、...展开pack expansion、...and/...or折叠、...expand元组拆分等核心语法的语义理解其编译期循环执行模型、模式匹配的求值逆运算原则以及类型系统中 segment/shape 的合并与拆分算法并能对照 C20 与 Swift 的实现看清 Carbon 的设计取舍。问题背景为什么需要变长参数Carbon 需要一种方式来定义变长variadic的函数与参数化类型——即可以接收可变数量参数的能力。这一需求在 C 生态中由来已久C 语言通过varargs机制支持变长函数但该机制在 C 中因缺乏类型安全而饱受诟病C 为此另起炉灶提供了变长模板variadic templates可以用于函数、类甚至变量。然而变长模板存在若干明显短板必须是模板变长模板无法被定义检查definition-checked同时带来必须写在头文件、模板实例化导致代码膨胀等一系列成本。固定类型变长函数难以书写要定义一个参数类型固定不变的变长函数异常困难且这种函数的签名无法向读者清晰传达参数类型固定这一信息。递归而非迭代既有设计鼓励用递归而非迭代来处理变长参数列表中的元素导致模板实例化更多pack 规模在编译期有时也包括运行期通常产生至少平方级的开销。C 较新版本虽可利用逗号运算符上的折叠表达式fold expressions以过程式方式迭代 pack但该技巧笨拙且不广为人知。C 标准社区曾提出多项改进提案如 P1219R2 同质变长函数参数、P1306R1 展开语句、P1858R2 泛化 pack 声明与使用、P2277R0 模板之外的 pack但由于 C 连非变长函数都未选择定义检查定义检查的变长参数短期内难以落地支持固定类型参数 pack 的提案曾被否决而迭代参数 pack 的提案长期停滞。Swift 支持元素类型一致的变长参数并已批准 SE-0393Value and Type Parameter Packs为定义检查的异构变长参数铺路配套的 SE-0404Pack Iteration尚未最终通过。Rust 也曾多次尝试引入此类特性但相关工作目前处于停滞状态。正是在这样的背景下Carbon 提出了自己的变长参数核心设计。核心概念pack、each-name 与 pack expansion提案在抽象层面给出了三个相互关联的基础概念Pack固定数量元素构成的序列元素类型可以互不相同。pack 与元组值tuple values在很多方面相似但它不是一等值——没有任何运行时表达式会求值出一个 pack。pack 的元数arity是一个编译期值表示序列中值的个数。each-name由关键字each后接某个 pack 的名称构成只能出现在 pack expansion 内部。在 pack expansion 的第 N 次迭代中each-name 引用的是命名 pack 的第 N 个元素。因此带 each-name 的绑定模式如each ElementType: type相当于声明了命名 pack 的全部元素从而隐式地声明了 pack 本身。Pack expansion以...开头的语法单元是一种对名为 pack 的序列执行的编译期循环。值得强调的一点是each属于名称语法的一部分而非表达式运算符因此它比任何表达式语法结合得都紧。例如 Zip 实现中的循环条件...and each iter ! each vector.End()等价于...and (each iter) ! (each vector).End()。语法全景上下文决定...的四种语义pack expansion 的语法与行为取决于其所在上下文某些情况下还取决于...后跟随的关键字元组字面量表达式上下文如函数调用实参列表...迭代求值其操作数表达式并把求得的每个值作为元组的相继元素。布尔表达式上下文...and与...or迭代求值布尔表达式用and/or合并结果并在底层运算符短路时提前终止循环。语句上下文...迭代执行一条语句。元组字面量模式上下文如函数形参列表...迭代匹配被匹配元组scrutinee的元素。与 pack 绑定pack bindings配合即可让函数接收任意数量的实参。此外还有一个特殊语法... expand 表达式。它以,的优先级出现在元组表达式元素位置不算pack expansion而是有自己的语义——要求操作数具有元组类型求值该操作数并把它解包后的各个元素作为外层元组字面量的元素。这在把非字面量元组值用作函数调用实参时尤其有用fn F(x: i32, y: String); fn MakeArgs() - (i32, String); F(...expand MakeArgs());...and、...or、...expand只需一个 token 的 lookahead 即可区分...的其他含义则靠所在上下文区分。由此产生一个推论如果...最近的包围定界符是圆括号那么这些括号会被解释为构成元组而非分组——所以(... each ElementType)是元组字面量即使它不含逗号。按约定...之后总是跟空白唯独...and、...or、...expand两 token 之间不写空白以强调关键字不是展开体的一部分而是对...语法与语义的修饰符。约束规则同一展开中的所有 each-name 必须引用元数相同的 pack即该展开的元数若展开中不含 each-name则它必须是模式或是绑定模式类型位置上的表达式其元数由被匹配对象scrutinee推导。pack expansion 或...expand表达式不能包含另一个 pack expansion 或...expand表达式each-name 不能在声明它的同一个 pack expansion 中被使用大多数情况下可改写为普通名称因为 each-name 仅在需要把 pack 从一个展开传递到另一个展开时才必要。示例精读七个典型用法设计正文提供了多个贯穿全文的示例它们是理解变长参数语义的最佳入口。固定类型的求和函数——最简单的同质变长函数... each param: i64表示任意数量的 i64 参数// Computes the sum of its arguments, which are i64s fn SumInts(... each param: i64) - i64 { var sum: i64 0; ... sum each param; return sum; }异构类型的字符串拼接——类型层面的泛型 pack... each T: ConvertibleToString是隐式参数列表中的类型 pack参数... each param: each T中每个参数类型各不相同但都约束为可转换到 String函数体先迭代累加长度预分配缓冲区再迭代追加字符串// Concatenates its arguments, which are all convertible to String fn StrCat... each T: ConvertibleToString - String { var len: i64 0; ... len each param.Length(); var result: String ; result.Reserve(len); ... result.Append(each param.ToString()); return result; }同类型最小值——混用普通参数与变长参数的first/rest风格且展开体内含if语句// Returns the minimum of its arguments, which must all have the same type T. fn MinT: Comparable Value - T { var result: T first; ... if (each next result) { result each next; } return result; }函数应用——用...expand args把元组参数展开为调用实参返回类型auto由推导决定// Invokes f, with the tuple args as its arguments. fn Apply[... each T: type, F: Call(... each T)] (f: F, args: (... each T)) - auto { return f(...expand args); }Zip——设计正文反复引用的旗舰示例任意个、任意元素类型的向量返回元素为元组的向量。它同时用到了类型 packeach ElementType、值 packeach vector、each iter、...and短路循环、...语句展开与元组展开// Takes an arbitrary number of vectors with arbitrary element types, and // returns a vector of tuples where the ith element of the vector is // a tuple of the ith elements of the input vectors. fn Zip[... each ElementType: type] (... each vector: Vector(each ElementType)) - Vector((... each ElementType)) { ... var each iter: auto each vector.Begin(); var result: Vector((... each ElementType)); while (...and each iter ! each vector.End()) { result.push_back((... each iter)); ... each iter; } return result; }混排变长与非变长参数——允许变长参数位于参数列表中间// Toy example of mixing variadic and non-variadic parameters. // Takes an i64, any number of f64s, and then another i64. fn MiddleVariadic(first: i64, ... each middle: f64, last: i64);元组拼接——利用变长类型推导结果构造新元组// Toy example of using the result of variadic type deduction. fn TupleConcat... each T1: type, ... each T2: type, t2: (... each T2)) - (... each T1, ... each T2) { return (...expand t1, ...expand t2); }注意TupleConcat的返回类型(... each T1, ... each T2)在同一元组字面量中出现了两个...表达式——元组表达式允许出现多个...这一点与元组模式恰好相反见后文备选方案中对模式限制的讨论。执行语义编译期循环的精确刻画设计正文以包索引pack index形式给出精确语义。设 N 为展开的元数$I为表示 pack 索引的概念变量。这些语义在单态化monomorphization阶段实现因此 N 是已知的整型常量$I的值虽在执行中变化但始终被视为常量。语句展开形如 ...语句 的语句通过把该语句执行 N 次$I从 0 到 N-1来求值。例如若a、x、y都是元数为 3 的 pack则... each a each x * each y;大致等价于三条顺序执行的语句a[:0:] x[:0:] * y[:0:];、a[:1:] x[:1:] * y[:1:];、a[:2:] x[:2:] * y[:2:];。这里的[:N:]只是用于说明的概念性 pack 索引运算符——pack 实际上不能在 Carbon 代码中被索引未来是否加入索引能力留作后续工作需另行提案。...and形如 ...and表达式 的表达式先初始化概念布尔变量$R为true然后最多执行 N 次 $R $R and表达式$I从 0 到 N-1一旦$R变为 false 即提前终止最终$R的值即表达式值。例如...and F(each x, each y)的行为类似true and F(x[:0:], y[:0:]) and F(x[:1:], y[:1:]) and F(x[:2:], y[:2:])。由于其短路特性它也可被理解为逐次求值、遇 false 即 break的循环。注意 C 的折叠表达式恰恰也为、||以及,这三个运算符给予特殊待遇——它们是仅有的允许省略初始值的运算符如... args而非true ... args且折叠似乎正是 C 引入折叠表达式的原始动机。...or与...and相同只是用or替代and并把true/false对调。元组表达式展开形如 ...表达式 的元组表达式元素求值为 N 个值的序列其中第 k 个值是该表达式在$I等于 k-1 时的值。例如(... F(each x, each y))等价于(F(x[:0:], y[:0:]), F(x[:1:], y[:1:]), F(x[:2:], y[:2:]))。each-name求值为其所引用 pack 的、从零开始索引的$I个元素的值。模式匹配语义模式是求值的逆运算pack expansion 模式遵循一个总原则模式匹配是表达式求值的逆运算。例如若模式(... each x: auto)匹配了某个被匹配值s则表达式(... each x)应当等于s。这些语义同样在单态化阶段实现因此所有类型与元数都是已知常量。一个元组模式最多只能包含一个形如 ...操作数 的子模式。当存在这样的子模式时展开前的 N 个模式元素与被匹配元组的前 N 个元素匹配展开后的 M 个模式元素与被匹配元组的最后 M 个元素匹配若被匹配元组元素少于 N M 个则模式不匹配剩余的被匹配元素按顺序与操作数迭代匹配每次迭代中$I等于被匹配元素索引减去 N第 N 次迭代时绑定模式把命名 pack 的第 N 个元素绑定到第 N 个被匹配值。在Zip的签名中参数列表就是一个单独的 pack expansion 模式... each vector: Vector(each ElementType)因此整个实参列表都会与绑定模式each vector: Vector(each ElementType)匹配。由于子模式会在一次模式匹配操作中匹配多个或零个被匹配值pack expansion 模式内的绑定模式必须声明 each-name其类型表达式可以包含 each-name如each ElementType但此时该 each-name 必须是外围模式的推导参数。若 pack expansion 模式中出现阶段关键字如generic、template和/或修饰符如ref顺序为...阶段 修饰符each名称。例如... generic each T: type、... runtime ref each x: i32。这一顺序确保each始终与绑定名称结合得最紧密该限制未来或可放宽但目前缺乏约束设计的动机用例。类型检查segments、shapes 与迭代检查设计正文为变长参数的类型检查建立了专门的词汇表与算法。为表示类型系统中存在但无法直接用 Carbon 代码书写的值与表达式文档引入了一批伪语法记号如«»‖⟬⟭以示区别。元组、pack、segment 与 shape在变长参数语境下元组字面量由逗号分隔的segment 序列组成而元素一词保留给 pack expansion 之后的元组字面量组成部分。例如(... each foo)可以求值为任意元素个数的元组值但表达式本身恰好只有一个 segment。每个 segment 都有一个类型它可能以符号形式同时表达 segment 元素的类型与 segment 的元数元组字面量的类型是其各 segment 类型的元组字面量。为了表示由调用方隐式指定重复次数的类型文档引入两个新的伪语法‖each X‖引用包含each X声明的 pack expansion 的推导元数«E; N»求值为 E 的 N 次重复称为元数强制arity coercion其约束是 E 不得包含 pack expansion、each-name 或 pack 字面量。于是... each y声明为each y: i32的类型就是... «i32; ‖each y‖»。当一个 pack而非元组拥有与某元组相同的元素时其类型用⟬⟭定界的pack 字面量表示如⟬f32, Optional(each T), «i32; ‖each y‖»⟭pack 字面量每个 segment 都充当独立的循环体。shape形状是 pack 字面量各 segment 元数构成的元组其他表达式与模式也各有 shape元数强制的 shape 是(A,)each X的 shape 是(‖each X‖,)不含 pack 字面量/形状强制/each-name 的表达式的 shape 是(1,)表达式元数即其 shape 各分量之和。文档提供了完整的 shape 判定规则typing and shaping rules见附录若某 AST 节点所有子节点的 shape 均为 1其 shape 为 1若存在某个 shape S 使所有子节点 shape 均为 1 或 S其 shape 为 S否则该节点ill-shaped。pack 字面量可以在不含...的外围表达式中被展开expanded把外层表达式搬进 pack 字面量内部完全展开即反复展开 pack 字面量与元数强制直到无法再展开。完全展开后的表达式其标量分量scalar components集合递归定义pack 字面量的标量分量为各 segment 标量分量的并集元数强制«F; S»的唯一标量分量是 F否则唯一标量分量是 E 自身。标量分量不含 pack 字面量、pack expansion 或元数强制但可以含 each-name因此可套用普通非变长表达式的规则把名称当作不透明项来处理。迭代类型检查展开的执行语义建立在同时迭代 each-name的概念重写形式上理论上可以靠检查重写形式来检查展开。但重写形式通常无法作为普通 Carbon 代码通过检查each-name 在不同迭代上可能有不同类型且类型差异会通过表达式传播each x与each y类型可变each x * each y的类型也可变因此必须逐迭代检查循环体。然而类型检查阶段往往连迭代次数都未知——each-name 的类型是 pack是 segment 序列而非元素序列。解决之道是要求展开中所有 each-name 的类型必须具有相同 shape从而把对输入元素的迭代换成对 segment 的同步迭代。于是pack expansion 内表达式或模式的类型是 segment 序列即 pack代表其在迭代过程中的各类型但注意具有 pack 类型的表达式并不求值为 pack 值而是在展开循环过程中求值为一系列非 pack 值其 pack 类型只是对该序列类型的概括。单次迭代内按普通非变长规则检查只是取 each-name 类型当前 segment 的标量分量。展开体检查完成后语句展开无需进一步检查语句没有类型...and/...or表达式的类型为bool操作数类型 pack 的每个 segment 必须可转换为bool元组元素展开表达式或模式的操作数类型 pack 的各个 segment成为外层元组类型或模式的 segment。模式检查固定元数与推导元数pack expansion 模式若至少包含一个非外围 full pattern 参数的 each-name 用法则具有固定元数否则具有推导元数一个元组模式最多只能有一个推导元数 segment。例如class C(... each T: type) { fn F... each U: type; }... each t: each T的元数由传给C的实参在调用 F 之前决定故为固定元数... each u: each U的元数由传给 F 的实参决定故为推导元数。检查完 full pattern 后编译器会尽量合并元组 segment 以简化后续模式匹配。例如fn MinT: type - T;会被重写为单一参数形式fn MinT: type - T;其中的‖each next‖1精确刻画了至少匹配一个元素的约束。异构模式下的合并更复杂如ZipAtLeastOne会被重写为引入⟬First, each Next⟭名称 pack 的形式最终用发明的 each-nameeach __Args替代名称 pack使函数只有一个参数。把名称 pack 替换为发明的 each-name 需要满足严格条件名称 pack 中每个名字至多使用一次恰好包含一个 each-name替换需移除全部组成名字含声明的使用被重写的 pack expansion 不得含除被替换名称 pack 外的其他 pack 字面量其推论是所有组成名字类型相同。模式匹配检查拆分与合并要检查元组模式与元组被匹配值之间的匹配类型检查器尝试拆分与合并被匹配值类型的 segment使其 segment 数与模式类型一致且对应 segment 元数相同。例如检查对ZipAtLeastOne的调用时被匹配值类型(... Vector(each T), Vector(i32))被合并为单个 segment(... ⟬Vector(each T), Vector(i32)⟭)其元数‖each T‖1与模式 segment 的‖each Next‖1通过推导‖each Next‖ ‖each T‖匹配。合并被匹配值 segment 时不做名称 pack 发明替换也不要求合并后的 segment 只有一个标量分量。搜索重写时推导元数 segment 左侧的模式 segment 自左向右处理右侧的自右向左从外向内每个模式 segment 贪婪地合并未匹配的被匹配值 segment必要时从右端切分以精确对齐 shape。segment 一一对应后逐对检查标量分量走普通非变长类型检查规则。文档还留了 TODO将来可扩展角色互换的互补方法——最大化合并被匹配值元组、要求每个 segment 单个标量分量再合并/拆分模式元组以匹配它。附录类型系统形式化提案附录给出了完整的类型系统形式化是理解实现与后续演进的关键。显式推导元数该形式化中推导元数是显式的Carbon 代码按如下方式脱糖desugar每个 pack expansion 模式都会在外围 full pattern 的推导参数中引入一个绑定模式__N: Arity名字避免冲突展开内每个形如each X: T的绑定模式若 T 不含 each-name则重写为each X: «T; __N»若未引入任何__N的使用则删除其声明。Arity是编译器内部类型表示非负整数仅支持运算与整数/其他 Arity 相加只用于类型检查无运行时语义符号上满足交换律与结合律。类型与 shape 规则摘录元数强制的 shape 为;后表达式的值pack 字面量的 shape 为其各 segment 元数的拼接each-name 表达式的 shape 为其声明绑定模式的 shape。若绑定模式的名称部分与类型部分 segment 数相同且每个名称 segment 是 each-name 当且仅当对应类型 segment 的 shape 不为 1则该绑定模式的 shape 为类型表达式的 shape否则 ill-shaped。类型计算each x: auto的类型是新建的推导参数each __X如同声明为... each __X: typeeach-name 表达式的类型是声明它的绑定模式的类型表达式元数强制«E; S»的类型为«T; S»T 为 E 的类型pack 字面量类型由各 segment 类型拼接而成嵌套 pack 字面量被扁平化pack expansion 表达式/模式的类型是...BB 为体类型元组字面量类型是其 segment 类型的元组字面量若表达式/模式含包字面量或元数强制且不在 pack expansion 内则取其完全展开形式的类型。归约规则Reduction rules所有规则都是等价关系可在推导时反向使用且只要求 well-shaped 而不要求 well-typed。核心规则包括Singular pack removal⟬E⟭归约为 EE 是单个 pack segment。Singular expansion removal若 E 的 shape 为(1,)...E归约为 E。Pack expansion splitting...⟬E, S⟭归约为...E, ...⟬S⟭。Pack expandingF(⟬P1, P2⟭, X, ⟬Q1, Q2⟭, «Y; S1S2»)归约为⟬F(P1, X, Q1, «Y; S1»), F(P2, X, Q2, «Y; S2»)⟭。该规则可泛化F 可以是任意非...的表达式语法如⟬X1, X2⟭ * ⟬Y1, Y2⟭归约为⟬X1 * Y1, X2 * Y2⟭甚至是模式语法当绑定模式语法充当 F 时其名称部分必须是名称 pack。Coercion expandingF(«X; S», Y, «Z; S»)归约为«F(X, Y, Z); S»F 不可为模式语法。Coercion removal«E; 1»归约为 E。Tuple indexing定义了对含 pack expansion 元组的取下标行为涉及 segment 元数比较与归约。等价、相等与可转换性Pack renaming满足条件含至少一个 each-name、范围内名字不被重复使用、无其他 pack 字面量共处同一展开时可把名称 pack 的所有出现重写为each __A。Expansion convertibility...T可转换为...U当 U 的元数等于 T 的元数且 T 的每个标量分量都可转换为 U 的所有标量分量。Shape equalityshape 元组按分量递归相等。模式匹配类型检查算法与规范化full pattern 的**正规形normal form**不含 pack 字面量、且所有元数强制完全展开所有用户手写的 full pattern 天然处于正规形。**规范形canonical form**是最大化合并的唯一正规形——每个元组模式与元组字面量 segment 数最少。例如__N: Arity的规范形是__N: Arity。函数调用类型检查即把 F 转成规范形检查实参类型 A 是否可转换为形参类型套用前述推导规则成功后把所得绑定映射作用于函数返回类型以得到调用表达式的类型。其他模式匹配操作的类型检查则归结为函数调用检查把被匹配类型 S 对模式 P 的检查转化为对假设签名fn __F(P,)-();调用__F(S,)的检查。规范化算法从正规形出发增量地把相邻的单一形参类型合并进变长形参类型。文档以fn FFirst: type, Second: type, ... each Next: type, second: Vector(Second), ... each next: Vector(each Next)) - (First, Second, ... each Next);为例完整演示先脱糖显式元数再依次反向应用 singular pack removal、pack expanding、pack expansion splitting、pack renaming先把Second并入each Next再把First并入最终得到无可再合并的规范形。这套算法演示清晰展示了变长参数类型检查如何与形式化归约规则协同工作。与 C20 / Swift 的横向对比提案的对比表用同一组问题求和、最小值、字符串拼接并排展示了四种写法直观说明 Carbon 语法在表达力与可读性上的取舍。求和SumIntsi64 参数求和Carbon 直接用... each param: i64声明同质变长参数语句展开... sum each param;迭代累加。C20 需用模板 折叠表达式(static_castint64_t(params) ... 0)且要写requires约束若采用 P1219R2 扩展可简化为int64_t SumInts(int64... params)但折叠仍不可避免。Swift 用_ params: Int64...声明靠for param in params普通循环实现——这对 Swift 可行是因为其变长参数限定同类型。最小值Min同类型 T至少一个实参Carbon 的 first/rest 风格签名fn MinT: Comparable Value - T直接表达首参数 其余同类型参数。C20 需要模板递归if constexpr处理基例Min(rest...)递归调用这正是文档批评的递归处理 pack模式加 P1306R2 的template for后可改为过程式迭代。Swift 因参数类型同为T仍可写为普通for循环。字符串拼接StrCat异构、均可转 StringCarbon 用类型 pack[... each T: ConvertibleToString]与... each param: each T先... len each param.Length();累加长度、result.Reserve(len)预分配再... result.Append(each param.ToString());追加——一次预分配避免中间结果物化。C20 用折叠表达式分别计算长度与追加C 加 P1306R2 后可用template forSwift 加 SE-0393 与 SE-0404 后可写repeat each T与for param in repeat each param思路与 Carbon 高度相似但语法不同。设计理由Rationale为什么必须做变长参数C 互操作与迁移变长模板在 C 中相当常见Carbon 要实现与 C 的互操作和迁移见 docs/project/goals.md变长参数是前提。可读性有些 API如printf无法自然地用固定数量参数表达变长参数让代码更易读、易理解、易编写见 docs/project/goals.md。泛型变长参数定义检查使 API 更易读、易演进见 docs/project/goals.md变长与泛型等独立特性自然组合比相互排斥更利于语言整体的一致性。性能关键软件变长 API 可能比非变长方案更高效见 docs/project/goals.md。例如StrCat本质上比std::string的operator链更高效无需物化一系列部分结果并可预先分配足够容纳最终结果的缓冲区。所有 API 都是库 API原则元组、可调用对象等类型的库表示都需要变长能力见 docs/project/principles/library_apis_only.md。提案看似在若干处偏离该原则实则不然元组字面量语法本身是内建且明确被该原则排除pack expansion 模式把元组类型视为内建但元组模式与元组字面量语法相同可视为元组字面量的一种提案还修订了原则文本以明确这一点pack 类型是内建类型且无库 API但该原则只适用于一等类型——pack 类型绝非一等不能作为函数返回类型、甚至不能被命名、表达式除非处于 pack expansion 内且具编译期表达式相位否则无法求值为 pack 类型值。备选方案关键设计决策的权衡记录提案用大量篇幅记录被否决或推迟的备选方案这对理解当前设计边界极有价值。Member packs成员 pack支持把 each-name 声明为类成员会带来全新设计问题——pack 绑定当前完全依赖类型推导获得元数等信息而类成员通常没有初始化器驱动推导。而且这类需求基本可用元组/数组成员绕开故推迟到未来。Pack expansion 的单一语义模型运行时所有 pack expansion 被建模为过程式循环每次迭代表达式是标量值而类型系统中展开内表达式被概念化为一次求值产生 pack 值——如同 SIMD 的向量并行模型。二者在交界处存在阻抗失配展开内表达式具有 pack 类型却不对应 pack 值违背表达式类型等于其值类型的基本直觉。若在运行时也采用并行模型则无法建模分支控制流(... if (expand cond) then F(expand param) else G(expand param))会要求 N 次同时调用 F 和 G而非 N 次调用其一分支还可能藏在函数调用内部而无法静态检测若 F/G 有副作用甚至构成正确性问题。早期版本曾用类型 pack措辞名义上回避但未触及实质。泛化expand把...expand视为... each xx 为发明绑定的语法糖后可进一步把expand推广为优先级同*的前缀运算符。但语义可能令人惊讶例如...if (Condition()) { var x: auto expand F(y); }中F(y)在进入 pack expansion 前无条件求值且无法引用 if 块内声明的名字。省略expand...expand本质是语法糖省略可简化设计并消除以...开头却非展开的异常但会显著降低把元组展开为实参列表这一常见操作的便利性。支持展开数组定长数组几乎可视为元组类型的特例仅多一个运行时下标允许expand作用于数组甚至把类型数组当元组类型很自然但因缺乏动机用例而从当前提案中省略可作扩展加入。省略 each-names以类型区分 pack改为按类型而非名字区分 pack如fn Zip[ElementTypes:! [type;]](... vectors: Vector(expand ElementTypes)) - Vector((... expand ElementTypes));。优点无需引入一个绑定同时绑定多个值的概念、避免类型系统有 pack 类型却无对应值的异常、expand可写成符号 token、同质变长SumInts/Min可直接用数组类型普通循环实现。缺点pack 类型绑定的隐式展开伤害可读性易忽略while (...and expand iters ! vectors.End())有两个展开点必须禁止模板依赖名具有 pack 类型否则某表达式在部分实例化中是展开点而在其他实例化中不是一个使用对应单一值与复数命名/pack 类型矛盾。其变体禁止 pack 类型绑定改用变长元组类型绑定如(... expand vectors: (... Vector(expand ElementTypes)))函数体内 vectors 是元组而非 pack可规避上述缺点但签名显著复杂晦涩——而变长函数签名比函数体读得更频繁此权衡不可取leads 已在 issue #1162 决定不采用该方案。折叠表达式Fold expressions可把...and/...or推广为支持更多二元运算符与初始值的 C 式折叠。但折叠对非交换运算符如-更可能造成困惑而非有用自定义 and/or 的零元行为几乎没有用例折叠只支持固定运算符集而非函数或复合表达式。而且支持可二元/前缀一元两用的运算符 token如*需要为元组元素列表另选语法否则...*each foo会歧义。即便支持一般折叠and/or因短路特性仍需特殊对待。允许...出现在普通二元语法中还可能妨碍未来通用折叠、诱导用户尝试其他运算符、复杂化解析与 AST、迫使x or ...与... or x的无意义选择。允许元组模式中多个 pack expansion元组字面量表达式允许多个...但元组模式只允许一个表面诱人放宽实则不可行多个...模式会造成被匹配值边界歧义fn F(... each xs: i32, ... each ys: i32)无法判定 xs 与 ys 的分界除非类型不同——但那会令类型不相等成为模式的承重部分难以用泛型类型表达。函数作者可用分隔符绕开fn F((... each xs: i32), (... each ys: i32))不仅易支持还使调用点更安全可读。抽象地看把表达式语法复用为模式语法时是让语言找出使表达式求值出给定值的操作数这要求操作可逆不丢失信息含多个...的元组字面量求值丢弃了边界结构信息不可逆故不能用作模式操作。注意该绕开方案预设函数签名允许顶层以下有绑定此问题当时尚未决定issue #1229。允许嵌套 pack expansion早期版本允许展开嵌套但需为each-name 词法上处于多个展开时由谁迭代建立不令人惊讶的规则而缺乏动机用例使该设计难以评估故本提案不支持嵌套但尽量避免排除未来扩展。后缀...C 中...是后缀运算符与自然语言…一致后缀写法看似更一致。但前缀语法尤其对人类更易解析读语句时若用后缀读者可能要读完任意长度的代码块才发现它是变长执行的完全不可接受and/or/,情形虽较含糊但为与语句保持一致全部选择前缀运算符。避免 pack expansion 的上下文敏感性提案重载了...token 的多个含义含不同优先级部分依赖上下文而 Carbon 有避免上下文敏感性的原则docs/project/principles/low_context_sensitivity.md。可改用独立语法表示各含义但均有重大缺陷折叠式语法...,、...;...;作为前缀运算符与;标记语句结束的直觉完全相悖且在不需要;的上下文语句以}结束中使用会很突兀...,中,不再充当分隔符但人眼习惯将其读作分隔符——多数读者会不由自主地把(..., each x)读成两个子表达式这在浏览TupleConcat这类大段代码时尤其干扰。变长块...{ }无法为元组形式提供替代反而加剧其问题...{可能被读作对结构体字面量应用...运算符且无法变长声明展开外可见的变量如 Zip 的each iter只能退而用元组声明增加复杂度。关键字语法repeat/do_repeat/all_of/any_of受 Swift 影响但不同关键字与each更一致、视觉噪声小、更自明。但各展开根 token 间失去句法共性难以视觉识别且学习更费劲all_of/any_of无法像...and/...or那样借用非变长对应运算符的优先级可能导致需要额外括号while (all_of (expand iters ! each vectors.End()))repeat expand foo听上去像反复展开 foo实则只展开一次模式语境是描述性而非命令性的用repeat这种祈使词如(repeat each param: i64)含义含糊且这些关键字占用本可用于标识符的词法空间它们恰是 C 标准库函数名。要求each加括号给each更低优先级使each vector.End()须写作(each vector).End()对新读者更清晰但视觉上更拥挤且可能误导读者以为each可作用于标识符以外的任何东西。文档建议先观察无括号语法在实际中的可读性问题再决定还讨论了把前缀运算符嵌入-token 的通用方案x-?y或可接受但x-eachy难解析且与each是名称一部分而非作用于名称的运算符的定位相悖。融合展开 token把...and/...or当作单个 token而非禁止中间空白的两个 token更准确反映其语义与...不同但用户在...后误加空格会得到更差的用户体验。不做参数合并当前提案中编译器尝试合并函数参数以支持如Min(... each arg, 0 as i32)这类调用。不合并可使类型检查更可预测签名中每个单一参数都要求首实参单一但需额外语法声明至少 N 个实参早期版本曾用each(N)如fn MinT: type param: T) - T;而该语法存在审美问题、破坏each 是名称一部分的心智模型、且约束实际作用于整个展开而非 each-name还偏离每个可能单态化都通过即类型检查通过的理想first/rest 风格对 C 程序员更自然他们无从得知自己在给调用方强加非预期约束。穷尽式函数调用类型检查当前设计用合并与拆分对齐实参/形参使每个实参恰有一个可匹配的形参并计划扩展反向思路每个形参恰有一个可匹配的实参但并非总能对齐。例如fn F... each T: type;配合fn G(... each z: i32) { F(... each z, 0 as i16); }——每个可能的单态化都可通过但无法合并类型不同也无法拆分可能为空。理论上可枚举结构性不同的匹配情形逐对检查再逻辑与合并但情形数随形参数目增长、且须对每个变长实参重复成本二次方增长违背快速可扩展开发目标docs/project/goals.md更严重的是调用类型检查还要输出调用表达式的类型其结果依赖推导参数组合各情形结果需要把情形分支直接并入类型系统如类型(... each Y, X).0中下标.0就是伪装的 case 分支且会通过返回类型泄漏回调用方。虽然可行但类型系统复杂度与性能代价惊人且缺乏动机用例故不采用。仓库中的阅读路径提案正文与动机、备选方案全记录proposals/p002240-variadics.md设计正文语法、执行语义、类型检查与形式化附录docs/design/variadics.md提案引用的语言目标与原则docs/project/goals.md、docs/project/principles/library_apis_only.md、docs/project/principles/low_context_sensitivity.md变长参数相关语法在工具链解析器中的落点可围绕 toolchain/parse 目录下的handle_binding_pattern.cpp、node_kind.def等文件继续追踪注文档中标注为 TODO 的部分如...expand、...and/...or的类型检查细节与规范化算法的一般化定义说明该设计仍处于演进中。需要说明的是本提案发布于 Carbon 语言早期阶段文档中多处标注 TODO 与 Future work如 pack 索引、成员 pack、嵌套展开等读者应结合仓库中 docs/design 目录的最新状态与工具链实现判断当前落地程度。【免费下载链接】carbon-langCarbon Languages main repository: documents, design, implementation, and related tools. (NOTE: Carbon Language is experimental; see README)项目地址: https://gitcode.com/GitHub_Trending/ca/carbon-lang创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考