Copyright 2023 Modular, Inc: Licensed under the Apache License v2.0 with LLVM Exceptions.
Mojo 语言基础
Mojo 是一种功能强大的编程语言,主要用于高性能系统编程,因此与 Rust 和 C++ 等其他系统语言有很多共同之处。然而,Mojo 也被设计成 Python 的超集,因此您可能从 Python 中了解到的许多语言特性和概念都可以很好地转换到 Mojo 中。
例如,如果您在 REPL 环境或 Jupyter 笔记本(如本文档)中,您可以像 Python 一样运行顶层代码:
In [1]:
print("Hello Mojo!")
Hello Mojo!
这在其他系统编程语言中通常是看不到的。
Mojo 保留了 Python 的动态特性和语言语法,甚至允许从 Python 包中导入和运行代码。不过,重要的是要知道,Mojo 是一种全新的语言,而不仅仅是一种带有语法糖的 Python 新实现。Mojo 将 Python 语言提升到了一个全新的水平,包括系统编程功能、强大的类型检查、内存安全、新一代编译器技术等。然而,它仍然被设计成一种简单的语言,适用于通用编程。
本页将温和地介绍 Mojo 语言,只需要一点点编程经验。让我们开始吧!
如果您是一位经验丰富的系统程序员,并希望深入了解该语言,请查阅 Mojo 编程手册。
语言基础
首先,Mojo 是一种编译语言,它的许多性能和内存安全特性都源于此。Mojo 代码可以进行超前编译(AOT)或实时编译(JIT)。
与其他编译语言一样,Mojo 程序(.mojo
或 .🔥
文件)需要一个 main()
函数作为程序的入口。例如:
In [2]:
fn main(): var x: Int = 1 x += 1 print(x)
如果你了解 Python,你可能会想到函数名应该是 def main()
而不是 fn main()
。实际上,这两种方法在 Mojo 中都能使用,但使用 fn
时的行为有些不同,我们将在下文讨论。
当然,如果您构建的是一个 Mojo 模块(一个 API 库),而不是一个 Mojo 程序,那么您的文件就不需要 main()
函数(因为它将被其他有 main()
函数的程序导入)。
注意: 注意:在 .mojo
/.🔥
文件中编写代码时,不能运行本页所示的顶层代码--Mojo 程序或模块中的所有代码都必须封装在函数或结构体中。不过,顶层代码可以在 REPL 或 Jupyter Notebook(如当前的 Jupyter Notebook)中运行。
现在让我们来解释一下这个 main()
函数中的代码。
语法和语义
这很简单: Mojo 支持(或将支持)所有 Python 语法和语义。如果您不熟悉 Python 的语法,网上有大量的资源可以教您。
例如,与 Python 一样,Mojo 使用换行和缩进来定义代码块(而不是大括号),并且 Mojo 支持所有 Python 的控制流语法,如 if
条件和 for
循环。
不过,Mojo 仍在不断完善中,所以有些来自 Python 的功能还没有在 Mojo 中实现(参见 Mojo 路线图)。所有缺失的 Python 功能都会及时出现,但 Mojo 已经包含了许多 Python 以外的功能和特性。
因此,以下章节将重点介绍 Mojo 独有的一些语言特性(与 Python 相比)。
函数
Mojo 函数可以用 fn
(如上图所示)或 def
(如 Python)来声明。fn
声明强制执行强类型和内存安全行为,而 def
则提供 Python 风格的动态行为。
fn
和 def
函数都有其价值,学习这两种函数非常重要。不过,在本介绍中,我们将只关注 fn
函数。有关这两种函数的更多详情,请参阅编程手册。
在以下章节中,您将了解 fn
函数如何在代码中执行强类型和内存安全行为。
变量
您可以使用 var
声明变量(如上文 main()
函数中的 x),以创建可变值,或使用 let
声明变量,以创建不可变值。
error: Expression [15]:7:5: expression must be mutable for in-place operator destination x += 1 ^
这是因为 let
使值不可变,所以不能递增。
如果完全删除 var
,就会出错,因为 fn
函数需要明确的变量声明(与 Python 风格的 def
函数不同)。
后,请注意 x
变量有明确的 Int
类型说明。fn
中的变量并不需要声明类型,但有时需要。如果省略,Mojo 会推断类型,如图所示:
In [3]:
fn do_math(): let x: Int = 1 let y = 2 print(x + y) do_math()
3
函数参数和返回值
虽然函数体中声明的变量不需要类型,但 fn
函数的参数和返回值需要类型。
例如,下面是如何将 Int
声明为函数参数和返回值的类型:
In [4]:
fn add(x: Int, y: Int) -> Int: return x + y z = add(1, 2) print(z)
3
可选参数和关键字参数
您还可以指定参数默认值(也称为可选参数),并使用关键字参数名称传递值。例如:
In [5]:
fn pow(base: Int, exp: Int = 2) -> Int: return base ** exp # Uses default value for `exp` z = pow(3) print(z) # Uses keyword argument names (with order reversed) z = pow(exp=3, base=2) print(z)
9 8
注意: Mojo 目前只支持部分关键字参数,因此还不支持某些功能,如只支持关键字参数和可变关键字参数(如 **kwargs
)。
参数可变性和所有权
Mojo 支持完整的值语义,并通过强大的值所有权模型(类似于 Rust 借用检查器)实现内存安全。因此,下面将快速介绍如何通过函数参数共享对值的引用。
请注意,上面的 add()
函数并不修改 x
或 y
,它只是读取值。事实上,按照写法,函数不能修改它们,因为 fn
参数默认是不可变的引用。
在参数约定方面,这被称为 borrowed
,虽然这是 fn
函数的默认设置,但你可以通过这样的借用声明将其明确化(其行为与上面的 add()
完全相同):
In [6]:
fn add(borrowed x: Int, borrowed y: Int) -> Int: return x + y
如果希望参数是可变的,则需要将参数约定声明为 inout
。这意味着在函数内部对参数所做的更改在函数外部是可见的。
例如,该函数可以修改原始变量:
In [7]:
fn add_inout(inout x: Int, inout y: Int) -> Int: x += 1 y += 1 return x + y var a = 1 var b = 2 c = add_inout(a, b) print(a) print(b) print(c)
2 3 5
另一种方法是将参数声明为 owned
参数,这样函数就拥有了参数值的完全所有权(参数值可变且保证唯一)。这样,函数就可以修改值,而不必担心影响函数外部的变量。例如:
In [8]:
fn set_fire(owned text: String) -> String: text += "🔥" return text fn mojo(): let a: String = "mojo" let b = set_fire(a) print(a) print(b) mojo()
mojo mojo🔥
在本例中,Mojo 复制了 a
并将其作为 text
参数传递。原始的 a
字符串仍然完好无损。
但是,如果你想让函数拥有值的所有权,而不想进行复制(这对某些类型来说可能是一个昂贵的操作),那么你可以在将 a
传递给函数时添加 ^
"转移 "操作符。转移操作符会有效地销毁局部变量名--以后任何调用它的尝试都会导致编译器错误。
试试上面的方法,将 set_fire()
的调用改成下面的样子:
mojo
let b = set_fire(a^)
现在你会得到一个错误,因为转移操作符实际上破坏了 a
变量,所以当下面的 print()
函数试图使用 a
时,该变量就不再初始化了。
如果删除 print(a)
,则可以正常工作。
这些参数约定旨在为系统程序员提供全面的内存优化控制,同时确保安全访问和及时删除--Mojo 编译器确保没有两个变量可以同时访问相同的值,每个值的生命周期都定义得很好,以严格防止任何内存错误,如 "use-after-free "和 "double-free"。
注意: 目前,Mojo 总是在函数返回值时复制一个副本。
结构
您可以在 struct
中为类型(或 "对象")建立高级抽象。Mojo 中的 struct
类似于 Python 中的 class
:它们都支持方法、字段、运算符重载、用于元编程的装饰器等。不过,Mojo 的结构体是完全静态的--它们在编译时就已绑定,因此不允许动态派发或在运行时对结构体进行任何更改。(Mojo 未来还将支持类)。
例如,下面是一个基本的结构体:
In [9]:
struct MyPair: var first: Int var second: Int fn __init__(inout self, first: Int, second: Int): self.first = first self.second = second fn dump(self): print(self.first, self.second)
下面是如何使用它的方法:
In [10]:
let mine = MyPair(2, 4) mine.dump()
2 4
如果您熟悉 Python,那么 __init__()
方法和 self
参数应该不会陌生。如果您不熟悉 Python,那么请注意,当我们调用 dump()
时,实际上并没有为 self
参数传递值。self
的值是结构体的当前实例自动提供的(它的用法类似于其他语言中用来指代对象/类型的当前实例的 this
name)。
有关结构体和其他特殊方法(如 __init__()
方法,也称为 "dunder "方法)的更多详情,请参阅编程手册。
Python 集成
尽管 Mojo 仍在不断完善之中,还不是 Python 的完全超集,但我们已经建立了一个原样导入 Python 模块的机制,因此您可以立即利用现有的 Python 代码。在引擎盖下,该机制使用 CPython 解释器运行 Python 代码,因此能与目前所有的 Python 模块无缝兼容。
例如,下面是导入和使用 NumPy 的方法(必须安装 Python numpy
):
In [11]:
from python import Python let np = Python.import_module("numpy") ar = np.arange(15).reshape(3, 5) print(ar) print(ar.shape)
[[ 0 1 2 3 4] [ 5 6 7 8 9] [10 11 12 13 14]] (3, 5)
注意: Mojo 还不是 Python 功能完备的超集。因此,您不能总是复制 Python 代码并在 Mojo 中运行。有关我们计划的更多细节请参阅 Mojo🔥 roadmap & sharp edges。
注意: 安装 Mojo 时,安装程序会搜索系统中与 Mojo 配合使用的 Python 版本,并将路径添加到 modular.cfg
配置文件中。如果您更改了 Python 版本或切换了虚拟环境,Mojo 将使用错误的 Python 库,这可能会导致一些问题,例如导入 Python 包时出错(Mojo 只显示在 Python 中发生了错误,这是一个单独的已知问题)。目前的解决方案是使用 MOJO_PYTHON_LIBRARY
环境变量来覆盖 Mojo 的 Python 库路径。有关如何查找和设置此路径的说明,请参阅此相关问题。
最后
我们希望本页介绍的基础知识足以让您入门。由于篇幅有限,如果您想了解更多相关内容,请查阅 Mojo 编程手册。
-
如果你想将代码打包成一个库,请阅读 Mojo 模块和包。
-
如果您想探索一些 Mojo 代码,请查看 GitHub 上的代码示例。
-
要查看所有可用的 Mojo API,请查阅 Mojo 标准库参考。
注意: Mojo SDK 仍处于早期开发阶段。有些东西还很粗糙,但您可以期待语言和工具的不断变化和改进。请查看已知问题,并在 GitHub 上报告任何其他问题。