Carbon 语言 Lambda 与函数声明的连续语法设计(proposals/p003848-lambdas 深度解析)
【免费下载链接】carbon-langCarbon Language's main repository: documents, design, implementation, and related tools. (NOTE: Carbon Language is experimental; see README)项目地址: https://gitcode.com/GitHub_Trending/ca/carbon-lang
本篇技术指南以 Carbon Language 仓库中的正式设计提案 proposals/p003848-lambdas.md 为核心,系统讲解 Carbon 语言中 lambda 表达式与函数声明统一语法(fn引入符、=>简洁表达式体、$0位置参数、捕获与函数字段)的完整设计;同时结合 toolchain/parse/handle_lambda.cpp、toolchain/parse/testdata/lambda/lambda.carbon 等实现源码,说明该语法在编译器前端中的落地现状。读完本文,你将掌握 Carbon 中"从 lambda 到函数声明"的连续语法全貌,理解捕获、位置参数等核心机制的语义约束,并能对照源码定位对应的解析实现与测试用例。
背景与设计动机
Carbon 是实验性语言,其设计目标之一是Code that is easy to read, understand, and write(见 docs/project/goals.md 中的对应章节),同时强调与现有 C++ 代码的互操作与迁移(Interoperability with and migration from existing C++ code)。在 C++ 中,lambda 在使用点定义、通常匿名,如果迁移工具只能将 C++ lambda 机械替换为具名函数声明,就必须为每个 lambda 挑选名字,造成显著的可用性负担。因此,Carbon 需要一种原生的 lambda 语法来承接 C++ lambda 的迁移场景。
提案参考了三种以"捕获(capture)"为核心的语言特性——lambda 因此拥有状态和生命周期:
- C++ 的 lambda 表达式(
[capture] (params) -> ret { body }); - Swift 的闭包(closures)及其 Shorthand Argument Names(简写参数名,即
$0/$1); - Rust 的闭包。
这三者与 Carbon 方向最接近的共同点,是"捕获使函数对象持有状态、具备生命周期"。
本提案的核心主张是:lambda 与函数声明之间采用大体连续(continuous)的语法——两者都用fn引入,是否带名字(name)是区分"函数声明"与"lambda 表达式"的唯一关键,其余语法对两类函数完全通用。
语法概览:lambda 与函数声明的对照
提案首先给出"lambda 写在变量里 / 用在函数调用中"与"函数声明"两组对照示例。
简洁表达式体(=>,返回类型推断为auto):
// 变量中: let lambda: auto = fn => T.Make(); // C++23 等价:const auto lambda = [] { return T::Make(); }; // 函数调用中: Foo(10, 20, fn => T.Make()); // C++23 等价:Foo(10, 20, [] { return T::Make(); });显式返回类型 + 语句体:
// 变量中: let lambda: auto = fn -> T { return T.Make(); }; // C++23 等价:const auto lambda = [] -> T { return T::Make(); }; // 函数调用中: PushBack(my_list, fn => T.Make()); // C++23 等价:PushBack(my_list, [] { return T::Make(); });函数声明同样支持上述两种形式:
fn FunctionDeclaration => T.Make(); // C++23 等价:auto FunctionDeclaration() { return T.Make(); } fn FunctionDeclaration -> T { return T.Make(); } // C++23 等价:auto FunctionDeclaration() -> T { return T::Make(); }提案强调:"返回表达式的函数,其返回类型为auto"。以下组合全部成立:
fn => T.Make()/fn FunctionDeclaration => T.Make()—— 返回表达式,返回类型auto;fn -> T { return T.Make(); }/fn FunctionDeclaration -> T { return T.Make(); }—— 显式返回类型 + 语句体;fn { Print(T.Make()); }/fn FunctionDeclaration { Print(T.Make()); }—— 有语句体但无返回值(相当于 C++23 的[] -> void { ... })。
在函数体内部,lambda 与函数声明还可以使用方括号承载隐式参数:支持捕获(captures)、函数字段(fields)与推导参数(deduced parameters)。对位于类或接口定义内部的函数声明,方括号中还可以加入self: Self或addr self: Self*:
fn Foo(x: i32) { // 变量中: let lambda: auto = fn [var x, var y: i32 = 0] { Print(++x, ++y); }; // C++23 等价:const auto lambda = [x, y = int32_t{0}] mutable -> void { Print(++x, ++y); }; // 函数调用中: Foo(fn [var x, var y: i32 = 0] { Print(++x, ++y); }); fn FunctionDeclaration[var x, var y: i32 = 0] { Print(++x, ++y); } }函数还支持在使用点定义的"位置参数(positional parameters)":用美元符号加非负整数表示(如$0),隐式类型为auto:
fn Foo() { let lambda: auto = fn { Print($0); }; // C++23 等价:auto lambda = [](auto _0, auto...) -> void { Print(_0); }; // Swift 等价:let lambda = { Print($0) }; fn FunctionDeclaration { Print($0); } }函数当然也可以使用具名参数,但同一函数不能同时混用具名参数与位置参数:
fn Foo() { // 变量中: let lambda: auto = fn (v: auto) { Print(v); }; // 函数调用中: Foo(fn (v: auto) { Print(v); }); fn FunctionDeclaration(v: auto) { Print(v); } }在"具名参数或位置参数"之外,推导参数(deduced parameters)始终允许:
fn Foo() { let lambda: auto = fn T:! Printable { Print(t); }; fn FunctionDeclarationT:! Printable { Print(t); } }语法正式定义
函数定义与 lambda 表达式具有以下两种句法形式(方括号内的项均为可选项且彼此独立):
fn [name] [implicit-parameters] [tuple-pattern] => expression [;] fn [name] [implicit-parameters] [tuple-pattern] [-> return-type] { statements }其中,第一种形式是第二种的简写:=> expression ;等价于-> auto { return expression; }。
关于各组成部分的语义:
implicit-parameters:由方括号括起,内部为可选的默认捕获模式,以及任意数量的显式捕获、函数字段与推导参数,全部以逗号分隔。默认捕获模式(如有)必须位于首位,其余项顺序不限。若省略implicit-parameters,等价于空[]。name是否出现,决定这是函数定义还是 lambda 表达式。第一种形式中函数定义必须带尾部;,而该分号不属于 lambda 表达式语法的一部分。tuple-pattern是否出现,决定函数体使用具名参数还是位置参数。-> return-type是否出现,决定函数体能否(且必须)返回值。
为了让"连续语法"直观可读,提案给出了一张"句法位置"对照表,将两个语法簇的每种合法组合一一列出:
| 句法位置 | 该位置允许的语法(除特别说明外均为可选) |
|---|---|
| A1 | 必需返回表达式(允许位置参数) |
| A2 | 必需返回表达式(禁止位置参数) |
| B | 默认捕获模式 |
| C | 显式捕获、函数字段与推导参数(任意顺序) |
| D | 显式参数 |
| E1 | 语句体(无返回值,允许位置参数) |
| E2 | 语句体(有返回值,允许位置参数) |
| E3 | 语句体(无返回值,禁止位置参数) |
| E4 | 语句体(有返回值,禁止位置参数) |
| F | 必需返回类型 |
| G | 函数声明名称 |
对应的lambda 合法形态(全部出现在表达式上下文中,自身即为表达式):
fn => A1 fn [B, C] => A1 fn (D) => A2 fn B, C => A2 fn { E1; } fn -> F { E2; } fn [B, C] { E1; } fn [B, C] -> F { E2; } fn (D) { E3; } fn (D) -> F { E4; } fn B, C { E3; } fn B, C -> F { E4; }对应的函数声明合法形态(允许作为函数体中的语句,或其他作用域中的声明):
fn G => A1; fn G[B, C] => A1; fn G(D) => A2; fn GB, C => A2; fn G { E1; } fn G -> F { E2; } fn G[B, C] { E1; } fn G[B, C] -> F { E2; } fn G(D) { E3; } fn G(D) -> F { E4; } fn GB, C { E3; } fn GB, C -> F { E4; }可见,除"名字 G + 尾部;"之外,lambda 与函数声明共享完全相同的句法骨架,这正是"连续语法"的含义所在。
引入符:统一使用fn
提案规定,lambda 与函数声明统一用fn关键字引入,以镜像函数声明。判定规则为:
- 若一条语句或声明以
fn开头,则必须提供名字,成为函数声明; - 否则,若处于表达式上下文,
fn即引入一个 lambda。
let lambda1: auto = fn => T.Make(); let lambda2: auto = fn -> T { return T.Make(); }; fn FunctionDeclaration1 => T.Make(); fn FunctionDeclaration2 -> T { return T.Make(); }这一设计在编译器前端已经落地:解析器中LambdaIntroducer节点对应的正是fn词元(见 toolchain/parse/handle_lambda.cpp 中的HandleLambdaIntroducer)。解析状态机依次经历LambdaAfterIntroducer→(可选)LambdaAfterImplicitParams→LambdaAfterParams→LambdaBody/LambdaBodyFinish,最终产出NodeKind::Lambda节点。测试数据 toolchain/parse/testdata/lambda/lambda.carbon 中的basic.carbon用例展示了三种基础形态的解析树输出:
var a: auto = fn [T: type] (x: T) -> T { return x; }; var b: auto = fn (x: i32) => x; var c: auto = fn { };其中var b一行在解析树中同时出现LambdaIntroducer 'fn'与TerseBodyArrow '=>'节点,直观印证了"=>简洁体"在实现层面是独立语法单元。同时,测试还覆盖了错误恢复路径:当 lambda 缺少函数体(var x: auto = fn ;)或返回类型之后缺少=>/{(var x: auto = fn -> i32 ;)时,解析器分别报告ExpectedLambdaBody与ExpectedLambdaBodyAfterReturnType诊断,并插入InvalidParse节点以保持 lambda 是合法表达式,避免崩溃。
位置参数(Positional Parameters)
位置参数在函数体内通过美元符号加非负的位置整数引入(如$3),它们是所在函数的auto参数,灵感来自 Swift 的 Shorthand Argument Names。要点如下:
- 可被用于**任何缺少显式参数列表(圆括号)**的 lambda 或函数声明;
- 天然可变参(variadic):缺少显式参数列表的函数可以接收任意数量的实参;
- 函数体只读取其命名的参数,最高编号的参数决定了所需的最少实参数;
- 函数体可以自由省略低编号的参数(例如
fn { Print($10); })。
典型用法是作为比较器:
// 接收两个位置参数、用作比较器的 lambda Sort(my_list, fn => $0.val < $1.val); // Swift 等价:{ $0.val < $1.val }位置参数的限制
提案对带位置参数的函数施加两条限制:
- 函数声明的定义必须附着在声明上(即不能前置声明后再另行定义);
- 位置参数只能用于"恰好只有一个无显式参数列表的外层函数"的上下文中。
以下示例逐条演示了合法性判定:
fn Foo1 { fn Bar1 {} // ❌ 无效:Foo1 已在使用位置参数 } fn Foo2 { Print($0); fn Bar2 {} // ❌ 无效:Foo2 已在使用位置参数 } fn Foo3 { fn Bar3 { Print($0); // ❌ 无效:Foo3 已在使用位置参数 } } fn Foo4() { fn Bar4 { Print($0); // ✅ 有效:Foo4 有显式参数 } } fn Foo5 { fn Bar5() {} // ✅ 有效:Bar5 有显式参数 } fn Foo6() { my_list.Sort( fn => $0 < $1 // ✅ 有效:Foo6 有显式参数 ); }函数捕获(Function Captures)
函数捕获镜像 C++ 的非初始化捕获(non-init captures)。一条捕获声明由捕获模式(针对var捕获)后跟外层作用域中某个绑定的名字组成,使该标识符在内部函数体中可用;捕获的生命周期等于其所处函数的生命周期。
例如,捕获外层let handle后传给线程:
fn Foo() { let handle: Handle = Handle.Get(); var thread: Thread = Thread.Make(fn [var handle] { handle.Process(); }); thread.Join(); }也可以用带名字的函数声明版本:
fn Foo() { let handle: Handle = Handle.Get(); fn MyThread[handle]() { handle.Process(); } var thread: Thread = Thread.Make(MyThread); thread.Join(); }捕获模式(Capture Modes)
let与var都可以作为捕获模式出现,其行为与普通绑定一致:
let捕获:按值捕获,捕获副本不可变;var捕获:按对象捕获(by-object),捕获副本可变,修改作用于副本。
为防止歧义,捕获只能存在于"定义附着于声明"的函数上:lambda(始终在表达式上下文)以及直接定义在另一函数体内的函数声明(语句上下文)可以捕获;前置声明的函数不支持捕获,类成员(允许self: Self之处)也不支持捕获。
捕获模式既可用作默认捕获模式说明符,也可用于显式捕获。显式捕获示例:
fn Example { var a: i32 = 0; var b: i32 = 0; let lambda: auto = fn [a, var b] { a += 1; // ❌ 无效:按值捕获不可变 b += 1; // ✅ 有效:修改按对象捕获的副本 }; lambda(); }生命周期约束的典型案例——被返回的 lambda 不能引用已死亡的变量:
fn Example { fn Invalid() -> auto { var s: String = "Hello world"; return fn [s]() => s; } // ❌ 无效:返回的 lambda 引用的 `s` 在 lambda 被调用时已不再存活。 Print(Invalid()()); }提案还特别说明了一个可变状态调用约定:如果函数对象 F 具有可变状态(无论是按对象捕获还是按对象函数字段),那么对 F 的调用要求被调用方是引用表达式而非值表达式——因为我们需要一个可变句柄才能修改 F 的可变状态。
默认捕获模式(Default Capture Mode)
默认情况下,函数不做任何捕获:没有方括号等价于空方括号[]。用户通过显式捕获或更简洁的默认捕获模式选择捕获行为。默认捕获模式大致对应 C++ 的[=]与[&],在方括号内作为首个元素出现:
fn Foo1() { let handle: Handle = Handle.Get(); fn MyThread[var]() { handle.Process(); // 由于默认捕获模式为 `var`,`handle` 被按对象捕获 } var thread: Thread = Thread.Make(MyThread); thread.Join(); } fn Foo2() { let handle: Handle = Handle.Get(); fn MyThread[let]() { handle.Process(); // 由于默认捕获模式为 `let`,`handle` 被按值捕获 } var thread: Thread = Thread.Make(MyThread); thread.Join(); }函数字段(Function Fields)
函数字段镜像 C++ 的初始化捕获(init captures)。一条函数字段定义由不可反驳模式(irrefutable pattern)、=与初始化器组成;函数定义被求值时,模式与初始化器进行匹配。模式中的绑定与函数具有相同的生命周期,其作用域延伸到函数体末尾。
与捕获相同,函数字段也只能存在于"定义附着于声明"的函数上(lambda、函数体内的函数声明),不支持前置声明与类成员。
fn Foo() { var h1: Handle = Handle.Get(); var h2: Handle = Handle.Get(); var thread: Thread = Thread.Make(fn [a: auto = h1, var b: auto = h2] { a.Process(); b.Process(); }); thread.Join(); }拷贝语义(Copy Semantics)
为镜像 C++ 行为,函数声明与 lambda 的可拷贝性由其所包含的函数字段与函数捕获决定:
- 若函数持有按对象(by-object)函数字段,且该字段类型可拷贝,则包含它的函数也可拷贝——对捕获同理;
- 另一类是按值(by-value)函数字段:类比 C++ 中 const 引用作为类字段会阻止该类进行拷贝赋值,按值函数字段会阻止其所处函数进行拷贝赋值。
Self 与递归
镜像 C++ 通过捕获this的做法,self应始终作为来自外层作用域的捕获出现:
- lambda 上永远不允许
self: Self; - 函数声明仅在作为类类型或接口的成员时允许
self: Self(此时它指向类/接口,而非函数自身)。
提案还引用了一个相关方向(PR #3720)的推论:形如x.(F)的表达式——其中 F 是带self或addr self参数、绑定为x的函数——产生的可调用对象持有x的值而非F的值。因此,捕获和函数字段无法与self参数组合使用。
设计理由(Rationale)
Carbon 的 lambda 服务于两个目的:
- 支持"代码易于阅读、理解和编写"目标:为此才引入
=>返回表达式语法,以及以"省略显式参数元组 + 函数体内使用$N"为特征的位置参数; - 支持"与现有 C++ 代码互操作并迁移"目标:C++ lambda 在使用点定义、通常匿名,若迁移时只允许替换为具名函数声明,迁移工具就不得不为每个 lambda 取名,产生可用性负担。
备选方案与取舍
备选一:简洁 vs 详尽(Terse vs Elaborated)
此前的讨论曾考虑将函数分为三类:简洁 lambda(terse lambdas)、详尽 lambda(elaborated lambdas)与函数声明。但分类带来了句法悬崖(syntactic cliff):
- 简洁 lambda 计划最小化语法,授予隐式默认捕获模式——没有方括号就允许按值捕获,配合 sigil 引入符形成:
let zero: i32 = 0; let list_all: List(i32) = GetAllValues(); let list_positive: List(i32) = list_all.Filter( @ $0 > zero );- 但详尽 lambda 为引入方括号与显式参数付出了更多语法代价,还制造了悬崖:既然期望"空方括号 = 禁用捕获",而无方括号形态又需要支持捕获,详尽 lambda 就必须同时加上方括号和显式默认捕获模式才能维持既有捕获行为,结果代码变成:
let list_positive: List(i32) = list_all.Filter( @let x > zero );- 若再把 lambda 升级为函数声明,又会遇到"从 sigil 切换到
fn关键字 + 添加名字"的又一处悬崖。
最终结论:连续语法是更优路径,即便最短的 lambda 拼写会比备选方案略长。
备选二:Sigil 引入符
曾考虑用$或@等 sigil 引入 lambda。由于引入符标点本身就是稀缺资源、对选择哪个 sigil 缺乏共识,且希望 lambda 与函数声明保持连续语法,该方案被否决。若采用会形如:
let lambda1: auto = @ => T.Make(); let lambda2: auto = @[]() -> T { return T.Make(); };备选三:额外位置参数限制
除上述两条限制外,还考虑过将带位置参数的函数可见性限制在非公共接口内。该方案经由 leads 问题讨论后被否决,推测此类限制更适合以 HOA(head of agreement/规则约定)方式约束,而非由编译器报错强制。
备选四:递归 Self
为支持递归,曾考虑在所有函数与 lambda 上允许self: Self并指向函数自身。但这会在类成员与非类成员之间制造不连续性,故被否决。
未来工作:引用捕获(Reference Captures)
关于按引用捕获的语义影响已有大量讨论。目前这类行为并非通过捕获实现,而是借助"由外层作用域中对象的地址构造的函数字段"来间接支持。提案强调,必须在该领域投入更多工作,以解决当前方案的可用性痛点。
附:编译器实现现状
从源码结构看,lambda 语法已在前端解析阶段完整落地,而语义检查阶段仍为待办:
- 解析实现位于 toolchain/parse/handle_lambda.cpp:状态机覆盖
fn引入、隐式/显式参数列表、->返回类型、=>简洁体与{ }语句体,并在缺体场景下产生ExpectedLambdaBody/ExpectedLambdaBodyAfterReturnType诊断; - 解析测试数据位于 toolchain/parse/testdata/lambda/lambda.carbon:包含正常形态的解析树断言(如
LambdaIntroducer、TerseBodyArrow节点)与错误恢复断言,可用bazel test //toolchain/testing:file_test --test_arg=--file_tests=toolchain/parse/testdata/lambda/lambda.carbon单独运行验证; - 检查阶段 toolchain/check/handle_lambda.cpp 中
HandleLambdaIntroducer、HandleLambda、HandleTerseBodyArrow目前均返回context.TODO(...),说明本提案所描述的捕获、函数字段、位置参数、拷贝语义等语义检查语义尚未在toolchain/check中实现,本文的语义描述以设计提案为准。
需要说明的是,上述结论均为对当前仓库快照的分析:lambda 的解析语法已可验证,而语义层行为仍停留在设计层面,阅读源码时请注意这一阶段性差异。
【免费下载链接】carbon-langCarbon Language's 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),仅供参考