Mojo🔥 编程手册

您所在的位置:网站首页 可变参数python代码 Mojo🔥 编程手册

Mojo🔥 编程手册

2024-07-14 21:02| 来源: 网络整理| 查看: 265

浏览 4233 扫码 分享 2023-09-10 16:44:22 使用 Mojo 编译器基础系统编程扩展let 和 var 声明struct 类型Int 和 int 对比强类型检查重载函数和方法fn 定义copyinit 和 moveinit 特殊方法参数传递控制和内存所有权为什么参数约定很重要?不可变参数(借用)(borrowed)

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 world963$

同样,您可以使用 .🔥 或 .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.14your_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 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 = ab.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能够提供更易于使用的编程模型。

若有收获,就点个赞吧

0 人点赞

上一篇: 下一篇:


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3