Mojo 可变参数(Variadics)完整指南:从参数列表、VariadicList 到 VariadicPack 与关键字参数
【免费下载链接】mojoThe Modular Platform (includes MAX & Mojo)项目地址: https://gitcode.com/GitHub_Trending/mo/mojo
本指南以 Mojo/proposals/variadics-design.md 为骨架,系统讲解 Mojo(Modular 平台)中可变参数特性的设计、语法与底层实现:编译期可变参数列表(TypeList/ParameterList)、运行时同构可变参数(VariadicList)、异构可变参数包(VariadicPack)以及可变关键字参数(StringDict,设计文档中写作OwnedKwargsDict)。读完本文,你将掌握如何在函数签名中使用*args、*args: *Ts与**kwargs,理解其借用/所有权语义、低层 lowering 方式,并能结合标准库源码与测试用例写出惯用的元编程与转发代码。
概述:Mojo 可变参数的四大形态
Mojo 的可变参数(variadic)能力横跨编译期与运行期两条主线:
| 形态 | 语法示例 | 背后类型 | 作用域 |
|---|---|---|---|
| 编译期类型列表 | *Ts: AnyType | TypeList | def/struct/comptime参数位 |
| 编译期值列表 | *elts: Int | ParameterList | 同上 |
| 运行时同构参数 | *args: Int | VariadicList | 运行时调用点 |
| 运行时异构参数包 | *args: *Ts | VariadicPack | 运行时调用点 |
| 运行时关键字参数 | **kwargs: Int | StringDict[V](文档中的OwnedKwargsDict[V]) | 运行时调用点 |
这些特性统一采用 Python 风格的语法(*args与**kwargs),对熟悉 Python 的用户有天然的亲切感。编译期参数列表把静态已知数量的元素打包进一个绑定;运行时同构参数允许单个参数位接收任意数量的"同形状"实参;异构参数包用*args: *Ts语法把一组可变参数与一组可变类型一一对应;关键字参数则收集未被命名参数消费的key=value操作数。本文档当前状态为 Draft,是面向语言/标准库贡献者与进阶用户的实现级参考。
本文所有示例均可对照标准库测试 Mojo/stdlib/test/builtin/test_variadic.mojo(该文件是语法最新真相的来源)与核心实现 Mojo/stdlib/std/builtin/variadics.mojo。
Variadic parameter lists:编译期参数列表
可变参数(parameter)列表出现在编译期参数位置(def、struct、comptime等声明上),把静态已知数量的元素捆绑进一个绑定。与 Python 相同,Mojo 用前导*表示一组值或类型:
def takes_types[*Ts: AnyType](): ... def takes_values[*elts: Int](): ...Mojo 严格区分这两种情况,标准库分别以TypeList与ParameterList呈现(均定义在 Mojo/stdlib/std/builtin/variadics.mojo)。两者建立在同一个 KGEN 概念之上:一个 MLIR!kgen.param_list值,其元素共享同一种编译期形态(要么全部满足某个 trait 的类型,要么全部是同一种类型的值)。它们是内建类型,正常使用无需显式导入——模块头部注释即声明:"These are Mojo built-ins, so you don't need to import them."
TypeList:类型序列的编译期操作
类型列表绑定一系列类型,声明时使用 trait 约束而非值类型,例如*Ts: AnyType或*Ts: Movable。编译器通过TypeList操作将其暴露出来:对上面的示例,type_of(Ts)就是TypeList。从源码看,TypeList底层类型正是_MLIR.KGENParamListType[Self.Trait],即!kgen.param_list<Trait>(见 variadics.mojo),并借助#kgen.param_list.size、#kgen.param_list.get等 MLIR 属性实现查询与索引。
TypeList提供一系列实用操作:
- 编译期
size/length:元素个数,如tl.length; - 定长索引:
Ts[i](可用于comptime循环,通过__getitem_param__[idx]实现); - 构造与变换助手:
TypeList.of、splat、tabulate、map、reduce、filter_idx、contains、reversed、slice等。
典型用途包括:对多个类型参数做 trait 谓词判断(参见 Mojo/stdlib/std/traits/movable.mojo),以及遍历/变换类型包的元编程。测试用例 test_variadic.mojo 中的test_type_list_map_to_values、test_type_list_filter_idx_*、test_type_list_reduce_idx等覆盖了这些操作的常见组合。
ParameterList:同类型编译期值序列
值列表绑定一系列共享同一类型T的编译期值,例如*args: Int或*names: String。它变成一个元素类型为T的ParameterList。可以编译期迭代或索引;ParameterList.get_span()在需要指针线性布局时,把元素物化为连续常量数组背后的Span——源码显示它通过#pop.variadic_to_array把param_list展平成扁平数组、再映射为运行期常量并取首元素指针(见 variadics.mojo)。ParameterList.of、splat等构造器与TypeList的故事一一对应。
两个类型刻意保持平行设计(文档注释表示未来在 Mojo metatype 故事成熟后有望统一):TypeList.map_to_values把每个元素类型经编译期生成器映射为同构值的ParameterList——"类型包驱动值包"是常见模式。测试 test_variadic.mojo 的test_type_list_map_to_values正是此用法。
需要注意的是:与参数列表不同,参数列表目前不支持异构列表或关键字参数列表,文档明确表示"若未来有足够需求支撑其复杂度,可能加入"。
Homogeneous variadic arguments:运行时同构可变参数
上一节讨论的是声明上的参数;本节讨论运行时调用点的实参:一个参数位接受任意数量的实参,但所有实参必须是同一类型T。表面语法同样是前导*,但现在它命名的是一个VariadicList(而非ParameterList)。
签名与基本用法
同构可变参数形如*args: Int或*parts: String。在 callee 内部,args可以像一个小型顺序集合使用:len(args)、args[i]、for x in args;当T满足Writable时还有args.write_to(s)之类助手来渲染元组形态:
def print_ints(*values: Int): for i in range(len(values)): print(values[i]) def main(): print_ints(10, 20, 30) # 三个独立实参,一个可变参数绑定只关心值不关心索引时,迭代器路径同样自然:
def sum_ints(*values: Int) -> Int: var total = 0 for v in values: total += v return totalcallee 收到的是VariadicList
*args: T语法映射到标准库类型VariadicList(variadics.mojo)。它的内部字段是:
var _value: Span[Self._EltPointerType, ImmUntrackedOrigin] # 元素指针的 Span也就是说,它携带的是一个指向各实参引用的指针的Span,而不是T值的密集数组。这种布局让 callee 能以args[idx]暴露正确的借用或移动语义(__getitem__通过self._value.unsafe_ptr()[unsafe_offset=idx][]解引用),同时让胶水对象小到足以按值传递,还允许非Movable的值通过可变参数传递。文档给出的粗略 lowering 图景(仅为示意名,非精确 MLIR):
// Caller: foo(a, b, c) with def foo(*xs: T) 1. 编译器照常为每个实参分配存储(栈槽、寄存器或 ABI 要求的形式)。 2. 构造一个临时数组,其第 i 项是指向第 i 个实参的 pointer-to-ref (所有元素的指针/引用 MLIR 类型相同)。 3. 调用 foo,传入一个 VariadicList:其内部 Span 指向该数组,长度为实参个数。 4. VariadicList.__init__(由该数组隐式构造)把 POP 数组转成元素指针的 Span; __getitem__ 通过该 span 索引并加载引用。源码佐证了第 4 步:隐式__init__[size, container_origin]接收编译器生成的元素指针数组引用,用pop.array.gep取首元素指针后unsafe_bitcast为元素指针类型,再包成Span(unsafe_ptr=..., length=size)(见 variadics.mojo)。
所以可变参数束永远是指针的 span——即使T是 trivial 类型也是如此。阅读性能文档时需记住:可变参数在 caller 侧仍是独立对象,列表只是一层用于统一索引的间接层。
借用与拥有实参
默认情况下*args: T借用每个元素。当需要取得所有权时(例如T是线性类型,或想用consume_elements移出),使用var *args: T。VariadicList用is_owned参数跟踪该状态;置位时__deinit__反向遍历列表、逐个销毁元素,与常规实参析构顺序一致(见 variadics.mojo):
@__parameter def destroy_elem(_idx: Int, var arg: ExplicitDelOnly): arg^.destroy() def take_owned_linear(var *args: ExplicitDelOnly): args^.consume_elements[destroy_elem]() # Caller 传临时对象;callee 逐个消费。 take_owned_linear(ExplicitDelOnly(5), ExplicitDelOnly(10))该示例直接取自 test_variadic.mojo 的test_variadic_list_linear_type。consume_elements调用处的^选择可变参数束的 owned 视图。
consume_elements与想要var元素的 API
许多"下沉一串值"的 API 与List列表字面量构造器形状一致:var *values: Self.T加上values^.consume_elements[...]把每个实参移入新分配的存储。从实现看,consume_elements(deinit self, elt_handler)只在Self.is_owned时可用(where约束即报错信息"consume_elements may only be called on owned variadic lists"),它用__get_address_as_owned_value逐个转移所有权给处理器闭包(见 variadics.mojo)。值得注意的是源码注释:这里刻意不用Pointer.unsafe_take_pointee,因为它要求元素是Movable,而VariadicList明确不需要这一前提。
打印与调试
当element_type满足Writable时,VariadicList实现write_to/write_repr_to:内部_write_elements循环拼接(a, b, c)形态(is_repr=True时逐元素调用write_repr_to),write_repr_to还会套上VariadicList[Int]类型名(见 variadics.mojo)。这就是为什么测试期望write_to输出(1, 2, 3)、write_repr_to输出VariadicListInt, Int(2), Int(3)))。测试用例见test_variadic_list_write_to与test_variadic_list_write_repr_to(test_variadic.mojo)。
Variadic packs:异构可变参数包
"可变参数包"(variadic pack)是同构VariadicList的异构对应物。不再用*args: T(单一静态元素类型),而是把类型参数包与实参包配对,实参类型取自该包:
def callee*Ts: Writable raises: ...callee 收到一个VariadicPack(定义于 variadics.mojo)。与VariadicList一样,它是RegisterPassable且参与所有权(is_owned、var *args: *Ts、consume_elements、__del__),但内部表示是类似Tuple的异构值:其底层 MLIR 类型是!lit.ref.pack<:param_list<Trait> ... isParamPack>形态的!kgen.struct(见 variadics.mojo),每个槽位可能对应不同大小、不同 ABI 的具体类型。
为什么包需要comptime for
每个实参槽位可能是不同的具体类型,大小与 ABI 各异。因此包更接近元组而非数组:不存在单一的T可用于args[runtime_idx]。索引通过__getitem_param__[index]暴露,使用编译期索引(实现中用lit.ref.pack.extract抽取槽位),编译器才能为每个位置生成正确的加载指令。
这正是惯用代码用comptime for而非运行期for i in range(len(args))遍历包的原因:
def count_many_things*ArgTypes: Intable -> Int: var total = 0 comptime for i in range(args.__len__()): # 每个 args[i] 都有来自 *ArgTypes 的不同具体类型。 total += Int(args[i]) return total def main(): print(count_many_things(Int8(5), UInt32(11), Int(12))) # 28该示例改编自VariadicPack的 docstring(variadics.mojo)。关键点:循环变量i是编译期参数,因此每个args[i]都单独 monomorphize。VariadicPack.__len__直接返回Self.Ts.length(类型包长度,编译期已知)。
Writable包与转发
当每个元素类型都满足Writable时,包实现write_to及相关操作:
def helper*Ts: Writable raises: var s = String() args.write_to(s) # 对 (1, "hello", True) -> "(1, hello, True)" def forwarder*Ts: Writable raises: helper(*args) # splat 原样转发包 # Caller: forwarder(1, "hello", True)转发模式callee(*args)在test_variadic_pack_forwarding、单元素变体test_variadic_pack_forwarding_single_element、空包变体test_variadic_pack_forwarding_empty以及多跳版本test_variadic_pack_forwarding_through_two_levels中均有覆盖(test_variadic.mojo)。空包与单元素包的转发方式完全相同(对应测试中的forwarder()与forwarder(42))。
SomeTypeList语法糖
当只需要"任意数量的类型,且每个都满足Trait"时,可以不引入显式*Ts绑定,直接在def上命名实参包:
def foo(*args: *SomeTypeList[Writable]) raises: var s = String() args.write_to(s)SomeTypeList是定义于 Mojo/stdlib/std/builtin/anytype.mojo 的编译期别名,把Some[T: Trait]的思想扩展到受同一 trait 约束的整个TypeList,在可变调用点尤其有用。对应测试为test_variadic_pack_some(test_variadic.mojo)。
与Tuple的关系
Tuple是拥有异构序列的典型结构体。其构造函数接收一个与其元素类型列表对齐的可变参数包:
# 概念示意(见 Mojo/stdlib/std/builtin/tuple.mojo): struct Tuple*element_types: Movable: def __init__(out self, var *args: *Self.element_types): ...也就是说,VariadicPack本质上就是运行期被降级为结构体的异构元组,编译器在编译期掌握每个元素的精确类型,以生成正确的内存布局与访问代码。
Variadic keyword arguments:可变关键字参数
可变关键字参数是 Python**kwargs的运行时对应物。callee 可以接受任意数量的额外关键字实参,且这些实参的值共享同一个类型V。键始终是运行时String(调用点写下的关键字名)。与可变参数包不同,这里没有异构值类型列表:**kwargs: Int意味着每个传入值都必须是Int,而非混合类型元组。
表面语法是参数名前导**,且必须位于签名末尾、其他参数之后:
def variadic_kwargs(a: Int, b: Int, *args: Int, c: Int, d: Int, **kwargs: Int): pass def print_nicely(**kwargs: Int): for item in kwargs.items(): print(item.key, "=", item.value) def main(): print_nicely(a=7, y=8)每个函数只允许一个**参数,且必须带类型注解:例如**kwargs: Int,不允许裸写**kwargs。解析器侧的支持可见于 Mojo/test/mojo-parser/decls/variadic_kwargs.mojo 与 Mojo/lib/MojoParser/Signatures.cpp(后者负责签名中kw_vararg槽位的解析)。
callee 收到的是StringDict(文档中的OwnedKwargsDict)
设计文档写作时,**kwargs: V以var kwargs: OwnedKwargsDict[V]传入。当前仓库中该类型已更名为StringDict[V],定义于 Mojo/stdlib/std/collections/dict.mojo,其 docstring 明确写着"用于向函数传递拥有的可变关键字实参的容器",且"用户不应直接实例化它"——编译器在调用点构建、传入 callee。这一更名在 Mojo/docs/site/releases/v1.0.0.md 的发布说明中有记录(OwnedKwargsDict→StringDict)。StringDict内部包装了一个Dict[String, V, default_comp_time_hasher],对外暴露字典风格接口:len、in、__getitem__(支持按String或ImmStringSpan查找,后者免分配)、__setitem__、keys()、values()、items()、find、pop等,另有deinit_with用于值非Deinitable时的显式析构。
关键字可变参数以拥有(var)方式传递,因为字典通常为单个调用点构建。不能显式写read或mut约定:
# 暂不支持。 def borrowed_kwargs(mut **kwargs: Int): ...在 callee 内部,kwargs拥有该字典及其插入的值,这与调用 lowering 用_insert把每个关键字操作数转入字典的方式一致。
调用 lowering
文档给出的粗略 lowering 图景(仅为示意名,非精确 MLIR):
// Caller: foo(x=9, stuff=8) with def foo(**kwargs: Int) 1. 分配一个空的 OwnedKwargsDict[Int](局部临时对象)。 2. 对调用点每个关键字操作数: - 把键物化为编译期 String 字面量。 - 求值值表达式。 - 调用 OwnedKwargsDict::_insert(dict, key, value),把值的所有权转入字典。 3. 把填好的字典作为拥有的 **kwargs 实参传给 foo。重载解析收集所有未绑定到前面具名参数的关键字操作数,路由到kw_vararg槽位;若 callee 没有**kwargs参数,这些操作数即报错。位置参数*args与**kwargs可出现在同一签名中,各自"吃掉"自己种类的操作数。
用**kwargs^转发
要把整个关键字束转发给另一个可变关键字 callee,使用双星解包形式并配合^转移所有权:
def pass_kwargs(**kwargs: Int): takes_int_variadic_kwargs_multiline(**kwargs^)不加^时,转发会尝试拷贝字典并因StringDict不可隐式拷贝而失败;加上^后,caller 的字典被移入内层调用。这是callee(*args)splat 可变参数包的关键字对应物。
当前尚不支持从普通Dict[String, V]解包(例如print_nicely(**my_dict));今天只有从另一个**kwargs绑定转发可用。
泛型推断
当函数在值类型上泛型时,关键字实参可以像位置实参一样驱动类型推断:
def infers_from_kwargsT: SomeTrait: pass # T 由关键字值推断为 MemOnly。 infers_from_kwargs(y=MemOnly(), z=s)推断出的元素类型必须满足声明的 trait 约束,且兼容转入StringDict._insert(owned、movable 值)。测试 Mojo/stdlib/test/collections/test_dict.mojo 的test_owned_kwargs_dict与第 1944 行的test_owned_kwargs_dict_linear覆盖了可变关键字参数在 callee 内部暴露的字典 API 表面,以及线性值(非Copyable)的转移规则。
当前限制
以下缺口是首个版本的有意取舍,亦可参见 Mojo 手册 functions 章节:
- 仅支持同构值:所有关键字共享一个值类型
V; - 键始终是
String,没有类型化键变体; - 始终 owned:不支持
read **kwargs或mut **kwargs; - 调用点不支持从普通
Dict解包; - 不支持编译期可变关键字参数(声明上的
**kwparams: ...); - 值必须是
Movable(并满足StringDict的约束);非拥有的线性值遵循与其他 owned 调用实参相同的转移规则。
实践建议与测试参照
- 选择形态:只需要同类型的一串值时用
*args: T(VariadicList);需要每个槽位类型不同、类型由 trait 约束时用*args: *Ts(VariadicPack),并务必用comptime for遍历;需要收集任意关键字键值对时用**kwargs: V(StringDict[V])。 - 所有权:要消费(移动)实参,一律写成
var *args: T/var *args: *Ts,再经consume_elements或**kwargs^转移;借用场景保持默认read语义。 - 元编程:
TypeList的map/reduce/filter_idx/contains与ParameterList.get_span()是编译期类型/值计算的主力工具;SomeTypeList[Trait]是可变调用点上约束类型包的最简写法。 - 验证语法:任何语法细节以测试文件 Mojo/stdlib/test/builtin/test_variadic.mojo 为准,其中
test_variadic_*、test_type_list_*、test_parameter_list_*、test_variadic_pack_*、test_dynamic_variadic_pack等用例分别覆盖了本文讨论的各条路径;关键字字典相关验证在 Mojo/stdlib/test/collections/test_dict.mojo。
文档仍标注为 Draft,TypeList/ParameterList的统一、**kwparams编译期参数等能力属于未来演进方向;实际能力请以当前仓库代码与测试为准。
【免费下载链接】mojoThe Modular Platform (includes MAX & Mojo)项目地址: https://gitcode.com/GitHub_Trending/mo/mojo
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考