打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
Mojo🔥编程手册

带有代码示例的 Mojo 语言功能的全面介绍。

Mojo 是一种编程语言,与 Python 一样易于使用,但具有 C++ 和 Rust 的性能。此外,Mojo 提供了利用整个 Python 库生态系统的能力。

Mojo 通过利用具有集成缓存、多线程和云分布技术的下一代编译器技术实现了这一壮举。此外,Mojo 的自动调整和编译时元编程功能允许您编写可移植到最奇特的硬件的代码。

更重要的是,Mojo 允许您利用整个 Python 生态系统,因此您可以继续使用您熟悉的工具。Mojo 旨在通过保留 Python 的动态特性,同时为系统编程添加新原语,逐渐成为 Python 的超集。这些新的系统编程原语将允许 Mojo 开发人员构建当前需要 C、C++、Rust、CUDA 和其他加速器系统的高性能库。通过汇集最好的动态语言和系统语言,我们希望提供一个统一的跨抽象级别工作的编程模型,对新手程序员友好,并可扩展从加速器到应用程序编程和脚本的许多用例。

本文档是对 Mojo 编程语言的介绍,适合 Mojo 程序员使用。它假定读者了解 Python 和系统编程概念,但不希望读者成为编译器迷。目前,Mojo 仍在进行中,文档面向具有系统编程经验的开发人员。随着该语言的发展和普及,我们希望它对每个人(包括初学者)都友好且易于使用。它只是今天不存在。

使用 Mojo 编译器

您可以像使用 Python 一样从终端运行 Mojo 程序。因此,如果您有一个名为hello.mojo(或者hello.🔥- 是的,文件扩展名可以是表情符号!)的文件,只需键入mojo hello.mojo:

$ cat hello.🔥def main():    print("hello world")    for x in range(9, 0, -3):        print(x)$ mojo hello.🔥hello world963$

同样,您可以使用表情符号或后缀.mojo。

如果您有兴趣深入了解 Mojo 的内部实现细节,查看标准库中的类型、笔记本中的示例代码、博客和其他示例代码可能会很有帮助。

基本系统编程扩展

鉴于我们的兼容性目标和 Python 在高级应用程序和动态 API 方面的优势,我们不必花太多时间来解释语言的这些部分是如何工作的。另一方面,Python 对系统编程的支持主要委托给 C,我们希望提供一个在那个世界上很棒的单一系统。因此,本节将分解每个主要组件和功能,并通过示例描述如何使用它们。

let和var声明

在 Mojo 中的 a 中def,您可以为名称分配一个值,它会隐式创建一个函数范围变量,就像在 Python 中一样。这提供了一种非常动态且不拘一格的代码编写方式,但由于两个原因,这是一个挑战:

系统程序员通常希望声明一个值是不可变的,以保证类型安全和性能。

如果他们在赋值中输入了错误的变量名,他们可能希望得到一个错误。

为了支持这一点,Mojo 提供了范围内的运行时值声明:let是不可变的,并且var是可变的。这些值使用词法范围并支持名称阴影:

def your_function(a, b):    let c = a    c = b  # error: c is immutable    if c != b:        var c = b        stuff()

let和var声明支持类型说明符和模式,以及后期初始化:

def your_function():    let x: Int8 = 42    let y: Int64 = 17    let z: Int8    if x != 0:        z = 1    else:        z = foo()    use(z)

请注意,在声明中,let和var是完全选择加入的def。您仍然可以像 Python 一样使用隐式声明的值,并且它们像往常一样获得函数作用域。

struct类型

Mojo 基于 MLIR 和 LLVM,它们提供了用于许多编程语言的尖端编译器和代码生成系统。这让我们可以更好地控制数据组织、直接访问数据字段以及其他提高性能的方法。现代系统编程语言的一个重要特征是能够在这些复杂的低级操作之上构建高级和安全的抽象,而不会造成任何性能损失。在 Mojo 中,这是由struct类型提供的。

Mojo 中的Astruct类似于 Python class:它们都支持方法、字段、运算符重载、元编程的装饰器等。它们的区别如下:

Python 类是动态的:它们允许在运行时进行动态分派、猴子修补(或“调配”)和动态绑定实例属性。

Mojo 结构是静态的:它们在编译时绑定(您不能在运行时添加方法)。结构允许您以灵活性换取性能,同时安全且易于使用。

这是结构的简单定义:

struct MyPair:    var first: Int    var second: Int    def __init__(self&, first: Int, second: Int):        self.first = first        self.second = second    def __lt__(self, rhs: MyPair) -> Bool:        return self.first < rhs.first or              (self.first == rhs.first and               self.second < rhs.second)

从句法上讲,与 Python 相比最大的区别class是 a 中的所有实例属性都struct 必须使用varor声明显式let声明。

在 Mojo 中,“struct”的结构和内容是预先设置好的,在程序运行时不能更改。在 Python 中,您可以动态添加、删除或更改对象的属性,而 Mojo 不允许对结构进行这种操作。这意味着您不能del在运行程序的过程中使用删除方法或更改其值。

然而, 的静态特性struct有一些很大的好处!它可以帮助 Mojo 更快地运行您的代码。该程序确切地知道在哪里可以找到结构的信息以及如何使用它而无需任何额外的步骤或延迟。

Mojo 的结构还可以很好地与您可能已经从 Python 中了解的功能配合使用,例如运算符重载(它可以让您更改数学符号喜欢+和-处理您自己的数据的方式)。此外,所有“标准类型”(如Int、甚至)都是使用结构创建Bool的。这意味着它们是您可以使用的标准工具集的一部分,而不是硬连接到语言本身。这为您在编写代码时提供了更大的灵活性和控制力。StringTuple

如果您想知道&参数的含义self:这表明该值是可变的,这在下面的By-reference arguments中进行了解释。

Int对比int

在 Mojo 中,您可能会注意到我们使用Int(大写“I”),这与 Python 的int(小写“i”)不同。这种差异是故意的,这实际上是一件好事!让我解释一下原因:

在 Python 中,该int类型可以处理非常大的数字并具有一些额外的功能,比如检查两个数字是否是同一个对象。但这带来了一些额外的负担,可能会减慢速度。Mojo的Int是不同的。它的设计简单、快速,并针对您的计算机硬件进行了调整以快速处理。

我们做出这个选择主要有两个原因:

我们希望为需要与计算机硬件密切合作的程序员(系统程序员)提供一种透明且可靠的与硬件交互的方式。我们不想依靠花哨的技巧(如 JIT 编译器)来加快速度。

我们希望 Mojo 能够很好地与 Python 一起工作而不会引起任何问题。通过使用不同的名称(Int 而不是 int),我们可以在 Mojo 中保留这两种类型,而无需更改 Python 的 int 工作方式。

作为奖励,它Int遵循与您可能在 Mojo 中创建的其他自定义数据类型相同的命名风格。此外,Int它struct包含在 Mojo 的标准工具集中。

强类型检查

尽管您仍然可以像在 Python 中那样使用灵活的类型,但 Mojo 允许您使用严格的类型检查。类型检查可以使您的代码更可预测、更易于管理和更安全。

使用强类型检查的主要方法之一是使用 Mojo 的struct类型。Mojo 中的定义struct定义了一个编译时绑定的名称,并且在类型上下文中对该名称的引用被视为对所定义值的强规范。例如,考虑以下使用MyPair上面显示的结构的代码:

def pairTest() -> Bool:    let p = MyPair(1, 2)    return p < 4 # gives a compile time error

当您运行这段代码时,您会收到一个编译时错误,告诉您“4”不能转换为MyPair,而这是 RHS 的MyPair.__lt__要求。

在使用系统编程语言时,这是一种熟悉的体验,但这不是 Python 的工作方式。Python 的MyPy类型注释在语法上具有相同的功能,但它们不是由编译器强制执行的:相反,它们是通知静态分析的提示。通过将类型绑定到特定声明,Mojo 可以在不破坏兼容性的情况下处理经典类型注释提示和强类型规范。

类型检查并不是强类型的唯一用例。因为我们知道类型是准确的,所以我们可以根据这些类型优化代码,在寄存器中传递值,并且在参数传递和其他低级细节方面与 C 一样高效。这是 Mojo 为系统程序员提供的安全性和可预测性保证的基础。

重载函数和方法

与 Python 一样,您可以在 Mojo 中定义函数而无需指定参数数据类型,Mojo 将动态处理它们。当您想要仅通过接受任意输入并让动态调度决定如何处理数据的表达性 API 时,这很好。然而,当您想要确保类型安全时,如上所述,Mojo 还提供对重载函数和方法的全面支持。

这允许您定义多个具有相同名称但具有不同参数的函数。这是许多语言中常见的特性,例如 C++、Java 和 Swift。

当解析一个函数调用时,Mojo 会尝试每个候选者并使用一个有效的(如果只有一个有效),或者它选择最接近的匹配(如果它可以确定一个接近的匹配),或者它报告该调用是不明确的(如果它可以)弄清楚该选哪一个。在后一种情况下,您可以通过在调用站点上添加显式强制转换来解决歧义。让我们看一个例子:

struct Array[T: AnyType]:    fn __getitem__(self, idx: Int) -> T: ...    fn __getitem__(self, idx: Range) -> ArraySlice: ...

您可以重载结构和类中的方法以及重载模块级函数。

Mojo 不支持仅对结果类型进行重载,并且不使用结果类型或上下文类型信息进行类型推断,从而保持简单、快速和可预测。Mojo 永远不会产生“表达式太复杂”的错误,因为它的类型检查器根据定义简单且快速。

同样,如果您在没有类型定义的情况下保留参数名称,那么该函数的行为就像具有动态类型的 Python 一样。一旦你定义了一个参数类型,Mojo 就会寻找重载候选者并如上所述解析函数调用。

fn定义

上面的扩展是提供低级编程和提供抽象功能的基石,但许多系统程序员更喜欢比 Mojo 中的“def”提供更多的控制和可预测性。回顾一下,'def' 被定义为非常动态、灵活并且通常与 Python 兼容:参数是可变的,局部变量在首次使用时隐式声明,并且不强制执行作用域。这对于高级编程和脚本编写非常有用,但对于系统编程并不总是很好。为了补充这一点,Mojo 提供了一个fn类似于 . 的“严格模式”的声明def。

备选方案:我们可以不使用像 之类的新关键字,而是添加像“ @strictfn def”这样的修饰符或装饰器。但是,我们无论如何都需要采用新的关键字,而且这样做的成本很小。此外,在系统编程领域的实践中,一直使用“fn”,因此将其设为一流可能是有意义的。

fn并且def总是可以从接口级别互换:没有什么'def'可以提供而fn不能提供(反之亦然)。不同之处在于 a在其身体内部fn受到更多限制和控制(或者:迂腐和严格)。具体来说,与 s 相比,s 有许多限制:fndef

参数值默认在函数体中是不可变的(如 a let),而不是可变的(如 a var)。这会捕获意外突变,并允许使用不可复制的类型作为参数。

参数值需要类型说明(self方法中除外),捕捉类型说明的意外遗漏。同样,缺少返回类型说明符被解释为返回None而不是未知返回类型。请注意,两者都可以显式声明返回“ ”,这允许一个人在需要时object选择加入 a 的行为。def

禁用局部变量的隐式声明,因此必须声明所有局部变量。let这会捕获名称拼写错误并与和提供的范围相吻合var。

两者都支持引发异常,但这必须在fn带有raises关键字的 a 上显式声明。

不同团队的编程模式会有很大差异,这种严格程度并不适合所有人。我们希望习惯于 C++ 并且已经在 Python 中使用 MyPy 样式类型注释的人们更喜欢使用fns,但更高级别的程序员和 ML 研究人员继续使用def. Mojo 允许您自由混合def和fn声明,例如,用一个方法实现一些方法,用另一个方法实现另一些方法,并允许每个团队或程序员决定什么是最适合他们的用例的。

和特殊__copyinit__方法__moveinit__

Mojo 支持完整的“值语义”,如在 C++ 和 Swift 等语言中所见,并且它使使用装饰器定义简单的字段聚合变得非常容易@value(在下面更详细地描述)。

对于高级用例,Mojo 允许您定义自定义构造函数(使用 Python 现有的__init__特殊方法)、自定义析构函数(使用现有的__del__特殊方法)以及使用新的__copyinit__和__moveinit__特殊方法的自定义复制和移动构造函数。

这些低级定制挂钩在进行低级系统编程时很有用,例如手动内存管理。例如,考虑一个动态字符串类型,它需要在构造时为字符串数据分配内存,并在销毁值时销毁它:

struct MyString:    var data: Pointer[Int8]    # StringRef has a data + length field    def __init__(self&, input: StringRef):        let data = Pointer[Int8].alloc(input.length+1)        data.memcpy(first.data, input.length)        data[input.length] = 0        self.data = Pointer[Int8](data)    def __del__(owned self):        self.data.free()

这种MyString类型是使用低级函数实现的,以展示其工作原理的简单示例 - 更现实的实现将使用短字符串优化等。但是,如果您继续尝试,您可能会感到惊讶:

fn useStrings():    var a: MyString = "hello"    print(a)   # Should print "hello"    var b = a  # ERROR: MyString doesn't implement __copyinit__    a = "Goodbye"    print(b)   # Should print "hello"    print(a)   # Should print "Goodbye"

编译器不允许我们复制字符串:MyString包含一个实例Pointer(相当于低级 C 指针),Mojo 不知道“指针的含义”或“如何复制它” ”——这就是为什么应用程序级程序员应该使用更高级别的类型(如数组和切片)的原因之一!更一般地说,某些类型(如原子数)不能被复制或移动,因为它们的地址提供了一个身份,就像类实例一样。

在这种情况下,我们确实希望我们的字符串可以被复制。为了实现这一点,我们实现了一个__copyinit__特殊的方法,通常是这样实现的:

struct MyString:    ...    def __copyinit__(self&, existing: Self):        self.data = Pointer(strdup(self.data.address))

通过这种实现,我们上面的代码可以正常工作,并且“b = a”副本会生成一个逻辑上不同的字符串实例,它具有自己的生命周期和数据。()按照上面代码行的指示,使用 C strdup 函数进行复制。Mojo 还支持该__moveinit__方法,它允许 Rust 风格的移动(在生命周期结束时获取一个值)和 C++ 风格的移动(其中值的内容被删除但析构函数仍然运行)并允许定义自定义移动逻辑。请参阅下面的“价值生命周期”部分了解更多信息。

Mojo 提供了对值生命周期的完全控制,包括使类型可复制、只能移动和不可移动的能力。这比 Swift 和 Rust 等要求值至少可以移动的语言更具控制力。如果你好奇如何在不创建副本的情况existing下传递到__copyinit__方法中,请查看下面“借用”参数约定部分。

参数化:编译时元编程

Python 最令人惊叹的特性之一是其可扩展的运行时元编程特性。这启用了范围广泛的库,并提供了一个灵活且可扩展的编程模型,各地的 Python 程序员都可以从中受益。不幸的是,这些功能也是有代价的:因为它们是在运行时评估的,所以它们直接影响底层代码的运行时效率。因为它们不为 IDE 所知,代码完成等 IDE 功能很难理解它们并使用它们来改善开发人员体验。

在 Python 生态系统之外,静态元编程也是开发的重要组成部分,支持开发新的编程范式和高级库。这个领域有很多现有技术的例子,有不同的权衡,例如:

预处理器(例如 C 预处理器、Lex/YACC 等)可能是最重的。它们完全通用,但在开发人员体验和工具集成方面最差。

一些语言(如 Lisp 和 Rust)支持(有时是“卫生的”)宏扩展功能,通过更好的工具集成实现语法扩展和样板代码减少。

一些较旧的语言,如 C++,具有非常庞大和复杂的元编程语言(模板),它们是运行时语言的双重。这些特别难以学习并且编译时间和错误消息都很差。

一些语言(如 Swift)以一流的方式将许多特性构建到核心语言中,以牺牲通用性为代价为常见情况提供良好的人体工程学。

一些较新的语言,如 Zig,将语言解释器集成到编译流程中,并允许解释器在编译时反映 AST。这允许许多与宏系统相同的功能,具有更好的可扩展性和通用性。

对于 Modular 在 AI、高性能机器学习内核和加速器方面的工作,我们需要高级元编程系统提供的高抽象能力。我们需要高级零成本抽象、表达库和多种算法变体的大规模集成。我们希望库开发人员能够扩展系统,就像他们在 Python 中所做的那样,提供一个可扩展的开发平台。

也就是说,我们不愿意牺牲开发人员的体验(包括编译时间和错误消息),我们也没有兴趣构建一个难以教授的并行语言生态系统。我们可以从这些以前的系统中学习,但也可以在其之上构建新技术,包括 MLIR 和细粒度语言集成缓存技术。

因此,Mojo 支持编译器中内置的完整编译时元编程功能,作为一个单独的编译阶段——在解析、语义分析和 IR 生成之后,但在降低到特定于目标的代码之前。它对运行时程序使用与元程序相同的宿主语言,并利用 MLIR 以可预测的方式表示和评估这些程序。

让我们看一些简单的例子。

关于命名的注意事项:在命名上兜兜转转后,我们最终将这些东西称为“参数”。Python 程序员将“arguments”和“parameters”这两个词互换使用,作为“传递给函数的东西”的近义词。我们目前决定收回“参数”、“参数表达式”这两个词来表示编译时值,但使用“参数”和“表达式”来指代运行时值。这使我们能够围绕“参数化”和“参数化”等词进行对齐。

定义参数化类型和函数

Mojo 结构和函数都可以参数化,但是一个例子可以帮助激发我们关心的原因。让我们看一下“ SIMD ”类型,它表示硬件中的低级向量寄存器,其中包含标量数据类型的多个实例。现在的硬件加速器正在获得奇异的数据类型,并且使用具有 512 位或更长 SIMD 向量的 CPU 并不少见。硬件有很多多样性(包括许多品牌,如 SSE、AVX-512、NEON、SVE、RVV 等),但许多操作是常见的,并被数字和 ML 内核开发人员使用——这种类型将它们暴露给 Mojo 程序员。

这是 Mojo 标准库中 SIMD API 的(缩减)版本:

struct SIMD[type: DType, size: Int]:    var value: … # Some low-level MLIR stuff here    # Create a new SIMD from a number of scalars    fn __init__(self&, *elems: SIMD[type, 1]):  ...    # Fill a SIMD with a duplicated scalar value.    @staticmethod    fn splat(x: SIMD[type, 1]) -> SIMD[type, size]: ...    # Cast the elements of the SIMD to a different elt type.    fn cast[target: DType](self) -> SIMD[target, size]: ...    # Many standard operators are supported.    fn __add__(self, rhs: Self) -> Self: ...

Mojo 中的参数使用PEP695 语法的扩展版本在方括号中声明。它们的名称和类型类似于 Mojo 程序中的普通值,但它们是在编译时而不是运行时由目标程序求值的。运行时程序可以使用参数的值——因为参数在编译时在运行时程序需要它们之前被解析——但编译时参数表达式可能不使用运行时值。

在上面摘录的情况下SIMD,有三个声明的参数:SIMD 结构由type参数和size参数参数化。该cast方法用target参数进一步参数化。因为 SIMD 是参数化类型,所以“self”参数的类型带有参数——完整的类型名称是“ SIMD[type, size]”。虽然写出来总是有效的(如 的返回类型所示splat),但这可能很冗长:我们建议像示例一样使用类型Self(来自PEP673)__add__。

使用参数化类型和函数

对于这种类型,'size' 指定 SIMD 向量中的元素数量,type 指定元素类型 - 例如,您可以使用“4xFloat”来表示小浮点向量或 AVX 上的“32xbfloat16's”具有“bfloat16”机器学习类型的 -512 系统:

fn funWithSIMD():    # Make a vector of 4 floats.    let smallVec = SIMD[DType.f32, 4](1.0, 2.0, 3.0, 4.0)    # Make a big vector containing 1.0 in bfloat16 format.    let bigVec = SIMD[DType.bf16, 32].splat(1.0)    # Do some math and convert the elements to float32.    let biggerVec = (bigVec+bigVec).cast[DType.f32]()    # You can write types out explicitly if you want of course.    let biggerVec2 : SIMD[DType.f32, 32] = biggerVec

请注意,“cast”方法需要一个额外的参数来指示要转换为哪种类型:这是通过参数化对“cast”的调用来处理的。上面的示例显示了具体类型的使用,但参数的主要功能来自于定义参数算法和类型的能力,例如,定义参数算法非常容易,例如与长度和 DType 无关的算法:

fn rsqrt[width: Int, dt: DType](x: SIMD[dt, width]) -> SIMD[dt, width]:    return 1 / sqrt(x)

Mojo 编译器在使用参数进行类型推断方面相当聪明。请注意,此函数可以sqrt(x)在不指定参数的情况下调用参数函数,编译器会像您sqrt[width,type](x)显式编写一样推断其参数。另请注意,rsqrt选择定义其第一个名为“width”的参数,但 SIMD 类型size毫无疑问地命名了它。

参数表达式只是 Mojo 代码

所有参数和参数表达式都使用与运行时程序相同的类型系统进行类型化:“Int”和“DType”在 Mojo 标准库中作为结构实现。参数非常强大,支持在编译时使用带有运算符的表达式、函数调用等,就像运行时程序一样。这允许使用许多“依赖类型”功能,例如,您可能想要定义一个辅助函数来连接两个 SIMD 向量:

fn concat[ty: DType, len1: Int, len2: Int](    lhs: SIMD[ty, len1], rhs: SIMD[ty, len2]) -> SIMD[ty, len1+len2]:      ...fn use_vectors(a: SIMD[DType.f32, 4], b: SIMD[DType.f16, 8]):    let x = concat(a, a)  # Length = 8    let y = concat(b, b)  # Length = 16

请注意生成的长度是输入向量长度的总和,您可以使用简单的 + 运算来表达它。举一个更复杂的例子,看看SIMD.shuffle标准库中的方法:它接受两个输入 SIMD 值,一个向量洗牌掩码作为列表,并返回一个与洗牌掩码长度匹配的 SIMD。

强大的编译时编程

虽然简单的表达式很有用,但有时您希望编写具有控制流的命令式编译时逻辑。例如,Math.mojo 中的“isclose”函数对整数使用精确相等,但对浮点数使用“接近”比较。您甚至可以进行编译时递归,例如,这里有一个示例“树缩减”算法,它将向量的所有元素递归地求和为一个标量:

struct SIMD[type: DType, size: Int]:    ...    fn reduce_add(self) -> SIMD[type, 1]:        @parameter        if size == 1:            return self[0]        elif size == 2:            return self[0] + self[1]        # Extract the top/bottom halves, add them, sum the elements.        let lhs = self.slice[size // 2](0)        let rhs = self.slice[size // 2](size // 2)        return (lhs + rhs).reduce_add()

这利用了这个@parameter if特性,它是一个在编译时运行的 if 语句。它要求其条件是一个有效的参数表达式,并确保只有 if 的活分支被编译到程序中。

Mojo 类型只是参数表达式

虽然我们已经展示了如何在类型中使用参数表达式,但在 Python 和 Mojo 中,类型注释本身可以是任意表达式。Mojo 中的类型有一种特殊的元类型类型,允许定义类型参数算法和函数,例如可以像这样定义像 C++ 类这样的算法std::vector:

struct DynamicVector[type: AnyType]:    ...    fn reserve(self&, new_capacity: Int): ...    fn push_back(self&, value: type): ...    fn pop_back(self&): ...    fn __getitem__(self, i: Int) -> type: ...    fn __setitem__(self&, i: Int, value: type): ...fn use_vector():    var v = DynamicVector[Int]()    v.push_back(17)    v.push_back(42)    v[0] = 123    print(v[1])      # Prints 42    print(v[0])      # Prints 123

请注意,“type”参数用作“value”参数的形式类型和函数的返回类型__getitem__。参数允许DynamicVector类型根据不同的用例提供不同的 API。还有许多其他案例受益于更高级的用例。例如,并行处理库定义了一种parallelForEachN算法,该算法并行执行闭包 N 次,从上下文中输入一个值。该值可以是任何类型:

fn parallelize[    arg_type: AnyType,    func: fn(Int, arg_type) -> None,](rt: Runtime, num_work_items: Int, arg: arg_type):    # Not actually parallel: see Functional.mojo for real impl.    for i in range(num_work_items):        func(i, arg)

这是可能的,因为允许“func”参数引用较早的“arg_type”参数,这反过来又改进了它的类型。

另一个重要的例子是可变泛型,其中可能需要在异构类型列表上定义算法或数据结构:

struct Tuple[*ElementTys: AnyType]:    var _storage : *ElementTys

注意:我们还没有足够的元类型助手,但我们将来应该可以写这样的东西,尽管重载仍然是处理这个问题的更好方法:

struct Array[T: AnyType]:    fn __getitem__[IndexType: AnyType](self, idx: IndexType)       -> (ArraySlice[T] if issubclass(IndexType, Range) else T):       ...

alias: 命名参数表达式

想要命名编译时值是很常见的。虽然var定义了一个运行时值,并let定义了一个运行时常量,但我们需要一种方法来定义一个编译时临时值。为此,Mojo 使用alias声明。例如,该DType结构使用枚举器的别名实现了一个简单的枚举,如下所示(实际的内部实现细节略有不同):

struct DType:    var value : Int8    alias invalid = DType(0)    alias bool = DType(1)    alias si8 = DType(2)    alias ui8 = DType(3)    alias si16 = DType(4)    alias ui16 = DType(5)    ...    alias f32 = DType(15)

这允许客户端自然地用作DType.f32参数表达式(也用作运行时值)。请注意,这是在编译时调用 DType 的运行时构造函数。

类型是别名的另一个常见用途:因为类型是编译时表达式,所以可以很方便地执行以下操作:

alias F32 = SIMD[DType.f32, 1]alias UI8 = SIMD[DType.ui8, 1]var x : F32   # F32 works like a "typedef"

像varand 一样let,别名服从作用域,您可以按预期在函数内使用局部别名。

自动调整/自适应编译

Mojo 参数表达式允许您像使用其他语言一样编写可移植的参数算法,但是在编写高性能代码时,您仍然必须选择具体的值用于参数。例如,在编写高性能数值算法时,您可能希望使用内存平铺来加速算法,但要使用的维度在很大程度上取决于可用的硬件功能、缓存的大小、融合到内核中的内容,以及许多其他繁琐的细节。

甚至矢量长度也很难管理,因为典型机器的矢量长度取决于数据类型,而某些数据类型(例如)并bfloat16没有在所有实现上得到完全支持。Mojo 通过autotune在标准库中提供一个函数来提供帮助。例如,如果您想将向量长度不可知算法写入数据缓冲区,您可以这样写:

from Autotune import autotunedef exp_buffer_impl[dt: DType](data: ArraySlice[dt]):    # Pick vector length for this dtype and hardware    alias vector_len = autotune(1, 4, 8, 16, 32)    # Use it as the vectorization length    vectorize[exp[dt, vector_len]](data)

在编译此代码的实例时,Mojo 分叉此算法的编译,并通过测量在实践中最适合目标硬件的值来决定使用哪个值。它评估表达式的不同值vector_len,并根据用户定义的性能评估器选择最快的一个。因为它单独测量和评估每个选项,所以它可能会为 F32 选择与 SI8 不同的矢量长度,例如。这个简单的特性非常强大——超越了简单的整数常量——因为函数和类型也是参数表达式。

exp_buffer_impl用户可以通过提供性能评估器和使用标准库函数来检测搜索search。search接受一个评估器和一个分叉函数,并返回评估器选择的最快实现作为参数结果。

from Autotune import searchfn exp_buffer[dt: DType](data: ArraySlice[dt]):    # Forward declare the result parameter.    alias best_impl: fn(ArraySlice[dt]) -> None    # Perform search!    search[      fn(ArraySlice[dt]) -> None,      exp_buffer_impl[dt],      exp_evaluator[dt] -> best_impl    ]()    # Call the selected implementation    best_impl(data)

在这个例子中,我们exp_evaluator作为性能评估器提供给搜索功能。使用候选函数列表调用性能评估器,并应返回最佳函数的索引。Mojo 的标准库提供了一个Benchmark可用于计时函数的模块。

from Benchmark import Benchmarkfn exp_evaluator[dt: DType](    fns: Pointer[fn(ArraySlice[dt]) -> None],    num: Int):    var best_idx = -1    var best_time = -1    for i in range(num):        candidate = fns[i]        let buf = Buffer[dt]()        # Benchmark this candidate.        fn setup():            buf.fill_random()        fn wrapper():            candidate(buf)        let cur_time = Benchmark(2).run[wrapper, setup]()        # Track the index of the fastest candidate.        if best_idx < 0:            best_idx = i            best_time = cur_time        elif best_time > cur_time:            best_idx = f_idx            best_time = cur_time    # Return the fastest implementation.    return best_idx

自动调整具有指数运行时间。它受益于 Mojo 编译器堆栈的内部实现细节(特别是 MLIR、集成缓存和编译分发)。这是一个高级用户功能,需要随着时间的推移不断开发和迭代。

参数传递控制和内存所有权

在 Python 和 Mojo 中,大部分语言都围绕着函数调用:许多(显然)内置功能是在标准库中使用“dunder”方法实现的。Mojo 比 Python 更进一步,将最基本的东西(如整数和对象类型本身)放入标准库中。

为什么参数约定很重要

在 Python 中,所有基本值都是对对象的引用——Python 程序员通常将编程模型视为一切都是引用语义。然而,在 CPython 或机器级别,我们可以看到引用本身实际上是通过复制传递的,通过复制指针和调整引用计数。

这种方法提供了一个舒适的编程模型(尽管由于引用共享而偶尔会令人惊讶)但它需要所有值都在堆上分配。Mojo 类(TODO:will)遵循与 Python 相同的引用语义实现方法,但这对于系统编程上下文中的整数等更简单的类型来说并不实用。在这些场景中,您希望这些值存在于堆栈中,甚至存在于硬件寄存器中。因此,Mojo 结构总是内联到它们的容器中,无论是作为另一种类型的字段还是包含函数的堆栈帧。

这就提出了一个有趣的问题:如何实现需要改变self结构类型的方法,例如“ __iadd__"?“如何let"工作以及它如何防止突变?如何控制这些值的生命周期以使 Mojo 成为一种内存安全的语言?

答案是 Mojo 编译器使用数据流分析和类型注释来提供对值副本、引用别名和变异控制的完全控制。提供的功能在许多方面与 Rust 语言提供的功能相似,但它们的工作方式有所不同,以使 Mojo 更易于学习并更好地集成到 Python 生态系统中,而无需大量注释负担。

引用参数

让我们从简单的案例开始:将可变引用传递给值与传递不可变引用。正如我们已经知道的,传递给 的参数fn在默认情况下是不可变的:

struct Int:    # self and rhs are both immutable in __add__.    fn __add__(self, rhs: Int) -> Int: ...    # ... but this cannot work for __iadd__    fn __iadd__(self, rhs: Int):        self = self + rhs  # ERROR: cannot assign to self!

这里的问题是__iadd__需要改变整数的内部状态。&Mojo 中的解决方案是通过在参数名称上使用标记(self在本例中)来声明参数是“通过引用”传递的:

struct Int:    # ...    fn __iadd__(self&, rhs: Int):        self = self + rhs    # OK

因为这个参数是通过引用传递的,所以“self”参数在被调用者中是可变的,并且任何更改在调用者中都是可见的——即使调用者有一个非平凡的计算来访问它,比如数组下标:

fn show_mutation():    var x = 42    x += 1    print(x)    # prints 43 of course    var a = InlinedFixedVector[16, Int](...)    a[4] = 7    a[4] += 1    # Mutate an element within the InlinedFixedVector    print(a[4])  # Prints 8    let y = x    y += 1       # ERROR: Cannot mutate 'let' value

Mojo 通过发出对临时缓冲区的调用,然后在调用后__getitem__存储 with 来实现 InlinedFixedVector 元素的就地突变。__setitem__值的变更let失败,因为不可能形成对不可变值的可变引用。__getitem__类似地,如果编译器实现但不实现 ,则编译器会拒绝尝试使用带有 by-ref 参数的下标__setitem__。

Mojo 中的“self”没有什么特别之处,您可以有多个不同的引用参数。例如,您可以像这样定义和使用交换函数:

fn swap(lhs&: Int, rhs&: Int):    let tmp = lhs    lhs = rhs    rhs = tmpfn show_swap():    var x = 42    var y = 12    swap(x, y)    print(x)  # Prints 12    print(y)  # Prints 42

该系统的一个非常重要的方面是它全部正确组合。

&替代方案:我们可以将其称为参数,而不是使用印记inout。这样的拼写会更好地与其他参数约定关键字对齐,并且考虑到 Mojo 计算的 LValues 的工作方式更正确。

“借来的”论证惯例

现在我们知道了按引用参数传递是如何工作的,您可能想知道按值参数传递是如何工作的以及它如何与__copyinit__实现复制构造函数的方法交互。在 Mojo 中,将参数传递给函数的默认约定是通过“借用”参数约定传递。如果您愿意,可以明确说明:

fn use_something_big(borrowed a: SomethingBig, b: SomethingBig):    """'a' and 'b' are passed the same, because 'borrowed' is the default."""    a.print_id()    b.print_id()

此默认值统一适用于所有参数,包括self方法的参数。借用的约定从调用者的上下文中传递对值的不可变引用,而不是复制值。这在传递大值或传递昂贵的值(如引用计数指针(这是 Python/Mojo 类的默认值))时效率更高,因为在传递参数时不必调用复制构造函数和析构函数。这是一个基于上述代码的更详细的示例:

# A type that is so expensive to copy around we don't even have a# __copyinit__ method.struct SomethingBig:    var id_number: Int    var huge: InlinedArray[Int, 100000]    fn __init__(self&): …    # self is passed by-reference for mutation as described above.    fn set_id(self&, number: Int):        self.id_number = number    # Arguments like self are passed as borrowed by default.    fn print_id(self):  # Same as: fn print_id(borrowed self):        print(self.id_number)fn try_something_big():    # Big thing sits on the stack: after we construct it it cannot be    # moved or copied.    let big = SomethingBig()    # We still want to do useful things with it though!    big.print_id()    # Do other things with it.    use_something_big(big, big)

因为借用了默认参数约定,我们得到了默认情况下做正确事情的简单且合乎逻辑的代码:例如,我们不想复制或移动所有只是SomethingBig为了调用“ print_id”方法,或者在调用时use_something_big。

借用的惯例是相似的,并且在其他语言中有先例。例如,借用参数约定在某些方面类似于const&C++ 中通过“ ”传递参数。这避免了值的副本并禁用了被调用方的可变性。借用的约定const&在两个重要方面不同于 C++ 中的“”:

Mojo 编译器实现了一个借用检查器(类似于 Rust),它可以防止代码在存在未完成的不可变引用时动态形成对值的可变引用,并防止对同一值有多个可变引用。您可以多次借用(如上面对“ use_something_big”的调用),但不能通过可变引用传递某些内容并同时借用。(TODO:当前未启用)。

Int像“ ”、“ Float”和“ ”这样的小值SIMD直接在机器寄存器中传递,而不是通过额外的间接寻址(这是因为它们是用“ @register_passable”装饰器声明的,见下文)。与 C++ 和 Rust 等语言相比,这是一个显着的性能增强,并将这种优化从每个调用站点转移到对类型进行声明。

Rust 是另一种重要的语言,Mojo 和 Rust 借用检查器强制执行相同的排他性不变量。Rust 和 Mojo 之间的主要区别在于调用方不需要 sigil 来通过借用传递,Mojo 在传递小值时效率更高,而 Rust 默认情况下默认移动值而不是通过借用传递它们。这些策略和语法决策允许 Mojo 提供更易于使用的编程模型。

“拥有”参数约定和后缀^运算符

Mojo 支持的最后一个参数约定是owned参数约定。此约定用于想要独占某个值的所有权的函数,并且通常与后缀运算^符一起使用。

例如,考虑使用像唯一指针这样的只能移动的类型。虽然借用约定使使用唯一指针变得容易而无需仪式,但在某些时候您可能希望将所有权转移给其他一些功能。这是^操作员所做的:

fn usePointer():    let ptr = SomeUniquePtr(...)    use(ptr)        # Perfectly fine to pass to borrowing function.    use(ptr)    take_ptr(ptr^)  # pass ownership of the `ptr` value to another function.    use(ptr) # ERROR: ptr is no longer valid here!

对于可移动类型,^运算符结束值绑定的生命周期并将值转移到其他对象(在本例中为函数take_ptr)。为了支持这一点,您可以将函数定义为采用自有参数,例如,您可以take_ptr这样定义:

fn take_ptr(owned p: SomeUniquePtr):    use(p)

因为它是声明的owned,所以该take_ptr函数知道它对该值具有唯一访问权限。这对于唯一指针之类的东西非常重要,可以用于避免复制,并且也是其他情况的概括。

例如,您会特别看到owned关于析构函数和使用移动初始化器的约定,例如,我们MyString之前的类型可以定义为:

struct MyString:    var data: Pointer[Int8]    # StringRef has a data + length field    def __init__(self&, input: StringRef): ...    def __copyinit__(self&, existing: Self): ...    def __moveinit__(self&, owned existing: Self):        self.data = existing.data    def __del__(owned self):        self.data.free()

这是因为你需要拥有一个价值来摧毁它或窃取它的部分!

@register_passable结构装饰器

如上所述,使用值的默认模型是它们存在于内存中,因此它们有一个身份,这意味着它们间接地传入和传出函数(等效地,它们在机器级别“通过引用”传递)。这对于无法移动的类型非常有用,并且对于大型对象或具有昂贵复制操作的事物来说是安全的默认值。然而,对于像单个整数或浮点数这样的微小事物,它是低效的!

为了解决这个问题,Mojo 允许结构选择在寄存器中传递,而不是通过装饰器传递内存@register_passable。Int你会在标准库中的类型上看到这个装饰器:

@register_passable("trivial")struct Int:    var value: __mlir_type.`!pop.scalar`    fn __init__(value: __mlir_type.`!pop.scalar`) -> Self:        return Self {value: value}    ...

基本@register_passable装饰器不会改变类型的基本行为:它仍然需要有一个__copyinit__可复制的方法,可能仍然有一个__init__和__del__方法等。这个装饰器的主要影响是内部实现细节:@register_passable类型通常被传入机器寄存器(取决于底层架构的细节)。

对于典型的 Mojo 程序员来说,这个装饰器只有几个可观察到的效果:

@register_passable类型不能持有不是它们自身的类型的实例@register_passable。

类型的实例@register_passable没有可预测的标识,因此“self”指针不稳定/不可预测(例如在哈希表中)。

@register_passable参数和结果直接暴露给 C 和 C++,而不是通过指针传递。

这种类型的和方法是隐式静态的(就像在 Python__init__中一样)并按值返回其结果而不是采用.__copyinit____new__self&

我们希望这个装饰器将普遍用于核心标准库类型,但对于一般应用程序级代码可以安全地忽略。

上面的例子Int实际上使用了这个装饰器的“普通”变体。它改变了如上所述的传递约定,但也不允许复制和移动构造函数和析构函数(将它们全部合成)。

TODO:Trivial 需要与它自己的装饰器分离,因为它也适用于内存类型。

def参数传递如何工作

传入def函数的参数是传入参数的糖分fn:

如果没有明确的类型注释,编译器默认为 type Object。

带有显式标记(例如通过引用或拥有)的参数服从它们的标记。

没有参数约定的参数通过隐式复制传递到与参数同名的可变变量中。隐式复制要求类型具有__copyinit__方法。

这些函数是等效的(调用者的关键字参数标签除外):

def example(a&: Int, b: Int, c):    ...fn example(a&: Int, b_in: Int, c_in: Object):    var b = b_in    var c = c_in    ...

如您所见,具有显式类型和约定的“a”参数的处理方式与之前完全相同。类型化的“b”参数保持其类型,但获得一个可变的影子副本,因此被调用者可以修改 def 主体内的值。'c' 参数获得一个隐式对象类型,并且在主体中是可变的。这些副本通常不会增加开销,因为像对象引用这样的小类型复制起来很便宜。昂贵的部分是引用计数调整,它通过移动优化消除。

Python 集成

在 Mojo 中使用您熟悉和喜爱的 Python 模块很容易。您可以将任何 Python 模块导入您的 Mojo 程序并从 Mojo 类型创建 Python 类型。

导入 Python 模块

要在 Mojo 中导入 Python 模块,只需Python.import_module()使用模块名称调用:

from PythonInterface import Python# This is equivalent to Python's `import numpy as np`let np = Python.import_module("numpy")# Now use numpy as if writing in Pythona = np.array([1, 2, 3])

是的,这会导入 Python NumPy,您可以导入任何其他 Python 模块。

但是,请记住,您正在使用 Python 对象,因此某些 Mojo 函数print()无法使用。如果你想在 Mojo 中打印一个 Python 类型,你需要使用 Python 的内置print()函数:

a = np.array([1, 2, 3])builtins = Python.import_module("builtins")builtins.print(a)

目前,您无法导入单个成员(例如单个 Python 类或函数)——您必须导入整个 Python 模块,然后通过模块名称访问成员。

导入本地 Python 模块

如果您想在 Mojo 中使用一些本地 Python 代码,只需将目录添加到 Python 路径,然后导入模块即可。

例如,假设您有一个这样的 Python 文件:

mypython.py

import numpy as npdef my_algorithm(a, b):    array_a = np.random.rand(a, a)    return array_a + b

以下是如何导入它并在 Mojo 中使用它:

mojo-code.mojo

from PythonInterface import PythonPython.add_to_path("path/to/module")let mypython = Python.import_module("mypython")let builtins = Python.import_module("builtins")let c = mypython.my_algorithm(2, 3)builtins.print(c)

在 Mojo 中使用 Python 时无需担心内存管理问题。一切正常,因为 Mojo 从一开始就是为 Python 设计的。

Python 中的 Mojo 类型

Mojo 原始类型隐式转换为 Python 对象。今天我们支持列表、元组、整数、浮点数、布尔值和字符串。

例如,给定打印 Python 类型的 Python 函数:

mypython2.py

def type_printer(my_list, my_tuple, my_int, my_string, my_float):    print(type(my_list))    print(type(my_tuple))    print(type(my_int))    print(type(my_string))    print(type(my_float))

您可以导入它并传递给它 Mojo 类型,没问题:

mojo-code.mojo

from PythonInterface import PythonPython.add_to_path("/path/to/module")let mypython2 = Python.import_module("mypython2")mypython2.type_printer([0, 3], (False, True), 4, "orange", 3.4)

它将隐式转换为 Python 类型后输出类型:

Mojo 还没有标准的字典,所以还不可能从 Mojo 字典创建 Python 字典。不过,您可以在 Mojo 中使用 Python 字典!

“价值生命周期”:一个价值的诞生、生命和死亡

Now that we have an understanding of the different ingredients that can go into building functions and the types system, we can look at how to put together together to model important types that you may want to express in Mojo.

许多现有语言通过不同的权衡来表达设计点:例如,C++ 非常强大,但经常被指责为“错误的默认设置”,这会导致错误和功能不当。Swift 易于使用,但其模型的可预测性较差,该模型会大量复制值,并且依赖于“ARC 优化器”来提高性能。Rust 以强大的价值所有权目标来满足其借用检查器的需求,但依赖于可移动的价值,这使得表达自定义移动构造函数具有挑战性,并且会给性能带来很大压力memcpy。在 Python 中,一切都是对类的引用,所以它从来没有真正面临过这些问题。

对于 Mojo,我们受益于从这些现有系统中学习,并旨在提供一个非常强大且易于学习和理解的模型。我们也不希望在“足够智能”的编译器中内置“尽力而为”和难以预测的优化过程。

为了探索这些问题,我们研究了不同的价值分类和表达它们的相关 Mojo 功能,并自下而上地构建。我们使用 C++ 作为示例中的主要比较点,因为它广为人知,但如果其他语言提供更好的比较点,则偶尔会参考它们。

无法实例化的类型

Mojo 中最基本的类型是不允许您创建它的实例的类型:这些类型根本没有初始化器,如果它们有析构函数,它将永远不会被调用(因为没有实例可以销毁):

struct NoInstances:    var state: Int  # Pretty useless    alias my_int = Int    @staticmethod    fn print_hello():        print("hello world")

默认情况下,Mojo 类型不会获得默认构造函数、移动构造函数、成员初始化器或其他任何东西,因此不可能创建此NoInstances类型的实例。为了获得它们,您需要定义一个__init__方法或使用合成初始化器的装饰器。如图所示,这些类型可用作“命名空间”,因为您可以引用静态成员NoInstances.my_int,NoInstances.print_hello()甚至无法实例化该类型的实例。

不可移动和不可复制的类型

如果我们在复杂性的阶梯上更上一层楼,我们将获得可以实例化的类型,但是一旦它们被固定到内存中的地址并且不能被隐式移动或复制。std::atomic这对于实现像原子操作(例如,在 C++ 中)或其他类型(其中值的内存地址是其标识并且对其用途至关重要)的类型很有用:

struct Atomic:    var state: Int    fn __init__(self&, state: Int = 0):        self.state = state    fn __iadd__(self&, rhs: Int):        #...atomic magic...    fn get_value(self) -> Int:        return atomic_load_int(self.state)

此类定义了一个初始化器但没有复制或移动构造函数,因此一旦它被初始化就永远不能移动或复制。这是安全和有用的,因为 Mojo 的所有权系统是完全“地址正确的”——当它被初始化到堆栈或其他类型的字段中时,它永远不需要移动。

请注意,Mojo 的方法仅控制内置操作,如a = b复制和x^消耗运算符。一种可用于此类类型的有用模式是添加一个显式copy()方法(一种非“dunder”方法),当程序员知道它是安全的时,它可以用于显式复制实例。

独特的“仅移动”类型

如果我们在能力阶梯上更上一层楼,我们将遇到“独特”的类型——在 C++ 中有很多这样的例子,例如类似的类型,甚至是拥有底层 POSIX 文件描述符的std::unique_ptr类型FileDescriptor。这些类型在 Rust 等语言中无处不在,不鼓励复制,但“移动”是免费的。__moveinit__在 Mojo 中,您可以通过使用像这样的消耗现有方法来实现方法来声明这些:

# This is a simple wrapper around POSIX-style fcntl.h functions.struct FileDescriptor:    var fd: Int    # This is the new.    fn __moveinit__(self&, consuming existing: Self):        self.fd = existing.fd    # This takes ownership of a POSIX file descriptor.    fn __init__(self&, fd: Int):        self.fd = fd    fn __init__(self&, path: String):        # Error handling omitted, call the open(2) syscall.        self = FileDescriptor(open(path, ...))    fn __del__(owned self):        close(self.fd)   # pseudo code, call close(2)    fn dup(self) -> Self:        # Invoke the dup(2) system call.        return Self(dup(self.fd))    fn read(...): ...    fn write(...): ...

新概念是我们添加了一个名为__moveinit__. 消费移动初始值设定项获取现有实例的所有权FileDescriptor,并将其内部实现细节移至新实例。这是因为 的实例FileDescriptor可能存在于不同的位置,并且它们可以在逻辑上四处移动——窃取一个值的主体并将其移动到另一个值。

这是一个令人震惊的例子,它会多次调用它:

fn egregious_moves(owned fd1: FileDescriptor):    # fd1 and fd2 have different addresses in memory, but the    # consume operator moves unique ownership from fd1 to fd2.    let fd2 = fd1^    # Do it again, a use of fd2 after this point will produce an error.    let fd3 = fd2^    # We can do this all day...    let fd4 = fd3^    fd4.read(...)    # fd4.__del__() runs here

请注意值的所有权如何在拥有它的各种值之间转移,使用后缀-'consume ^' 运算符来销毁先前的绑定。如果您熟悉 C++,那么考虑消耗运算符的简单方法就像std::move,但在这种情况下,我们可以看到它能够移动事物而不会将它们重置为可以销毁的状态:在 C++ 中,如果您移动运算符未能更改旧值的fd实例,它将被关闭两次。

Mojo 跟踪值的活跃度并允许您定义自定义移动构造函数。这很少需要,但在需要时非常强大。例如,某些类型喜欢llvm::SmallVector type使用“内联存储”优化技术,并且它们可能希望在其实例中使用“内部指针”来实现。这是一个众所周知的减少 malloc 内存分配器压力的技巧,但这意味着“移动”操作需要自定义逻辑来在发生这种情况时更新指针。

使用 Mojo,这就像实现自定义__moveinit__方法一样简单。这在 C++ 中也很容易实现(尽管在不需要自定义逻辑的情况下使用样板),但在其他流行的内存安全语言中很难实现。

需要注意的一点是,虽然 Mojo 编译器提供了良好的可预测性和控制,但它也非常复杂。它保留删除临时文件和相应的复制/移动操作的权利。如果这不适合您的类型,您应该使用显式方法而copy()不是 dunder 方法。

支持“偷走”的类型

内存安全语言的一个挑战是它们需要围绕编译器能够跟踪的内容提供可预测的编程模型,而编译器中的静态分析在本质上是有限的。例如,虽然编译器可以理解下面第一个示例中的两个数组访问是针对不同的数组元素,但(通常)不可能推断出第二个示例:

std::pair getValues1(MutableArray &array) {    return { std::move(array[0]), std::move(array[1]) };}std::pair getValues2(MutableArray &array, size_t i, size_t j) {    return { std::move(array[i]), std::move(array[j]) };}

这里的问题是根本没有办法(只看上面的函数体)知道或证明 和 的动态值i不j一样。虽然可以维护动态状态以跟踪数组的各个元素是否处于活动状态,但这通常会导致显着的运行时开销(即使未使用移动/消耗),这是 Mojo 和其他系统编程语言不愿意做的事情做。有多种方法可以解决这个问题,包括一些并不总是容易学习的非常复杂的解决方案。

Mojo 采用务实的方法让 Mojo 程序员无需绕过其类型系统即可完成工作。如上所示,它不强制类型可复制、可移动甚至可构造,但它确实希望类型表达其完整契约,并且它希望实现程序员期望从 C++ 等语言获得的流畅设计模式。这里的(众所周知的)观察是,许多对象的内容可以在不需要禁用其析构函数的情况下被“窃取”,因为它们具有“空状态”(如可选类型或可空指针)或因为它们具有空可以高效创建的值和无需操作即可销毁的值(例如, std::vector其数据可以具有空指针)。

为了支持这些用例,consume 运算符支持任意 LValue,当应用于一个 LValue 时,它会调用“窃取移动构造函数”。此构造函数必须将新值设置为处于活动状态,并且可以改变旧值,但需要将其置于其析构函数仍将工作的状态。例如,如果我们想将我们放入FileDescriptor一个向量中并从中移出,我们可能会选择扩展它以了解它-1是一个哨兵,这意味着它是“空的”。我们可以这样实现:

# This is a simple wrapper around POSIX-style fcntl.h functions.struct FileDescriptor:    var fd: Int    # This is the new key capability.    fn __moveinit__(self&, existing&: Self):        self.fd = existing.fd        existing.fd = -1  # neutralize 'existing'.    fn __moveinit__(self&, consuming existing: Self): # as above    fn __init__(self&, fd: Int): # as above    fn __init__(self&, path: String): # as above    fn __del__(owning self):        if self.fd != -1:            close(self.fd)   # pseudo code, call close(2)

请注意“窃取移动”构造函数如何从现有值中获取文件描述符并改变该值,以便其析构函数不会执行任何操作。这种技术需要权衡取舍,并不是对所有类型都是最好的。我们可以看到它向析构函数添加了一个(廉价的)分支,因为它必须检查哨兵情况。使这样的类型可以为 null 通常也被认为是不好的形式,因为像类型这样的更通用的特性Optional[T]是处理这种情况的更好方法。

此外,我们计划在 Mojo 本身中实现Optional[T],并且Optional需要此功能。我们还相信库作者比语言设计者更了解他们的领域问题,并且通常更愿意让库作者在该领域拥有全部权力。因此,您可以选择(但不是必须)让您的类型以选择加入的方式参与此行为。

可复制类型

可移动类型的下一步是可复制类型。可复制类型也很常见——程序员通常希望像字符串和数组这样的东西是可复制的,每个 Python 对象引用都是可复制的——通过复制指针和调整引用计数。

有很多方法可以实现可复制类型。一种可以实现引用语义类型,如 Python 或 Java,在其中传播共享指针,一种可以使用易于共享的不可变数据结构,因为它们一旦创建就永远不会发生变化,一种可以通过写时延迟复制实现深层值语义就像斯威夫特一样。这些方法中的每一种都有不同的权衡,Mojo 认为虽然我们需要一些通用的集合类型集,但我们也可以支持广泛的专注于特定用例的专用集合类型。

在 Mojo 中,您可以通过实现__copyinit__方法来做到这一点。String这是一个使用简单伪代码的示例:

struct MyString:    var data: Pointer[Int8]    # StringRef is a pointer + length and works with StringLiteral.    def __init__(self&, input: StringRef):        self.data = ...    # Copy the string by deep copying the underlying malloc'd data.    def __copyinit__(self&, existing: Self):        self.data = strdup(existing.data)    # This isn't required, but optimizes unneeded copies.    def __moveinit__(self&, owned existing: Self):        self.data = existing.data    def __del__(owned self):        free(self.data.address)    def __add__(self, rhs: MyString) -> MyString: ...

这个简单的类型是一个指针,指向一个用 malloc 分配的“以 null 结尾”的字符串数据,为了清晰起见,使用了老式的 C API。它实现了__copyinit__,它保持不变性,即 MyString 的每个实例都拥有它们的底层指针并在销毁时释放它。这个实现建立在我们上面看到的技巧之上,并实现了一个__moveinit__构造函数,这允许它在一些常见情况下完全消除临时副本。您可以在此代码序列中看到此行为:

fn test_my_string():    var s1 = MyString("hello ")    var s2 = s1    # s2.__copyinit__(s1) runs here    print(s1)    var s3 = s1^   # s3.__moveinit__(s1) runs here    print(s2)    # s2.__del__() runs here    print(s3)    # s3.__del__() runs here

在这种情况下,您可以看到为什么需要复制构造函数:没有复制构造函数,将值复制s1到其中s2将是一个错误 - 因为您不能拥有相同的不可复制类型的两个活动实例。移动构造函数是可选的,但有助于对 into 的赋值s3:没有它,编译器将从 s1 调用复制构造函数,然后销毁旧s1实例。这在逻辑上是正确的,但引入了额外的运行时开销。

Mojo 急切地销毁值,这允许它频繁使用将复制+销毁对转换为移动操作,这可以带来比 C++ 更好的性能,而无需对std::move.

琐碎的类型

最灵活的类型是那些只是“比特袋”的类型。这些类型是“微不足道的”,因为它们可以在不调用自定义代码的情况下被复制、移动和销毁。像这样的类型可以说是我们周围最常见的基本类型:像整数和浮点值这样的东西都是微不足道的。从语言的角度来看,Mojo 不需要对它们的特殊支持,类型作者将这些东西实现为空操作并允许内联器让它们消失就完全没问题了。

这种方法不是最优的有两个原因:一个是我们不希望样板文件必须在琐碎的类型上定义一堆方法,其次,我们不希望编译时间开销生成和推送一个一堆函数调用,只是让它们内联到什么都没有。此外,还有一个正交问题,即这些类型中的许多在另一个方面是微不足道的:它们很小,应该在 CPU 的寄存器中传递,而不是在内存中间接传递。

因此,Mojo 提供了一个结构装饰器来解决所有这些问题。您可以使用装饰器实现一个类型@register_passable("trivial"),这会告诉 Mojo 该类型应该是可复制和可移动的,但它没有用于执行此操作的用户定义逻辑。它还告诉 Mojo 更喜欢在 CPU 寄存器中传递值,这可以带来效率优势。

TODO:该装饰器需要重新考虑。缺少自定义逻辑复制/移动/销毁逻辑和“寄存器中的可传递性”是正交问题,应该分开。这个前一个逻辑应该包含在一个更通用的@value("trivial")装饰器中,它与 正交@register_passable。

@value装潢师

Mojo 的方法(如上所述)提供了简单且可预测的钩子,使您能够正确表达奇异的低级事物Atomic。这对于控制和简单的编程模型来说非常有用,但我们编写的大多数结构都是其他类型的简单聚合,我们不想为它们编写大量样板文件!为了解决这个问题,Mojo@value为结构提供了一个装饰器,可以为您合成样板。@value可以被认为是 Python@dataclass处理 new__moveinit__和__copyinit__Mojo 方法的扩展。

装饰@value器查看您的类型的字段,并生成缺少的成员。考虑这样一个简单的结构,例如:

@valuestruct MyPet:    var name: String    var age: Int

Mojo 会注意到您没有成员初始化程序、移动构造函数或复制构造函数,并且会为您合成这些,就像您编写的一样:

fn __init__(self&, owned name: String, age: Int):    self.name = name^    self.age = agefn __copyinit__(self&, existing: Self):    self.name = existing.name    self.age = existing.agefn __moveinit__(self&, owned existing: Self):    self.name = existing.name^    self.age = existing.age

如果您的类型包含任何只能移动的字段,它当然不能(因此也不会)为您生成复制构造函数。Mojo 仅在它们不存在时为您合成它们,因此可以通过定义您自己的版本来覆盖其行为。例如,想要定义自定义复制构造函数但使用默认的逐成员构造函数和移动构造函数是很常见的。

目前没有办法抑制特定方法的生成或自定义生成,但@value如果有需求,我们可以向生成器添加参数来执行此操作。

请注意,@value装饰器仅适用于其成员可复制和/或可移动的类型。如果Atomic你的结构中有类似的东西,那么它可能不是值类型,而且你也不需要这些成员。

析构函数的行为

Mojo 中的任何结构都可以有一个析构函数,它会在值生命周期结束时自动运行。例如,一个简单的字符串可能如下所示(伪代码):

struct MyString:    var data: Pointer[Int8]    def __init__(self&, input: StringRef): ...    def __add__(self, rhs: MyString) -> MyString: ...    def __del__(owned self):        free(self.data.address)

Mojo 编译器会在值失效时自动调用析构函数,并为析构函数何时运行提供强有力的保证。Mojo 使用静态编译器分析来推理您的代码并决定何时插入对析构函数的调用。例如:

fn use_strings():    var a = MyString("hello a")    var b = MyString("hello b")    print(a)    # a.__del__() runs here    print(b)    # b.__del__() runs here    a = MyString("temporary a")    # a.__del__() runs here    other_stuff()    a = MyString("final a")    print(a)    # a.__del__() runs here

在上面的代码中,您会看到 和a值b是在早期创建的,并且值的每个初始化都与对析构函数的调用相匹配。还要注意调用发生的位置:在b变量中。例如,Mojo 在变量的(不相关的)打印中保持值一直存在a,直到变量打印出来b,并在调用后立即销毁它。该a值在第一次打印后立即销毁,在重新分配一个新的(未使用的)临时值后立即销毁,并在最终打印后立即销毁。

Mojo 使用“尽快” (ASAP) 策略销毁值,表现得像一个在每次调用后运行的超活跃垃圾收集器——当我们说每次调用时,我们是认真的!使用内部表达式(如a+b+c+d)的代码会在不需要中间表达式时急切地销毁它们——销毁不会像 C++ 中那样推迟到语句的末尾。Mojo 完全理解控制流,当然包括循环、ifs 和 try/except。

现在,这可能会让 C++ 程序员感到惊讶:这使 C++ 程序员广泛使用的RAII 模式的使用无效。那么,为什么 Mojo 如此热切地销毁东西而不是使用 C++ 风格的范围销毁?好吧,我很高兴你问,有很多很好的理由!

与 C++ 模型相比,Mojo 设计具有许多强大的优势:

回想一下,Python 实际上并没有超出整个函数的作用域,Mojo 需要提供一个可行的模型,该模型在存在 Python 风格的“def”的情况下能够正确运行。

因为 Python 不对对象销毁提供强有力的保证,所以它不鼓励 RAII 模式。为了解决 RAII 模式,Mojo(和 Python)提供了一种with statement提供对资源的范围访问的方法,这比 RAII 更谨慎,语法更清晰。

Mojo 方法消除了类型实现重新赋值运算符的需要,就像C++ 中的operator=(const T&)和 一样operator=(T&&),使得定义类型和消除概念变得更容易。

Mojo 不允许可变引用与其他可变引用或不可变借用重叠。它提供可预测的编程模型的一种主要方式是确保对对象的引用尽快结束,避免编译器认为一个值可能仍然存在并干扰另一个值的混乱情况,但这并不清楚用户。

在最后使用时销毁值与“移动”优化很好地结合在一起,它将“复制+删除”对转换为值的“移动”,这是 C++ 移动优化的泛化,如 NRVO。

在 C++ 中销毁作用域末尾的值对于尾递归等一些常见模式来说是有问题的,因为析构函数调用发生在尾调用之后。对于某些函数式编程模式,这可能是一个重要的性能和内存问题。

Mojo 方法更类似于 Rust 和 Swift 的工作方式,因为它们都具有强大的价值所有权跟踪并提供内存安全。一个区别是它们的实现需要使用动态“丢弃标志” ——它们维护隐藏的影子变量以跟踪您的值的状态以提供安全性。这些通常会被优化掉,但 Mojo 方法完全消除了这种开销,使生成的代码更快并避免歧义。

现场敏感寿命管理

除了 Mojo 的生命周期分析是完全控制流感知之外,它也是完全字段敏感的(结构的每个字段都是独立跟踪的)。它单独跟踪“整个对象”是用初始化器初始化还是用整个对象析构函数销毁。例如,考虑这段代码:

struct TwoStrings:    var str1: MyString    var str2: MyString    fn __init__(self&): ...    fn __del__(owned self): ...fn use_two_strings():    var ts = TwoStrings()    # ts.str1.__del__() runs here    other_stuff()    ts.str1 = MyString("hello a")     # Overwrite ts.str1    print(ts.str1)    # ts.__del__() runs here

请注意,该ts.str1字段在设置后立即销毁,因为 Mojo 知道它会在下面被覆盖。你也可以在使用 consume 运算符时看到这一点,例如:

fn consume_and_use_two_strings():    var ts = TwoStrings()    consume(ts.str1^)    # ts is partially initialized here!    other_stuff()    ts.str1 = MyString()  # All together now    use(ts)               # This is ok    # ts.__del__() runs here

请注意,代码使用了其中一个字段:在 的持续时间内other_stuff(),该str1字段完全未初始化。幸运的是,对于上面的代码,str1它在被use函数使用之前被重新初始化——如果不是,Mojo 会拒绝带有未初始化字段错误的代码。

Mojo 在这方面的规则非常强大且有意直截了当:可以临时使用字段,但“整个对象”必须使用聚合类型的初始化器构造并使用聚合析构函数销毁。这意味着不可能通过初始化其字段来创建对象,也不可能通过销毁其字段来拆除对象:

fn consume_and_use_two_strings():    var ts = TwoStrings()    consume(ts.str1^)    consume(ts.str2^)    # Error: cannot run the 'ts' destructor without initialized fields.    var ts2 : TwoStrings    ts2.str1 = MyString()  # All together now    ts2.str2 = MyString()  # All together now    use(ts2) # Error: 'ts2' isn't fully initialized

虽然我们可以允许这样的模式发生,但我们拒绝这种模式,因为“一个值不仅仅是其部分的总和”。FileDescriptor考虑包含 POSIX 文件描述符作为整数值的a 。例如 - 销毁整数(空洞!)和销毁FileDescriptor(它可能调用close()系统调用)之间存在很大差异。正因为如此,我们要求所有的全值初始化都通过初始化器并用它们的全值析构函数销毁。

就其价值而言,Mojo 确实在内部具有与 Rust 的“ mem::forget ”功能等效的功能,它显式禁用析构函数并具有相应的“祝福”对象的内部功能,但它们不会暴露给用户使用这点。

场寿命__init__

方法的行为__init__几乎与任何其他方法一样——有一点神奇之处:它知道对象的字段未初始化,但它认为整个对象已初始化。这意味着您可以在初始化所有字段后立即将“self”用作整个对象:

struct TwoStrings:    var str1: MyString    var str2: MyString    fn __init__(self&, cond: Bool, other: MyString):        self.str1 = MyString()        if cond:            self.str2 = other            use(self)  # Safe to use immediately!            # self.str2.__del__(): destroyed because overwritten below.        self.str2 = self.str1        use(self)  # Safe to use immediately!

类似地,Mojo 中的初始化器完全覆盖 是完全安全的self,例如通过委托给其他初始化器:

struct TwoStrings:    var str1: MyString    var str2: MyString    fn __init__(self&): ...    fn __init__(self&, cond: Bool, other: MyString):        self = TwoStrings()  # basic        self.str1 = MyString("fancy")

和owned中参数的字段生命周期__del____moveinit__

析构函数和移动初始值设定项的“拥有”参数存在最后一点魔力。回顾一下,这些方法的定义如下:

struct TwoStrings:    var str1: MyString    var str2: MyString    fn __init__(...)    fn __moveinit__(self&, owned existing: Self): ...    fn __del__(owned self): ...

这些方法面临一个有趣但晦涩难懂的问题:这两个方法都负责拆解owned existing/self值,要么销毁与之相关的子元素,要么使用它们为自己的类型实现删除逻辑。移动构造函数希望self通过从现有实例中窃取部分来创建新实例。因此,他们都想消耗和转换“拥有”值的元素,并且绝对不希望拥有的值析构函数运行!最令人震惊的例子是方法__del__,它会变成无限循环。

为了解决这个问题,Mojo 通过假设它们的全部值在到达方法的任何返回值时被销毁来特殊处理这两个方法。这意味着可以在使用字段值之前使用整个对象。例如,这按您预期的那样工作:

struct TwoStrings:    var str1: MyString    var str2: MyString    fn __init__(...)    fn __moveinit__(self&, owned existing: Self): ...    fn __del__(owned self):        log(self)       # Self is still whole        # self.str2.__del__(): Mojo destroys str2 since it isn't used        consume(^str1)        # Everything has now been consumed, no destructor is run on self.

你通常不必考虑这一点,但如果你有内部指针指向成员的逻辑,你可能需要为析构函数中的某些逻辑保持它们的活动状态或移动初始化程序本身。您可以通过分配给丢弃模式来做到这一点:

fn __del__(owned self):    log(self) # Self is still whole    consume(^str1)    _ = self.str2    # self.str2.__del__(): Mojo destroys str2 after its last use.

在这种情况下,如果“消费”以某种方式隐含地指代某种价值str2,

这将确保 str2 在模式访问最后一次使用之前不会被销毁_。

寿命

TODO:解释返回引用是如何工作的,绑定到与参数相吻合的生命周期中。这还没有启用。

类型特征

这是一个非常类似于 Rust traits 或 Swift 协议或 Haskell 类型类的特性。注意,这还没有实现。

高级/晦涩的 Mojo 功能

本节描述了对于构建标准库的最底层很重要的高级用户功能。这一级别的堆栈包含狭窄的功能,需要具有编译器内部经验才能有效理解和利用。

@always_inline装潢师

@always_inline("nodebug"):同样的事情,但没有调试信息,所以你不会进入 Int 的 + 方法。

@parameter装潢师

装饰@parameter器可以放置在捕获运行时值的嵌套函数上,以创建“参数化”捕获闭包。这是 Mojo 中的一个不安全的功能,因为我们目前没有对 capture-by-reference 的生命周期进行建模。此功能的一个特殊方面是它允许捕获运行时值的闭包作为参数值传递。

魔法运算符

C++ 代码有许多与值生命周期相交的神奇运算符,例如“placement new”、“placement delete”和“operator=”,它们会重新分配现有值。当您使用 Mojo 的所有语言功能并在安全构造之上进行组合时,Mojo 是一种安全的语言,但任何堆栈都是 C 风格指针和猖獗的不安全性的世界。Mojo 是一种实用语言,由于我们对与 C/C++ 进行互操作以及直接在 Mojo 本身中实现安全结构(如 String)感兴趣,因此我们需要一种方法来表达不安全的事物。

Mojo 标准库Pointer[element_type]类型是通过 MLIR 中的底层!pop.pointer类型实现的,我们希望有一种方法可以在 Mojo 中实现这些 C++ 等效的不安全结构。最终,这些将迁移到 Pointer 类型上的所有 being 方法,但在那之前,一些需要作为内置运算符公开。

直接访问 MLIR

Mojo 提供对 MLIR 方言和生态系统的完全访问。请查看Mojo 中的低级 IR以了解如何使用__mlir_type, __mlir_op,__mlir_type构造。所有内置函数和标准库都是通过调用底层 MLIR 结构来实现的,Mojo 有效地充当了 MLIR 之上的语法糖。

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
Python 和 JS 有什么相似?
一门号称比Python快68000倍的新型AI编程语言
Hello, Mojo——首次体验Mojo语言
bind,call,apply模拟实现
为什么 Python 没有函数重载?如何用装饰器实现函数重载?
Mojo编程语言开放下载,声称比Python快68000倍
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服