Mojo 是一种易于使用且具有 C++ 和 Rust 性能的编程语言。此外,Mojo 还提供了利用整个 Python 库生态系统的能力。
Mojo 通过利用下一代编译器技术实现集成缓存、多线程和云分布技术来实现这一点。此外,Mojo 的自动调优和编译时元编程功能使您能够编写可移植到甚至最奇特硬件的代码。
更重要的是,Mojo 允许您利用整个 Python 生态系统,因此您可以继续使用您熟悉的工具。Mojo 的设计目标是随着时间的推移成为 Python 的超集,同时保留 Python 的动态特性,并添加新的系统编程原语。这些新的系统编程原语将使 Mojo 开发人员能够构建目前需要 C、C++、Rust、CUDA 和其他加速器系统的高性能库。通过将动态语言和系统语言的最佳实践结合起来,我们希望提供一个跨抽象层次、对初学者友好且可扩展多个用例(从加速器到应用程序编程和脚本)的统一编程模型。
本文档是对 Mojo 编程语言的介绍,而不是完整的语言指南。它假定了解 Python 和系统编程概念。目前,Mojo 仍处于开发中,文档针对具有系统编程经验的开发人员。随着该语言的不断发展和更广泛地可用性,我们打算使其对每个人都友好和易于访问,包括初学者程序员。但它当前尚不完善。
使用 Mojo 编译器
您可以从终端运行 Mojo 程序,就像可以运行 Python 程序一样。因此,如果您有一个名为 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 world
9
6
3
$
同样,您可以使用 .🔥 或 .mojo 后缀。
基础系统编程扩展
鉴于我们的兼容性目标和 Python 在高级应用程序和动态 API 方面的优势,我们不必花太多时间解释该语言的这些部分如何工作。另一方面,Python 对系统编程的支持主要委托给 C,我们希望提供一个在这个领域表现出色的单一系统。因此,本节将详细分解每个主要组件和功能,并描述如何使用它们以及示例。
let 和 var 声明
在 Mojo 的 def 中,你可以给一个变量赋值,它就像 Python 一样隐式地创建了一个函数作用域变量。这种方式提供了一种非常动态和简洁的编程方式,但是也带来了一些挑战:
- 系统程序员通常希望声明一个值是不可变的,以获得类型安全和性能优势。
- 如果他们在赋值时拼写错误了变量名,他们可能希望得到一个错误提示。
为了支持这些需求,Mojo 提供了受限制的运行时值声明:let 是只读的,var 是可变的。这些值使用词法作用域,并支持名称遮蔽:
def your_function(a, b):
let c = a
# Uncomment to see an error:
# c = b # error: c is immutable
if c != b:
let d = b
print(d)
your_function(2, 3)
3
let 和 var 声明支持类型说明符以及模式,还支持延迟初始化。
def your_function():
let x: Int = 42
let y: Float64 = 17.0
let z: Float32
if x != 0:
z = 1.0
else:
z = foo()
print(z)
def foo() -> Float32:
return 3.14
your_function()
1.0
请注意,当在 def 函数中使用 let 和 var 时(您可以改用隐式声明的值,就像 Python 一样),它们是可选的,但在fn函数中的所有变量都必需使用它们。
还要注意,当在 REPL 环境中使用 Mojo(例如这个笔记本)时,顶级变量(在函数或结构体之外存在的变量)被视为def 中的变量,因此它们允许隐式值类型声明(不需要 var 或 let 声明,也不需要类型声明)。这与Python REPL的行为相匹配。
struct 类型
Mojo基于MLIR和LLVM,它们提供了一种用于许多编程语言的尖端编译器和代码生成系统。这使我们能够更好地控制数据组织、直接访问数据字段,以及其他提高性能的方法。现代系统编程语言的一个重要特点是能够在这些复杂、低级操作的基础上构建高级且安全抽象,而不会损失任何性能。在Mojo中,这是通过结构类型提供的。
在Mojo中,一个结构类似于Python类:它们都支持方法、字段、运算符重载、元编程的装饰器等。它们的区别如下:
Python类是动态的:它们允许动态分派、“猴子补丁”(或“交换”)以及在运行时动态绑定实例属性。
Mojo结构是静态的:它们在编译时绑定(您不能在运行时添加方法)。结构允许您在保证安全性和易用性的同时,用性能来换取灵活性。
下面是一个简单的结构定义示例:
struct MyPair:
var first: Int
var second: Int
# We use 'fn' instead of 'def' here - we'll explain that soon
fn __init__(inout self, first: Int, second: Int):
self.first = first
self.second = second
fn __lt__(self, rhs: MyPair) -> Bool:
return self.first < rhs.first or
(self.first == rhs.first and
self.second < rhs.second)
在语法上,与 Python 类相比,结构体最大的区别在于,结构体中的所有实例属性必须使用 var 或 let 声明进行显式声明。
在Mojo中,“结构体”的结构和内容是预先设置的,程序运行时不能更改。与 Python 不同,您可以在运行时添加、删除或更改对象的属性,Mojo 不允许在结构体上这样做。这意味着您不能在程序运行过程中使用del删除方法或更改其值。
然而,结构的静态性质具有一些巨大的好处!它有助于 Mojo 更快地运行代码。程序确切地知道在哪里可以找到结构体的信息以及如何使用它,而无需任何额外的步骤或延迟。
Mojo 的结构体还与您可能已经从 Python 知道的许多功能完美配合,例如运算符重载(允许您更改像 + 和 - 这样的数学符号与您自己的数据一起工作的方式)。此外,所有“标准类型”(如Int、Bool、String甚至Tuple)都是使用结构体创建的。这意味着它们是您可以使用的标准工具集的一部分,而不是硬编码到语言本身中。这使您在编写代码时拥有更多的灵活性和控制权。
如果你想知道self参数上的inout是什么意思:这表示该参数是可变的,并且函数内所做的更改对调用者可见。有关详细信息,请参阅下面的关于inout参数的内容。
Int 和 int 对比
在 Mojo 中,你可能会注意到我们使用 Int
(大写字母“I”),这与 Python 的 int
(小写字母“i”)不同。这种差异是有意为之的,实际上是一件好事!
在 Python 中,int 类型可以处理非常大的数字,并具有一些额外的功能,例如检查两个数字是否是相同的对象。但是这会带来一些额外的负担,可能会降低速度。 Mojo 的 Int 是不同的。它的设计简单、快速,并且针对计算机硬件进行了优化,以便快速处理。
我们做出这个选择的主要原因有两个:
我们希望为需要密切与计算机硬件合作的程序员(系统程序员)提供透明和可靠的方式与硬件进行交互。我们不希望依赖花哨的技巧(如 JIT 编译器)来使事情变得更快。
我们希望 Mojo 能够与 Python 良好地协同工作而不会引起任何问题。通过使用不同的名称(Int 而不是 int ),我们可以在不改变 Python 的 int 的工作方式的情况下,在 Mojo 中保留这两种类型。
作为额外的好处,Int 遵循与其他您可能在 Mojo 中创建的自定义数据类型相同的命名风格。此外,Int 是一个结构体,包含在 Mojo 的标准工具集中。
强类型检查
虽然你仍然可以使用像 Python 中的灵活类型,Mojo 允许你使用严格的类型检查。类型检查可以使你的代码更加可预测、可管理、安全。
使用强类型检查的主要方式之一是使用 Mojo 的 struct 类型。Mojo 中的结构体定义定义了一个编译时绑定的名称,并且在类型上下文中对该名称的引用被视为正在定义的值的强规范。例如,考虑以下使用上面所示的 MyPair 结构的代码:
def pair_test() -> Bool:
let p = MyPair(1, 2)
# Uncomment to see an error:
# return p < 4 # gives a compile time error
return True
如果你取消注释第一个返回语句并运行它,你会得到一个编译时错误,告诉你 4 不能转换为 MyPair,这是lt()的右操作数(在 MyPair 定义中)所需要的。
在进行系统编程时,这是一个熟悉的感觉,但这不是 Python 的工作方式。Python 具有与 MyPy 类型注释语法相同的功能,但它们不是由编译器强制执行的:相反,它们是通知静态分析的提示。通过将类型与特定声明联系起来,Mojo可以在不破坏兼容性的情况下处理经典的类型注释提示和强类型规范。
类型检查不是强类型的唯一用例。由于我们知道类型是准确的,我们可以根据这些类型优化代码,将值传递到寄存器中,并在参数传递和其他低级细节方面与 C 一样高效。这是 Mojo 向系统程序员提供的安全性和可预测性保证的基础。
重载函数和方法
和 Python 一样,在 Mojo 中也可以定义没有指定参数数据类型的函数,Mojo 会动态地处理它们。当你希望拥有一个表达性的API,能够接受任意输入并让动态调度来决定如何处理数据时,这是非常方便的。然而,正如上文所讨论的,当你想要确保类型安全时,Mojo 也提供了对重载函数和方法的完整支持。
这使得你可以定义多个具有相同名称但参数不同的函数。这是许多语言中常见的特性,例如 C++、Java 和 Swift。
在解析函数调用时,Mojo会尝试每个候选者并使用其中一个有效的(如果只有一个有效的话),或者它会选择最接近的匹配(如果能确定一个接近的匹配),或者如果不能确定要选择哪个函数,它会报告调用是模糊的。在这种情况下,你可以在调用站点添加一个显式的类型转换来解决歧义。
让我们看一个例子:
struct Complex:
var re: Float32
var im: Float32
fn __init__(inout self, x: Float32):
"""Construct a complex number given a real number."""
self.re = x
self.im = 0.0
fn __init__(inout self, r: Float32, i: Float32):
"""Construct a complex number given its real and imaginary components."""
self.re = r
self.im = i
你可以在结构体和类中重载方法,也可以重载模块级别的函数。
Mojo不支持仅基于结果类型进行重载,也不使用结果类型或上下文类型信息进行类型推断,以保持简单、快速和可预测。Mojo永远不会生成“表达式过于复杂”的错误,因为它的类型检查器在定义上是简单而快速的。
另外,如果你不给出参数名称的类型定义,那么函数的行为就像具有动态类型的Python一样。一旦你定义了一个参数类型,Mojo 就会像上面描述的那样查找重载候选者并解析函数调用。
虽然我们还没有讨论过参数(它们与函数参数不同),但你还可以基于参数重载函数和方法。
fn
定义
上述扩展是提供低级编程和抽象能力的基石,但是许多系统程序员更喜欢比 Mojo 提供的 def 更多的控制和可预测性。要概括一下,由于必要性,def 被定义为非常动态、灵活且通常与 Python 兼容:参数是可变的,第一次使用时隐式声明局部变量,并且不强制使用作用域。这对于高级编程和脚本非常棒,但对于系统编程并不总是很好。为了补充这一点,Mojo 提供了一个类似于“严格模式”的 fn 声明。
替代方案:我们不必使用一个新的关键字,比如fn,而是可以使用修饰符或装饰器,比如@strict def。然而,我们需要使用新的关键字,并且这样做的代价很小。此外,在实践中,在系统编程领域中,fn经常被使用,因此将其作为第一类可能是有意义的。
对于调用者来说,fn 和 def 是可互换的:def 可以提供的功能,fn 也可以提供(反之亦然)。不同的是,fn在其函数体内部更为有限且受控(或者可以说是严谨和严格的)。具体而言,与 def 函数相比,fn 具有一些限制:
参数值默认在函数体中不可变(类似于 let ),而不是可变的(类似于 var )。这捕捉到了意外的变异,并允许使用不可复制的类型作为参数。
参数值需要指定类型(方法中的 self 除外),捕捉到意外省略类型说明的情况。类似地,缺少返回类型说明符被解释为返回 None 而不是未知的返回类型。请注意,这二者都可以显式声明为返回对象,如果需要的话,这允许人们选择加入 def 的行为。
隐式声明局部变量被禁用,因此所有局部变量都必须声明。这捕捉到了名称拼写错误并与 let 和 var 提供的作用域相吻合。
两者都支持引发异常,但这必须使用 raises 关键字在 fn 上显式声明。
编程模式在团队中会有很大的差异,这种严格程度并不适合每个人。我们预计那些习惯于 C++ 并在 Python 中使用 MyPy 风格的类型注释的人更喜欢使用fns,但是高级程序员和机器学习研究人员将继续使用 def。Mojo 允许你自由地混合使用 def 和 fn 声明,例如用一种实现某些方法,用另一种实现其他方法,并允许每个团队或程序员根据自己的用例来决定哪种最好。
有关 Mojo 函数中的参数行为的更多信息,请参阅以下有关参数传递控制和内存所有权的部分。
copyinit 和 moveinit 特殊方法
Mojo支持完全的“值语义”,就像在C++和Swift等语言中一样,它使用@value装饰器使定义简单的字段聚合变得非常容易。
对于高级用例,Mojo允许您使用 Python 现有的 __init__
特殊方法定义自定义构造函数,使用现有的 _del__
特殊方法定义自定义析构函数,并使用新的 __copyinit__
和 `_moveinit` 特殊方法定义自定义复制和移动构造函数。
这些低级定制挂钩在执行低级别系统编程时非常有用,例如进行手动内存管理。例如,考虑一个动态字符串类型,在构造时需要为字符串数据分配内存,并在值被销毁时销毁它:
from memory.unsafe import Pointer
struct HeapArray:
var data: Pointer[Int]
var size: Int
var cap: Int
fn __init__(inout self):
self.cap = 16
self.size = 0
self.data = Pointer[Int].alloc(self.cap)
fn __init__(inout self, size: Int, val: Int):
self.cap = size * 2
self.size = size
self.data = Pointer[Int].alloc(self.cap)
for i in range(self.size):
self.data.store(i, val)
fn __del__(owned self):
self.data.free()
fn dump(self):
print_no_newline("[")
for i in range(self.size):
if i > 0:
print_no_newline(", ")
print_no_newline(self.data.load(i))
print("]")
这个数组类型使用低级函数实现,以展示其工作原理的简单示例。然而,如果您尝试使用 = 运算符复制一个 HeapArray 实例,您可能会感到惊讶:
var a = HeapArray(3, 1)
a.dump() # Should print [1, 1, 1]
# Uncomment to see an error:
# var b = a # ERROR: Vector doesn't implement __copyinit__
var b = HeapArray(4, 2)
b.dump() # Should print [2, 2, 2, 2]
a.dump() # Should print [1, 1, 1]
[1, 1, 1]
[2, 2, 2, 2]
[1, 1, 1]
如果你取消注释将a复制到b的行,你会看到 Mojo 不允许你复制我们的数组:HeapArray
包含一个指向 Pointer 实例的指针(相当于低级 C 指针),而 Mojo 不知道它指向什么类型的数据或如何复制它。更一般地说,某些类型(如原子编号)不能被复制或移动,因为它们的地址提供了与类实例相同的标识。
在这种情况下,我们确实希望我们的数组是可复制的。要启用此功能,我们必须实现__copyinit__
特殊方法,通常按照以下方式实现:
struct HeapArray:
var data: Pointer[Int]
var size: Int
var cap: Int
fn __init__(inout self):
self.cap = 16
self.size = 0
self.data = Pointer[Int].alloc(self.cap)
fn __init__(inout self, size: Int, val: Int):
self.cap = size * 2
self.size = size
self.data = Pointer[Int].alloc(self.cap)
for i in range(self.size):
self.data.store(i, val)
fn __copyinit__(inout self, other: Self):
self.cap = other.cap
self.size = other.size
self.data = Pointer[Int].alloc(self.cap)
for i in range(self.size):
self.data.store(i, other.data.load(i))
fn __del__(owned self):
self.data.free()
fn dump(self):
print_no_newline("[")
for i in range(self.size):
if i > 0:
print_no_newline(", ")
print_no_newline(self.data.load(i))
print("]")
通过这种实现,上面的代码可以正确工作,并且 b = a 复制会生成具有其自身生命周期和数据的逻辑上不同的数组实例:
var a = HeapArray(3, 1)
a.dump() # Should print [1, 1, 1]
# This is no longer an error:
var b = a
b.dump() # Should print [1, 1, 1]
a.dump() # Should print [1, 1, 1]
[1, 1, 1]
[1, 1, 1]
[1, 1, 1]
Mojo 还支持 __moveinit__
方法,它允许 Rust 风格的移动(在生命周期结束时取值)和 C++ 风格的移动(移除值的内容但仍运行析构函数),并允许定义自定义移动逻辑。有关更多详细信息,请参阅下面的值生命周期部分。
Mojo 提供了对值生命周期的完全控制,包括使类型可复制、只可移动和不可移动的能力。这比 Swift 和 Rust 等语言提供的控制更多,这些语言要求值至少是可移动的。如果您想知道现有值如何被传递给 __copyinit__
方法而不会创建副本,请查看下面的借用参数部分。
参数传递控制和内存所有权
在 Python 和 Mojo 中,语言的大部分围绕着函数调用展开:许多(显然)内置的行为都是在标准库中通过双下划线方法实现的。在这些魔法函数内部,通过参数传递确定了许多内存所有权。
让我们回顾一下 Python 和 Mojo 如何传递参数的一些细节:
所有传递给Python def函数的值都使用引用语义。这意味着函数可以修改传递给它的可变对象,并且这些更改在函数外部是可见的。然而,对于不熟悉的人来说,这种行为有时会让人感到意外,因为你可以改变一个参数所指向的对象,而这些更改在函数外部是不可见的。
默认情况下,传递给Mojo def函数的所有值都使用值语义。与Python相比,这是一个重要的区别:Mojo def函数接收所有参数的副本 - 它可以在函数内部修改参数,但这些更改在函数外部是不可见的。
默认情况下,传递给Mojo fn函数的所有值都是不可变的引用。这意味着函数可以读取原始对象(它不是副本),但它根本无法修改该对象。
这个在 Mojo fn
中传递不可变参数的约定被称为“借用”。在接下来的部分,我们将解释如何在 Mojo 中更改 def 和 fn 函数的参数传递行为。
为什么参数约定很重要?
在 Python 中,所有基本值都是对对象的引用 - 如上所述,Python 函数可以修改原始对象。因此,Python 开发人员习惯于将所有内容视为引用语义。然而,在 CPython 或机器级别上,可以看到引用本身实际上是通过复制传递的 - Python 复制指针并调整引用计数。
这种 Python 方法为大多数人提供了一个舒适的编程模型,但它要求所有值都使用堆分配(并且结果有时会因引用共享而出现意外的结果)。Mojo 类对于大多数对象遵循相同的引用语义方法,但对于系统编程上下文中的简单类型(如整数),这种方法并不实用。在这些情况下,我们希望值在堆栈上或甚至硬件寄存器中存在。因此,Mojo结构体总是内联到其容器中,无论是作为另一个类型的字段还是包含函数的堆栈帧。
这引发了一些有趣的问题:如何实现需要修改结构类型本身的 self 的方法,例如__iadd__
?let 是如何工作的,以及它是如何防止变异的?如何控制这些值的生命周期以保持 Mojo 语言的内存安全?
答案是Mojo编译器使用数据流分析和类型注释来提供对值副本、引用别名和变异控制的全面控制。这些功能在许多方面与 Rust 语言中的类似,但它们的工作方式略有不同,以便使Mojo更容易学习,并且它们更好地集成到 Python 生态系统中,而不需要大量的注释负担。
在接下来的部分中,您将学习如何控制传递给 Mojo fn 函数的对象的内存所有权。
不可变参数(借用)(borrowed)
借用对象是对函数接收到的对象的不可变引用,而不是接收对象的副本。因此,被调用函数具有对该对象的完全读取和执行访问权限,但它无法修改它(调用者仍然拥有对象的独占“所有权”)。
例如,考虑以下结构体,当我们传递它的实例时,我们不希望进行复制:
# Don't worry about this code yet. It's just needed for the function below.
# It's a type so expensive to copy around so it does not have a
# __copyinit__ method.
struct SomethingBig:
var id_number: Int
var huge: HeapArray
fn __init__(inout self, id: Int):
self.huge = HeapArray(1000, 0)
self.id_number = id
# self is passed by-reference for mutation as described above.
fn set_id(inout 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)
将 SomethingBig
的实例传递给函数时,需要传递引用,因为 SomethingBig
无法被复制(它没有 __copyinit__
方法)。而且,如上所述,fn 参数默认是不可变的引用,但您可以使用借用关键字显式定义它,如图中的 use_something_big()
函数所示:
fn use_something_big(borrowed a: SomethingBig, b: SomethingBig):
"""'a' and 'b' are both immutable, because 'borrowed' is the default."""
a.print_id()
b.print_id()
let a = SomethingBig(10)
let b = SomethingBig(20)
use_something_big(a, b)
10
20
这个默认值适用于所有参数,包括方法的 self 参数。当传递大型值或像引用计数指针这样的昂贵值(这是 Python/Mojo 类的默认值)时,这要高效得多,因为传递参数时不需要调用复制构造函数和析构函数。
由于 fn 函数的默认参数约定是借用,Mojo 具有默认情况下做正确事情的简单而逻辑的代码。例如,我们不想复制或移动整个 SomethingBig
来调用 print_id()
方法,或者在调用 use_something_big()
时。
这种借用参数约定在某些方面类似于 C++ 中通过 const&
传递参数,它避免了值的复制并在被调用者中禁用了可变性。然而,借用约定与 C++ 中的 const& 在两个重要方面不同:
Mojo 编译器实现了借用检查器(类似于 Rust),防止在存在不可变引用的情况下动态形成对值的可变引用,并防止对同一值的多个可变引用。允许多个借用(如上面 use_something_big
所做的调用),但不允许同时传递可变引用和借用(尚未启用)。
像 Int、Float 和 SIMD 这样的小值直接在机器寄存器中传递,而不是通过额外的间接引用(这是因为它们使用 @register_passable
装饰器声明)。与 C++ 和 Rust 等语言相比,这在性能上有显著提升,并将此优化从每个调用站点变为类型上的声明性。
与 Rust 类似,Mojo 的借用检查器强制执行不变式的唯一性。Rust 和 Mojo 之间的主要区别在于 Mojo 不需要调用方端上的标记来通过借用传递。此外,Mojo 在传递小型值时更高效,而 Rust 默认为移动值而不是通过借用传递它们。这些策略和语法决策使 Mojo能够提供更易于使用的编程模型。