Carbon 函数、函数类型与函数调用规范解析:从 `fn` 声明到 `Call` 接口的统一调用模型

Carbon 函数、函数类型与函数调用规范解析:从 `fn` 声明到 `Call` 接口的统一调用模型 Carbon 函数、函数类型与函数调用规范解析从fn声明到Call接口的统一调用模型【免费下载链接】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 Language 官方设计提案 proposals/p002875-functions-function-types-and-function-calls.md完整讲解 Carbon 中函数声明的类型语义、函数调用语法、直接调用direct call的参数推导流程以及以Call接口为核心的间接调用/可调用对象callable统一模型。通过阅读本文你将掌握函数为什么是无状态且类型唯一的一等公民、绑定方法bound method的类型结构、泛型参数如何通过Call约束为可调用类型、以及用户类如何重载函数调用运算符。文中所有关键语义均结合当前仓库中的编译器实现toolchain/check/call.cpp、toolchain/sem_ir/与预置测试进行交叉印证适合希望深入理解 Carbon 语言语义或参与工具链开发的读者。背景为什么要规范函数与函数调用函数是 Carbon 定义执行语义的基本构件primitive building block但长期以来仓库缺少对函数是什么、函数调用如何工作的正式规范。该提案对应 Pull Request #2875要回答几个悬而未决的问题Carbon 是否拥有一等公民first-class的函数类型类型如何重载函数调用这一操作可调用实体callable entity如何归类——例如参数化类名Vector这样的实体究竟是函数还是别的什么该提案假定读者已经理解 C/C 中函数与函数指针的工作方式在此认知基础上给出 Carbon 自己的答案。值得注意的是提案写作时的诸多设计决策后来已在当前仓库的编译器中落地本文会逐一给出对应的源码位置加以印证。提案总览函数声明引入唯一无状态类型提案的核心主张可以概括为两条函数声明引入一个新且唯一的无状态类型stateless type称为函数类型function type。函数名被绑定为该函数类型的一个值。函数调用采用类 C 的语法一个命名可调用实体的表达式后跟一个形似元组的参数列表。提案将可调用实体划分为以下几类函数以及更一般的函数类型的值绑定方法例如my_vector.BeginLambda待 Carbon 引入之后参数化实体例如泛型类Vector或泛型接口AddWith被约束为可调用的依赖类型的值用户自定义的重载函数调用语法的类类型。在前四类情况下先使用**参数推导argument deduction确定被推导参数的值再使用模式匹配pattern matching**把参数模式绑定到实参值。而在其余情况下则由内置的Call接口决定调用语义调用F(args)被改写desugar为F.(Call(ArgTypes).Op)(args)。注意这个改写结果本身又是一个绑定成员函数调用而Call(ArgTypes)是对参数化实体的调用因此该改写只执行一次。函数与函数类型声明即引入新类型看一个典型的泛型函数声明fn FT:! type - T { return x; }它引入一个名为F的值其类型是唯一的函数类型。不同的函数拥有不同的函数类型即使它们的签名完全相同。函数类型是空且平凡empty, trivial的类型除了询问函数值的类型之外没有其他方式可以命名一个函数类型。函数值是一等公民函数值是普通的regular值可以存入变量、作为参数传递给其他函数// Compile-time function. fn TypeOfT:! type - type { return T; } // ✅ F is a first-class value with a first-class type. let template FType:! type TypeOf(F); var my_f: FType F;这里借助编译期函数TypeOf将函数F的类型捕获到FType随后my_f就能以该函数类型存储F。在另一个函数里可以直接调用它fn G() - i32 { // ✅ my_f has function type FType. This is a direct call to F. return my_f(1); }由于函数类型是唯一的把函数作为泛型参数传递时泛型不会因签名相同而混淆两个不同函数fn Sorttemplate F:! type, T:! type*, f: F); fn Compare(a: i32, b: i32) - Ordering { return a.(Ordered.Compare)(b); } fn SortInts(v: Vector(i32)*) { Sort(v, Compare); }孤儿规则中的归属就孤儿规则orphan rule而言函数类型被视为由引入该函数值的函数声明所声明declared。也就是说函数类型在孤儿规则中的归属方是它对应的函数声明而非其所在的类或包。绑定方法Bound Methods对每个与方法对应的函数类型还存在一个对应的绑定方法类型bound method type。当对类类型的对象执行成员访问以取得某个方法时得到的是一个绑定方法值bound method value其类型即绑定方法类型。语义上绑定方法类型描述的是方法调用中的被调用方callee绑定方法值则描述该调用中self参数的具体取值。示例class HasMember { // HasMember.F has a stateless function type, with signature // self: Self - i32. fn Fself: Self - i32; } fn F(h1: HasMember, h2: HasMember) - i32 { // ✅ h1.F is a bound method value whose type is a bound method type, // with signature (n: i32) - i32. var hf: auto h1.F; // ✅ h1.F and h2.F are of the same bound method type. hf h2.F; // ✅ Same as h2.F(4). return hf(4); }注意两点h1.F与h2.F属于同一种绑定方法类型因此可以互相赋值对该绑定方法值调用hf(4)等价于h2.F(4)即self已绑定为h2。仓库实现印证编译器在语义 IR 层面为绑定方法定义了专门的指令种类与单例类型见 toolchain/sem_ir/inst_kind.def 中的BoundMethod与BoundMethodType以及 toolchain/sem_ir/typed_insts.h 中的结构体BoundMethod持有object_id与function_decl_id。在检查阶段toolchain/sem_ir/function.cpp 的TryGetCalleeAsBoundMethod会从被调用表达式中解包出绑定方法并取出其object_id即self对象与function_decl_id随后调用逻辑据此把self参数重新接回调用——这与提案中绑定方法值描述self参数的语义完全一致。调用语法Call Syntax调用形如a(b, c, d)或a(b, c, d,)其中a是被调用方callee可以是一个名字、字面量、成员访问或是用括号包裹的更复杂表达式b、c、d是任意数量的实参表达式用逗号分隔如果实参列表非空末尾可选地允许一个但不要求尾随逗号。调用语法在句法上等价于一个主表达式后跟一个元组字面量。二者的区别在于元组字面量必须带尾随逗号才能形成单元素元组(b,)而调用语法中a(b)与a(b,)都是合法的。直接调用Direct Calls判定条件与检查流程当被调用方满足以下任一条件时该调用表达式就是直接调用direct call是一个参数化实体的名字例如泛型类或泛型接口具有函数类型或绑定方法类型。直接调用存在一个调用签名call signature用于把给定实参与被调用方声明的隐式参数implicit parameters和显式参数explicit parameters进行核对流程如下参数推导将声明的参数类型与实际实参类型比较推导出让类型相等的隐式参数值然后按顺序处理显式参数列表中的每个绑定binding把所有已推导的参数值代入该参数若参数是template :!绑定实参表达式被转换为与绑定同类型、同模板常量表达式template constant expression阶段若参数是符号化:!绑定实参表达式被转换为与绑定同类型、同符号常量表达式symbolic constant expression阶段否则参数与实参进行模式匹配。如果某参数是:!绑定则其对应的转换后实参表达式会被求值其值会在处理后续参数之前加入已推导参数值列表。仓库实现印证该流程在 toolchain/check/call.cpp 中有着几乎逐条对应的实现ResolveCalleeInCallcall.cpp首先校验实参数目arity是否与显式参数个数一致不一致时报CallArgCountMismatch诊断随后当实体是泛型时调用DeduceGenericCallArguments声明见 toolchain/check/deduce.h执行参数推导转换实参、匹配参数的工作由ConvertCallArgs完成见PerformCallToFunction中对它的调用call.cpp。调用表达式的结果类型调用表达式的结果取决于被调用方的种类若被调用方是参数化实体如泛型类或泛型接口结果是对应的具体实例某个类或接口整个调用是一个类型为type的值表达式若被调用方是函数值调用是一个初始化表达式initializing expression其类型为函数替换后的返回类型求值时调用函数并产生其返回值若被调用方是绑定方法值行为与函数值相同唯一区别是所调用函数的self参数被绑定为绑定方法值中的self值。仓库实现印证ResolveCalleeInCall内部使用EntityKind枚举区分四类被调用实体Function、GenericClass、GenericInterface、GenericNamedConstraintcall.cpp。四种情形各有专门的处理函数泛型类调用Vector(i32)走PerformCallToGenericClass产出ClassTypecall.cpp泛型接口/命名约束调用走PerformCallToGenericInterfaceOrNamedConstaint产出 facet 类型call.cpp函数调用走PerformCallToFunction推导出SpecificFunction或SpecificImplFunction作为具体化后的被调用方并最终构造Call指令call.cpp非函数的被调用方含泛型类型、C 模板名等走PerformCallToNonFunction对不可调用值报CallToNonCallable诊断call.cpp。PerformCall的统一入口会先尝试把被调用方视为函数再回退到非函数路径PerformCallHelpercall.cpp与提案先做直接调用判定、再做改写的设计顺序一致。泛型可调用参数与Call接口泛型参数可以用Call接口约束为可调用类型interface Call(Args: type) { let Result:! type; fn Opself: Self - Result; }TODOCall应当是可变的variadic。目前先把它建模为接收一个元组类型并把Call.Op建模为接收一个元组值。例如排序函数可以这样约束比较器参数fn Sort[T:! type, F:! Call((T, T)) where .Result Ordering] (v: Vector(T)*, cmp: F) {一个非直接调用non-direct call表达式会被改写为对Call(Args).Op的调用其中Args是调用实参元组的类型// In Sort... auto ord: auto cmp((*v)[i], (*v)[j]); // ... is translated into ... auto ord: auto cmp.(Call((T, T)).Op)((*v)[i], (*v)[j]);设计上有两个关键约束值得注意调用实参的类型被建模为接口参数interface parameter这使得Call接口能够建模函数重载返回类型是关联类型associated type而非参数——Carbon不允许按返回类型重载也不希望类型信息从调用表达式出现的上下文向内传播到调用本身。函数类型与Call的关系函数类型对Call的自动实现每个函数类型或绑定方法类型对直接调用该函数/绑定方法可接受的所有运行时实参类型集合实现Call接口。Call.Op的行为就是使用给定实参列表去调用该函数或绑定方法。以Select为例编译器相当于为其生成了下面的实现fn SelectT:! type - T { return if b then x else y; } // Generated: impl forall [T:! type, BType:! ImplicitAs(bool)] Select as Call((BType, T, T)) where .Result T { fn Opself: Self) { let (b: bool, x: T, y: T) args; return Select(b, x, y); } }对于不涉及推导参数的类型允许隐式转换。其意图是让impl在函数支持直接调用的同样情形下支持间接调用且语义一致。例如i64函数可以被当作接受i32的可调用对象传入fn TakeI32FnF:! Call(i32); fn I64Fn(n: i64); fn Run() { // ✅ I64Fn can be called with an i32, because // i32 impls ImplicitAs(i64). TakeI32Fn(I64Fn); }局限与边界情形通过Call接口发起的调用实参一律作为值表达式value expressions调用本身一律是初始化表达式initializing expression。如果函数用var接收参数或未来语言机制允许直接函数调用不是初始化表达式就需要额外的转换在这些转换不可行时例如实参或返回值不可拷贝Call接口被视为未被实现。提案预期在未来提案中重新审视这些约束把函数调用接口扩展为与fn声明同等的一般性。另外Call接口只建模给定参数类型的任意运行时值都能传入的函数调用。只要函数签名的显式实参列表中含编译期参数或者调用实参与函数参数之间存在可反驳的模式匹配refutable pattern matching该函数类型就不会实现Callfn RuntimeT:! type; fn CompileTime(T:! type, x: T); // Undecided whether this is valid. fn RefutablePattern(1 as i32); fn Run() { // ✅ Calls Runtime(0). Runtime.(Call(i32).Op)(0); // ❌ Cant call CompileTime this way, it cant implement Call(type, i32) // because the type would be passed at runtime. CompileTime.(Call(type, i32).Op)(i32, 0); // ❌ Cant call RefutablePattern this way, it cant implement Call(i32) // for arbitrary i32 arguments. RefutablePattern.(Call(i32).Op)(0); }重载调用运算符Overloaded Call Operator用户可以通过为类型实现Call接口来重载函数调用运算符的含义class Func(Arg:! type) { impl as Call((Arg,)) where .Result () { fn Opself: Self) { Print(hello, world); } } } fn Run() { let f: Func(i32) {}; // ✅ Prints hello, world. f(42); }对被调用方的类型没有任何额外约束只需满足实现接口的常规约束即可。下面的例子甚至展示了为单字段结构体字面量实现调用运算符——合法但明显不推荐class X { var n: i32; } // ✅ OK, but inadvisable. impl {.a: X} as Call(()) where .Result i32 { fn Opself: Self) - i32 { return self.a.n; } } fn Run() - i32 { // Returns 1. return {.a {.n 1} as X}(); }编译期参数的绕行方案Carbon 无法直接定义接收编译期参数的函数式可调用类类型——这与 C 中无法为类类型的值x定义operator()使xT()在编译期把T传给operator()的情形类似。但可以通过把编译期值放入实参的类型中来绕行class Wrap(T:! type) {} class Callable { impl forall [T:! Printable] as Call((Wrap(T), T)) where .Result () { fn Opself: Self, T)) { let (_: auto, v: T) args; Print(v); } } } fn CallItF:! Call(Wrap(i32), i32) { f({} as Wrap(i32), 0); } fn Run() { CallIt({} as Callable); }设计依据Rationale提案所依据的项目原则与目标如下原则层面原则唯一的静态开放扩展机制one static open extension mechanism重载调用通过实现接口来支持原则倾向为同一件事只提供一种方式one way把可调用对象传给泛型函数只有一种显然的方式并且无论实参是函数、可调用对象还是未来的lambda它都能高效工作。目标层面性能关键的软件Performance-critical software函数传递的效率不低于可调用对象的传递易读、易懂、易写的代码C/C 的签名式函数类型出了名地难读Carbon 通过不引入签名式函数类型来规避该问题与现有 C 代码互操作与迁移该设计为函数指针打下了基础见下文未来工作C 函数指针与成员函数指针都可以建模为实现了Call的值。备选方案Alternatives Considered方案一签名式函数类型可以让每个函数签名拥有独立类型如 C/C 那样这能为函数指针提供一个不依赖泛型、类型擦除或花哨表示优化的故事。但其主要缺点在 C 中屡见不鲜把函数传给模板泛型时效率低于把函数对象传给同一模板。例如 C 中std::vectorint v; bool cmp(int a, int b) { return simple_calculation(a, b); }调用ranges::sort(v, cmp)可能远不如ranges::sort(v, [](int a, int b) { return cmp(a, b); })高效——后者是直接调用前者传入函数指针导致间接调用。让函数产生唯一类型意味着对函数、lambda 与函数式对象的调用具有更相似的语义和接近的效率属性。方案二让直接调用与间接调用行为统一让直接调用与间接调用完全一致在理论上更理想但实际并不可行原因有三间接调用需要被改写为另一个调用表达式——语义的根基终究必须建立在某处的一次真实函数调用之上而不是无穷地把一个调用改写为另一个项目已选定接口作为唯一的静态开放扩展机制任何通用的函数调用重载机制都必须落在interface/impl的边界之内若试图在impl查询中标注每个实参是否可编译期传递这样的查询必须能回退到实参运行时传递可能导致寻找匹配impl时出现指数级搜索。未来工作Future Work函数重载Overloading重载在提案范围之外但设计须为重载预留合理路径。重载函数拥有多个不同的签名隐式与显式参数集合在提案框架下意味着一组重载函数引入单一的函数类型该类型支持以不同方式被调用。重载选择可能采用逐一检查候选命中即止或检查全部再择优两种策略当前意图是前者。对应到本提案一个重要后果是重载函数集合对应的函数类型拥有Call(...)接口的多个参数化实现具体调用所选中的实现必须遵循重载机制选出的规则。例如若按首个匹配选择下面的占位语法overloaded fn AbsT:! Unsigned - T { return x; } overloaded fn AbsT:! Floating - T { return x 0 ? -x : x }会被合成为如下行为match_first { impl forall [T:! Unsigned] Abs as Call((T,)) where .Result T { ... } impl forall [T:! Floating] Abs as Call((T,)) where .Result T { ... } }对于同时满足Unsigned Floating的类型前者被选中与重载选择行为一致。按表达式类别与阶段重载值得考虑是否允许按被调用方与实参的表达式类别expression category与表达式阶段expression phase重载调用对编译期实参或许还希望按常量值重载。其他语言已有先例Rust 提供Fn、FnMut、FnOnce区分可调用对象传入重载调用时的不同方式C 允许operator()把*this以const、非const甚至接收。Carbon 可以仿照 Rust 提供多个Call接口处理不同self参数种类建模方式类似下标所用的IndexWith与IndirectIndexWith接口。更通用、更宏大的方案是把调用签名作为调用接口的一部分来描述impl MyCallable as call(T:! type, x: T) - T;它可能是某种以接口表达签名的简写语法也可能是一种新的一等语言原语。相关探索正在 issue #3154 中进行本提案写作时。可变参数Variadics待 Carbon 支持可变参数后重载调用机制应改用可变参数而非用元组类型近似可变参数列表。Lambda预期 lambda 在本提案模型下与函数行为基本一致主要区别是 lambda 可以有状态stateful而函数无状态。函数指针Function Pointers出于与 C 互操作以及廉价存储无状态函数引用的目的应当支持函数指针。函数指针可建模为alias FunctionPtr(Args:! type, Result:! type) DynPtr(Stateless Call(Args) where .Result Result);其中Stateless是描述空、无身份概念、实例可平凡创建与销毁类型的接口DynPtr是对给定 facet 类型执行类型擦除与运行时派发的类型DynPtr带优化DynPtr(Stateless I)在I是恰好含一个关联函数的接口时表示为指向该函数的指针。仓库中的实现现状与延伸阅读本提案的大部分核心设计已在当前仓库编译器落地可作为继续深入阅读的入口调用检查主逻辑toolchain/check/call.cpp ——PerformCall/PerformCallHelper统一入口、ResolveCalleeInCall参数推导与 arity 校验、四类实体的调用处理PerformCallToFunction、PerformCallToGenericClass、PerformCallToGenericInterfaceOrNamedConstaint、PerformCallToNonFunction参数推导toolchain/check/deduce.hDeduceGenericCallArguments的声明绑定方法在语义 IR 中的表示toolchain/sem_ir/typed_insts.h 的BoundMethod/BoundMethodTypetoolchain/sem_ir/inst_kind.def以及 toolchain/sem_ir/function.cpp 的TryGetCalleeAsBoundMethod调用相关的设计文档docs/design/generics/details.md孤儿规则、动态类型、docs/project/principles/static_open_extension.md、docs/project/goals.md。需要说明的是Carbon 语言目前仍处于实验性阶段参见 README.md本文描述的语义以当前仓库的实现与上述提案为准Call的可变参数化、按表达式类别/阶段重载等能力仍在探索中后续提案可能进一步演进。【免费下载链接】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),仅供参考