Mojo 24.5 is here, and it's our biggest release yet. It's available now through the new Magic package manager, and is bundled with the MAX 24.5 release. Please install Magic to follow along.
Mojo 24.5 现已推出,这是我们迄今为止最大的版本。它现在通过新的 Magic 软件包管理器提供,并与 MAX 24.5 版本捆绑在一起。请安装 Magic 以继续操作。
This release includes several new core language changes and standard library enhancements. In this blog post, we will dive into many of these new features and language improvements using code examples. We focus our examples on the unified pointer type UnsafePointer
, and gradually introduce other notable features. By the end of this blog post, you will have a thorough understanding of these enhancements and can comfortably utilize them in your Mojo code.
此版本包括几项新的核心语言更改和标准库增强功能。在这篇博文中,我们将使用代码示例深入探讨其中的许多新功能和语言改进。我们将示例集中在统一指针类型 UnsafePointer
上,并逐渐引入其他值得注意的功能。在这篇博文的结尾,您将对这些增强功能有透彻的了解,并可以在您的 Mojo 代码中舒适地使用它们。
One of the biggest highlights of this release is the significant contributions from our community. We received numerous pull requests from 11 community contributors that included new features, bug fixes, documentation enhancements, and code refactoring. Special thanks 🙏🏼 to our community contributors:
此版本的最大亮点之一是我们社区的重大贡献。我们收到了来自 11 个社区贡献者的大量拉取请求,其中包括新功能、错误修复、文档增强和代码重构。特别感谢🙏🏼我们的社区贡献者:
@jjvraw, @artemiogr97, @martinvuyk, @jayzhan211, @bgreni, @mzaks, @msaelices, @rd4com, @jiex-liu, @kszucs, @thatstoasty.
@jjvraw、@artemiogr97、@martinvuyk、@jayzhan211、@bgreni、@mzaks、@msaelices、@rd4com、@jiex流、@kszucs、@thatstoasty。
For a complete list of changes, please refer to the changelog for version 24.5.
有关更改的完整列表,请参阅版本 24.5 的更改日志。
All the code for this blog post is available in our GitHub repository.
本博客文章的所有代码都可以在我们的 GitHub 存储库中找到。
Dramatically reduced the auto-imported modules
大幅减少了自动导入的模块
Mojo v24.5 has significantly reduced the set of automatically imported entities into users' programs. Previously, modules like memory, sys, os, utils, python, bit, random, math, builtin, and collections were automatically available. Now, only the explicitly enumerated entities in prelude/__init__.mojo are automatically imported. This change requires developers to explicitly import the entities they use, preventing unexpected namespace pollution and improving code clarity.
Mojo v24.5 显著减少了自动导入用户程序的实体集。以前,memory、sys、os、utils、python、bit、random、math、builtin 和 collections 等模块是自动可用的。现在,仅自动导入 prelude/__init__.mojo 中显式枚举的实体。此更改要求开发人员显式导入他们使用的实体,以防止意外的命名空间污染并提高代码清晰度。
No need for var in fn
fn 中不需要 var
Mojo 24.5 has relaxed the requirement of using var within fn functions, making fn more similar to def functions, while still maintaining its unique capabilities. However, fn stills differs from def by allowing greater control over memory, which is particularly important when interfacing with low-level C code through Foreign Function Interface (FFI). Another difference is that def raises implicitly.
Mojo 24.5 放宽了在 fn 函数中使用 var 的要求,使 fn 更类似于 def 函数,同时仍然保持了其独特的能力。但是,fn stills 与 def 的不同之处在于它允许对内存进行更大的控制,这在通过外部函数接口 (FFI) 与低级 C 代码连接时尤为重要。另一个区别是 def 隐式地引发。
Unified pointer data structure via UnsafePointer
通过 UnsafePointer 统一指针数据结构
In 24.5, we’ve unified the pointer data structures by consolidating all previous pointer types into a single UnsafePointer. The previous types -– DTypePointer, LegacyPointer and Pointer— have been removed. Their functionalities have been incorporated into UnsafePointer, simplifying the pointer system while retaining all necessary capabilities. This unification streamlines pointer usage in Mojo and reduces confusion for developers.
在 24.5 中,我们通过将所有以前的指针类型合并到一个 UnsafePointer 中来统一指针数据结构。以前的类型 (DTypePointer、LegacyPointer 和 Pointer) 已被删除。它们的功能已合并到 UnsafePointer 中,简化了指针系统,同时保留了所有必要的功能。这种统一简化了 Mojo 中的指针使用,并减少了开发人员的困惑。
The next section is a refresher on the UnsafePointer and how to properly use it. Please feel free to skip this section and go to the examples.
下一节将回顾 UnsafePointer 以及如何正确使用它。请随时跳过本节并转到示例。
Refresher on UnsafePointer
UnsafePointer 上的复习
Understanding Undefined Behavior (UB) with uninitialized memory
了解具有未初始化内存的未定义行为 (UB)
The UnsafePointer type creates an indirect reference to a location in memory. You can use an UnsafePointer to dynamically allocate and free memory, or to point to memory allocated by some other piece of code. You can use these pointers to write code that interacts with low-level interfaces, to interface with other programming languages, or to build certain kinds of data structures. But as the name suggests, they're inherently unsafe. For example, when using unsafe pointers, you're responsible for ensuring that memory gets allocated and freed correctly.
UnsafePointer 类型创建对内存中位置的间接引用。您可以使用 UnsafePointer 动态分配和释放内存,或指向由其他代码段分配的内存。您可以使用这些指针编写与低级接互的代码,与其他编程语言交互,或构建某些类型的数据结构。但顾名思义,它们本质上是不安全的。例如,当使用不安全的指针时,您有责任确保正确分配和释放内存。
When we allocate memory using UnsafePointer in Mojo, the allocated memory is uninitialized. Accessing this uninitialized memory without proper initialization is considered Undefined Behavior (UB). This can occur when using the __getitem__ operation (i.e., square brackets []) on an uninitialized pointer:
当我们在 Mojo 中使用 UnsafePointer 分配内存时,分配的内存是未初始化的。在没有正确初始化的情况下访问此未初始化的内存被视为未定义行为 (UB)。在未初始化的指针上使用 __getitem__ 操作(即方括号 [])时,可能会发生这种情况:
Mojo 魔 咒
ptr = UnsafePointer[Int].alloc(1)
ptr[0] = 42 <-- Undefined Behavior
The above example shows ptr[0] attempting to provide a valid reference, but since the memory is uninitialized, the validity and lifetime of the reference is ambiguous. Unlike C, which does not differentiate and has historically been plagued by memory vulnerabilities due to this, Mojo requires explicit initialization of unsafe pointers to ensure memory safety.
上面的示例显示了 ptr[0] 尝试提供有效的引用,但由于内存未初始化,因此引用的有效性和生存期不明确。与 C 语言不同,C 语言不区分,因此在历史上一直受到内存漏洞的困扰,Mojo 需要显式初始化不安全的指针以确保内存安全。
Proper Initialization with init_pointee_copy/move
使用 init_pointee_copy/move 进行正确初始化
To ensure that memory is properly initialized, Mojo provides methods like init_pointee_copy from the UnsafePointer, suitable for numeric types such as Int. This method allows for the safe initialization of memory which we should use to initialize ptr safely.
为了确保内存得到正确初始化,Mojo 提供了 UnsafePointer 的 init_pointee_copy 等方法,适用于 Int 等数字类型。此方法允许对内存进行安全初始化,我们应该使用它来安全地初始化 ptr。
Mojo 魔 咒
ptr = UnsafePointer[Int].alloc(1)
ptr.init_pointee_copy(42)
In the provided code snippet, initializing ptr with ptr[0] = 42 was identified as problematic due to it involving direct assignment to uninitialized memory. Using init_pointee_copy resolves this issue by ensuring that the memory is correctly and safely initialized before use.
在提供的代码片段中,使用 ptr[0] = 42 初始化 ptr 被确定为有问题,因为它涉及直接分配给未初始化的内存。使用 init_pointee_copy 可确保在使用前正确安全地初始化内存,从而解决此问题。
You can read a more extensive discussion of how to allocate and initialize pointers in the UnsafePointer documentation.
您可以在 UnsafePointer 文档中阅读有关如何分配和初始化指针的更广泛讨论。
With this primer, we will start using UnsafePointer and gradually introduce other notable features of Mojo 24.5, so that we can see end-to-end examples incorporating all the new features.
通过本入门,我们将开始使用 UnsafePointer 并逐步介绍 Mojo 24.5 的其他显着功能,以便我们可以看到包含所有新功能的端到端示例。
Example 1: UnsafeBuffer
示例 1:UnsafeBuffer
The following is a way to implement a buffer data structure in Mojo using the UnsafePointer. Can you spot why this buffer is unsafe?
以下是使用 UnsafePointer 在 Mojo 中实现缓冲区数据结构的一种方法。您能找出为什么这个缓冲区不安全吗?
Mojo 魔 咒
from memory import memset_zero
struct UnsafeBuffer:
var data: UnsafePointer[UInt8]
var size: Int
fn __init__(inout self, size: Int):
self.data = UnsafePointer[UInt8].alloc(size)
memset_zero(self.data, size)
self.size = size
fn __del__(owned self):
self.data.free()
If we run magic run mojo unsafe_buffer.mojo:
如果我们运行 magic run mojo unsafe_buffer.mojo:
Mojo 魔 咒
def main():
ub = UnsafeBuffer(10)
print("initial value at index 0:")
print(ub.data[0])
ub.data[0] = 255
print("value at index 0 after getting set to 255:")
print(ub.data[0])
We get the output: 我们得到输出:
Output 输出
initial value at index 0:
0
value at index 0 after getting set to 255:
0
But why? What went wrong?
但是为什么?哪里出了问题?
As stated in the safety part, UnsafePointer does not carry any lifetime information, which means that the lifetime of data in UnsafeBuffer is not connected to the lifetime of the ub instance. In the last print(ub.data\0]), the compiler considers ub as unused immediately after the pointer to the element is calculated by ub.data[0], but before reading from that pointer. As a result, [Mojo’s (ASAP) destructor gets activated, freeing the data pointer. Any subsequent call becomes Undefined Behavior (UB) because they are accessing freed memory. This is why, quite luckily, we see the unexpected output.
如安全部分所述,UnsafePointer 不携带任何生命周期信息,这意味着 UnsafeBuffer 中数据的生命周期与 ub 实例的生命周期无关。在最后一个 print(ub.data\0]) 中,编译器在 ub.data[0] 计算指向元素的指针之后,但在从该指针读取之前,立即将 ub 视为未使用。结果,[Mojo 的 (ASAP) 析构函数被激活,从而释放数据指针。任何后续调用都将变为未定义行为 (UB),因为它们正在访问释放的内存。这就是为什么我们很幸运地看到了意想不到的输出。
In the next example, we try to make our buffer implementation safe.
在下一个示例中,我们尝试使缓冲区实现安全。
Example 2: SafeBuffer
示例 2:SafeBuffer
The following implementation ties the lifetime of (private) _data pointer to an instance of a SafeBuffer via the write and read methods. Moreover, we are using debug_assert to check the bounds in write and read methods which gets activated using the compiler option -D MOJO_ENABLE_ASSERTIONS. We will shortly see how.
以下实现通过 write 和 read 方法将 (private) _data指针的生命周期绑定到 SafeBuffer 的实例。此外,我们使用 debug_assert 来检查使用编译器选项 -D MOJO_ENABLE_ASSERTIONS 激活的 write 和 read 方法中的边界。我们很快就会看到如何操作。
Mojo 魔 咒
from memory import memset_zero
struct SafeBuffer:
var _data: UnsafePointer[UInt8]
var size: Int
fn __init__(inout self, size: Int):
debug_assert(size > 0, "size must be greater than zero")
self._data = UnsafePointer[UInt8].alloc(size)
memset_zero(self._data, size)
self.size = size
fn __del__(owned self):
self._data.free()
fn write(inout self, index: Int, value: UInt8):
debug_assert(0 <= index < self.size, "index must be within the buffer")
self._data[index] = value
fn read(self, index: Int) -> UInt8:
debug_assert(0 <= index < self.size, "index must be within the buffer")
return self._data[index]
Now if we run the application code
现在,如果我们运行应用程序代码
Mojo 魔 咒
def main():
sb = SafeBuffer(10)
sb.write(0, 255)
print("value at index 0 after getting set to 255:")
print(sb.read(0))
via the following command:
通过以下命令:
Bash 重击
magic run mojo -D MOJO_ENABLE_ASSERTIONS safe_buffer.mojo
We see the output is as expected:
我们看到输出符合预期:
Output 输出
value at index 0 after getting set to 255:
255
Note that it is crucial to not directly access the _data and instead use the write and read methods in order to tie the lifetime of the _data pointer to the underlying instance.
请注意,不要直接访问 _data,而是使用 write 和 read 方法,以便将 _data 指针的生命周期绑定到底层实例,这一点至关重要。
Named result bindings 命名结果绑定
Mojo now supports named result bindings. Named result bindings are useful for directly emplacing function results into the output slot of a function. This feature provides more flexibility and guarantees around emplacing the result of a function compared to a "guaranteed" named return value optimization.
Mojo 现在支持命名结果绑定。命名结果绑定可用于将函数结果直接放入函数的输出槽中。与“保证的”命名返回值优化相比,此功能在放置函数结果方面提供了更大的灵活性和保证。
To see how it can simplify our application code, let’s have a look at the following code continuing our SafeBuffer:
为了了解它如何简化我们的应用程序代码,让我们看看以下代码继续我们的 SafeBuffer:
Mojo 魔 咒
struct SafeBuffer:
...
@staticmethod
fn initialize_with_value(size: Int, value: UInt8) -> Self as output:
output = SafeBuffer(size)
for i in range(size):
output.write(i, value)
return
def main():
buffer = SafeBuffer.initialize_with_value(size=10, value=128)
In this code, we define a static method initialize_with_value that returns an instance of SafeBuffer. The method declares output as the return variable of type Self. Within the method, we initialize output by creating a new SafeBuffer of the specified size. We then fill the buffer by writing the given value to each index.
在此代码中,我们定义了一个静态方法 initialize_with_value,它返回 SafeBuffer 的实例。该方法将 output 声明为 Self 类型的返回变量。在该方法中,我们通过创建指定大小的新 SafeBuffer 来初始化输出。然后,我们通过将给定的值写入每个索引来填充缓冲区。
By using a named return variable output, the compiler constructs and modifies the return value directly in the memory location where it will ultimately reside. This eliminates the need for unnecessary copying or moving of the SafeBuffer instance which is important for types that are not movable and copyable. The as output syntax in the function signature indicates that output is the return variable, and the final return statement returns control without specifying a value, as output is already the return value.
通过使用命名返回变量输出,编译器直接在返回值最终所在的内存位置构造和修改返回值。这样就不需要不必要地复制或移动 SafeBuffer 实例,这对于不可移动和可复制的类型非常重要。函数签名中的 as output 语法指示 output 是 return 变量,而最终的 return 语句返回 control 而不指定值,因为 output 已经是返回值。
Argument exclusivity verification
参数排他性验证
Mojo 24.5 now checks (at compile time) that mutable argument references don’t alias other references. That means that Mojo requires references (including implicit references due to borrow_ed/inout_ arguments) to be uniquely referenced (non-aliased) if mutable. This is important for code safety, because it allows the compiler (and readers of code) to understand where and when a value is mutated. It is also useful for performance optimization because it allows the compiler to know that accesses through immutable references cannot change behind the scenes.
Mojo 24.5 现在检查(在编译时)可变参数引用是否不会为其他引用提供别名。这意味着 Mojo 要求引用(包括由于 borrow_ed/inout_ 参数引起的隐式引用)在可变的情况下被唯一引用(非别名)。这对于代码安全很重要,因为它允许编译器(和代码的读者)了解值在何时何地发生变化。它对于性能优化也很有用,因为它允许编译器知道通过不可变引用的访问不能在后台更改。
To see how argument exclusivity helps to write safe code and can catch errors at compile time, let’s have a look at the following process_buffers function that uses our SafeBuffer to overwrite a mutable buffer with the values of a borrowed buffer:
要了解参数独占性如何帮助编写安全代码并在编译时捕获错误,让我们看看以下 process_buffers 函数,该函数使用我们的 SafeBuffer 用借用缓冲区的值覆盖可变缓冲区:
Mojo 魔 咒
fn process_buffers(buffer1: SafeBuffer, inout buffer2: SafeBuffer):
debug_assert(buffer1.size == buffer2.size, "buffer sizes much match")
for i in range(buffer1.size):
buffer2.write(i, buffer1.read(i))
Now image that in our application code we have:
现在想象一下,在我们的应用程序代码中,我们有:
Mojo 魔 咒
def main():
buffer1 = SafeBuffer.initialize_with_value(size=10, value=128)
buffer2 = SafeBuffer(10)
process_buffers(buffer1, buffer1)
In our application code, we accidentally pass buffer1 as both arguments to process_buffers, with the second argument intended to be mutable (inout). This means that we're trying to mutate buffer1 while also having a borrowed reference to it. Mojo's argument exclusivity rules enforce that mutable references (inout parameters) must be unique and non-aliased. The compiler detects this violation at compile time and creates a warning for potential bugs and ensuring code safety by enforcing no-aliasing rules. This also allows the compiler to optimize more aggressively by treating argument references as not-aliased.
在我们的应用程序代码中,我们不小心将 buffer1 作为两个参数传递给 process_buffers,第二个参数是可变的 (inout)。这意味着我们尝试改变 buffer1,同时也有一个借用的引用。Mojo 的参数排他性规则强制要求可变引用(inout 参数)必须是唯一的且无别名。编译器在编译时检测到此违规,并为潜在错误创建警告,并通过强制实施无别名规则来确保代码安全。这也允许编译器通过将参数引用视为非别名来更积极地进行优化。
When running the code, the compiler warns us about such issues:
运行代码时,编译器会警告我们此类问题:
Output 输出
warning: call argument allows writing a memory location previously readable through another aliased argument
process_buffers(buffer1, buffer1)
^~~~~~~~ ~~~~~~~
note: 'buffer1' value is passed through aliasing 'inout' argument
process_buffers(buffer1, buffer1)
^ ~~~~~~~
Formattable trait 可格式化特征
As of Mojo 24.5, the print function now requires that its arguments conform to the Formattable trait. This enables efficient stream-based writing by default, avoiding unnecessary intermediate String heap allocations.
从 Mojo 24.5 开始,print 函数现在要求其参数符合 Formattable trait。默认情况下,这将启用高效的基于流的写入,避免不必要的中间 String 堆分配。
Let’s use this new trait and implement format_to and __str__ so that we can print our buffer values easily. Everything stays the same, except we include Formattable and Stringable as follows:
让我们使用这个新 trait 并实现 format_to 和 __str__,以便我们可以轻松打印缓冲区值。一切都保持不变,除了我们包括 Formattable 和 Stringable,如下所示:
Mojo 魔 咒
struct SafeBuffer(Stringable, Formattable):
...
fn __str__(self) -> String:
return String.format_sequence(self)
fn format_to(self, inout writer: Formatter):
debug_assert(self.size > 0, "size must be greater than zero")
writer.write("[")
for i in range(self.size - 1):
writer.write(self._data[i], ", ")
writer.write(self._data[self.size - 1])
writer.write("]")
Now let’s test it within our application code via magic run mojo safe_buffer.mojo:
现在让我们通过 magic run mojo safe_buffer.mojo 在我们的应用程序代码中测试它:
Mojo 魔 咒
def main():
buffer1 = SafeBuffer.initialize_with_value(size=10, value=128)
buffer2 = SafeBuffer(10)
process_buffers(buffer1, buffer2)
print("buffer2:", buffer2)
Which produces the expected outputs:
这将产生预期的输出:
Output 输出
buffer2: [128, 128, 128, 128, 128, 128, 128, 128, 128, 128]
For the record, here is all the code for safe_buffer.mojo:
作为记录,以下是 safe_buffer.mojo 的所有代码:
Mojo 魔 咒
from memory import memset_zero
struct SafeBuffer(Stringable, Formattable):
var _data: UnsafePointer[UInt8]
var size: Int
fn __init__(inout self, size: Int):
debug_assert(size > 0, "size must be greater than zero")
self._data = UnsafePointer[UInt8].alloc(size)
memset_zero(self._data, size)
self.size = size
@staticmethod
fn initialize_with_value(size: Int, value: UInt8) -> Self as output:
output = SafeBuffer(size)
for i in range(size):
output.write(i, value)
return
fn __del__(owned self):
self._data.free()
fn write(inout self, index: Int, value: UInt8):
debug_assert(0 <= index < self.size, "index must be within the buffer")
self._data[index] = value
fn read(self, index: Int) -> UInt8:
debug_assert(0 <= index < self.size, "index must be within the buffer")
return self._data[index]
fn __str__(self) -> String:
return String.format_sequence(self)
fn format_to(self, inout writer: Formatter):
debug_assert(self.size > 0, "size must be greater than zero")
writer.write("[")
for i in range(self.size - 1):
writer.write(self._data[i], ", ")
writer.write(self._data[self.size - 1])
writer.write("]")
fn process_buffers(buffer1: SafeBuffer, inout buffer2: SafeBuffer):
debug_assert(buffer1.size == buffer2.size, "buffer sizes much match")
for i in range(buffer1.size):
buffer2.write(i, buffer1.read(i))
def main():
sb = SafeBuffer(10)
sb.write(0, 255)
print("safe buffer outputs:")
print(sb.read(0))
buffer1 = SafeBuffer.initialize_with_value(size=10, value=128)
buffer2 = SafeBuffer(10)
# process_buffers(buffer1, buffer1) # <-- argument exclusivity detects such errors at compile time
process_buffers(buffer1, buffer2)
print("buffer2:", buffer2)
Example 3: Generic SafeBuffer[T]
示例 3:泛型 SafeBuffer[T]
In this example, we aim to make our SafeBuffer implementation generic, allowing it to handle different data types. To achieve this, we use UnsafePointer with Optional[T] so that we can initialize our pointer with NoneType. By making SafeBuffer generic, we can create buffers for various data types while maintaining safety and proper initialization.
在此示例中,我们的目标是使 SafeBuffer 实现通用,使其能够处理不同的数据类型。为了实现这一点,我们将 UnsafePointer 与 Optional[T] 一起使用,以便我们可以用 NoneType 初始化我们的指针。通过使 SafeBuffer 成为通用的,我们可以为各种数据类型创建缓冲区,同时保持安全性和正确的初始化。
Mojo 魔 咒
from collections import Optional
struct SafeBuffer[T: CollectionElement]:
var data: UnsafePointer[Optional[T]]
var size: Int
fn __init__(inout self, size: Int):
debug_assert(size > 0, "size must be greater than zero")
self.data = UnsafePointer[Optional[T]].alloc(size)
for i in range(size):
(self.data + i).init_pointee_copy(NoneType())
self.size = size
@staticmethod
fn initialize_with_value(size: Int, value: T) -> Self as output:
output = SafeBufferT
for i in range(size):
output.write(i, value)
return
fn __copyinit__(inout self, existing: Self):
self.data = existing.data
self.size = existing.size
fn __moveinit__(inout self, owned existing: Self):
self.data = existing.data
self.size = existing.size
fn __del__(owned self):
self.data.free()
fn write(inout self, index: Int, value: Optional[T]):
debug_assert(0 <= index < self.size, "index must be within the buffer")
self._data[index] = value
fn read(self, index: Int) -> Optional[T]:
debug_assert(0 <= index < self.size, "index must be within the buffer")
return self._data[index]
fn take(inout self, index: Int) -> Optional[T] as output:
output = self.read(index)
self.write(index, OptionalT)
return
Conditional conformance 有条件的一致性
Mojo 24.5 allows types to “conditionally conform” to traits, which allows a generic struct to conform to a trait only when its type parameters meet certain conditions. In our case, we can make SafeBuffer[T: CollectionElement] conform to Stringable as long as T itself conforms to Stringable (i.e. T: Stringable). The key requirement for conditional conformance is that the trait used in the generic parameter must include the trait(s) used in the struct definition.
Mojo 24.5 允许类型“有条件地符合”特征,这允许通用结构体仅在其类型参数满足特定条件时才符合特征。在我们的例子中,只要 T 本身符合 Stringable(即 T: Stringable),我们就可以使 SafeBuffer[T: CollectionElement] 符合 Stringable。条件一致性的关键要求是 generic 参数中使用的 trait 必须包含 struct 定义中使用的 trait。
To achieve this, we use the StringableFormattableCollectionElement that is defined to include both Formattable and StringableCollectionElement traits:
为了实现这一点,我们使用 StringableFormattableCollectionElement,该元素被定义为同时包含 Formattable 和 StringableCollectionElement 特征:
Mojo 魔 咒
trait StringableFormattableCollectionElement(Formattable, StringableCollectionElement):
...
We then define the __str__ method with a type parameter U that must conform to StringableFormattableCollectionElement:
然后,我们使用必须符合 StringableFormattableCollectionElement 的类型参数 U 定义 __str__ 方法:
Mojo 魔 咒
fn __str__U: StringableFormattableCollectionElement -> String:
...
Note that the self parameter is typed as SafeBuffer[U], repeating the type parameter U. This ensures that the __str__ method is only available when U conforms to the necessary traits, allowing the compiler to catch type errors at compile time if we attempt to use SafeBuffer with a type that does not meet the requirements.
请注意,self 参数的类型为 SafeBuffer[U],重复类型参数 U。这确保了 __str__ 方法仅在 U 符合必要的 trait 时可用,如果我们尝试将 SafeBuffer 与不满足要求的类型一起使用,则允许编译器在编译时捕获类型错误。
And now we are ready to include such trait in __str__ and format_to by minimally adjusting the code as follows:
现在我们准备在 __str__ 中包含此类 trait 并通过对代码进行最小调整来format_to,如下所示:
Mojo 魔 咒
struct SafeBuffer[T: CollectionElement]:
...
fn __str__U: StringableFormattableCollectionElement -> String:
ret = String()
writer = ret._unsafe_to_formatter()
self.format_to(writer)
_ = writer^
return ret^
fn format_to[
U: StringableFormattableCollectionElement
](self: SafeBuffer[U], inout writer: Formatter):
debug_assert(self.size > 0, "size must be greater than zero")
writer.write("[")
for i in range(self.size - 1):
if self._data[i]:
writer.write(self._data[i].value(), ", ")
else:
writer.write("None", ", ")
if self._data[self.size - 1]:
writer.write(self._data[self.size - 1].value())
else:
writer.write("None")
writer.write("]")
To test how conditional conformance can catch type errors at compile time, let’s define a dummy type that is neither Stringable nor Formattable, yet still includes CollectionElement requirements:
为了测试条件一致性如何在编译时捕获类型错误,让我们定义一个既不是 Stringable 也不是 Formattable,但仍包含 CollectionElement 要求的虚拟类型:
Mojo 魔 咒
struct NotStringableNorFormattable(CollectionElement):
fn __init__(inout self): ...
fn __copyinit__(inout self, existing: Self): ...
fn __moveinit__(inout self, owned existing: Self): ...
If we try to use it in our application code:
如果我们尝试在应用程序代码中使用它:
Mojo 魔 咒
def main():
buf = SafeBufferNotStringableNorFormattable
buf.__str__()
We get the following compile time error indicating that the type does not conform to the required trait StringableFormattableCollectionElement:
我们收到以下编译时错误,表明该类型不符合所需的 trait StringableFormattableCollectionElement:
Output 输出
error: invalid call to '__str__': could not deduce parameter 'U' of callee '__str__'
buf.__str__()
~~~~~~~~~~~^~
note: failed to infer parameter 'U', argument type 'NotStringableNorFormattable' does not conform to trait 'StringableFormattableCollectionElement'
buf.__str__()
^~~
note: function declared here
fn __str__U: StringableFormattableCollectionElement -> String:
^
mojo test 魔力测试
Mojo 24.5 also comes with the mojo test option that uses the Mojo compiler for running unit tests. Let’s add a test case under test_generic_safe_buffer.mojo with the following:
Mojo 24.5 还附带了 mojo test 选项,该选项使用 Mojo 编译器来运行单元测试。让我们在 test_generic_safe_buffer.mojo 下添加一个测试用例,如下所示:
Mojo 魔 咒
from testing import assert_equal
from generic_safe_buffer import SafeBuffer
def test_buffer():
buffer = SafeBuffer[String].initialize_with_value(size=5, value=String("hi"))
val = buffer.take(2).value()
assert_equal(val, String("hi"))
assert_equal(buffer.__str__(), "[hi, hi, None, hi, hi]")
Now we can run magic run mojo test test_generic_safe_buffer.mojo and can see the output as:
现在我们可以运行 magic run mojo test test_generic_safe_buffer.mojo,并可以看到输出为:
Output 输出
Testing Time: 1.828s
Total Discovered Tests: 1
Passed : 1 (100.00%)
Failed : 0 (0.00%)
Skipped: 0 (0.00%)
Magic tip: we can include the following in the tasks section of mojoproject.toml:
神奇提示:我们可以在 mojoproject.toml 的 tasks 部分包含以下内容:
mojoproject.toml mojoproject.toml 文件
[tasks]
test = "mojo test test_*.mojo
So that running all the tests becomes as easy as running magic run test.
因此,运行所有测试变得像运行 magic run test 一样简单。
For completeness, the following includes the entire code for generic_safe_buffer.mojo which we can run via magic run mojo generic_safe_buffer.mojo:
为完整起见,以下内容包括 generic_safe_buffer.mojo 的整个代码,我们可以通过 magic run mojo generic_safe_buffer.mojo 运行:
Mojo 魔 咒
from collections import Optional
trait StringableFormattableCollectionElement(Formattable, StringableCollectionElement):
...
struct SafeBuffer[T: CollectionElement]:
var _data: UnsafePointer[Optional[T]]
var size: Int
fn __init__(inout self, size: Int):
debug_assert(size > 0, "size must be greater than zero")
self._data = UnsafePointer[Optional[T]].alloc(size)
for i in range(size):
(self._data + i).init_pointee_copy(NoneType())
self.size = size
@staticmethod
fn initialize_with_value(size: Int, value: T) -> Self as output:
output = SafeBufferT
for i in range(size):
output.write(i, value)
return
fn __copyinit__(inout self, existing: Self):
self._data = existing._data
self.size = existing.size
fn __moveinit__(inout self, owned existing: Self):
self._data = existing._data
self.size = existing.size
fn __del__(owned self):
self._data.free()
fn write(inout self, index: Int, value: Optional[T]):
debug_assert(0 <= index < self.size, "index must be within the buffer")
self._data[index] = value
fn read(self, index: Int) -> Optional[T]:
debug_assert(0 <= index < self.size, "index must be within the buffer")
return self._data[index]
fn __str__U: StringableFormattableCollectionElement -> String:
ret = String()
writer = ret._unsafe_to_formatter()
self.format_to(writer)
_ = writer^
return ret^
fn format_to[
U: StringableFormattableCollectionElement
](self: SafeBuffer[U], inout writer: Formatter):
debug_assert(self.size > 0, "size must be greater than zero")
writer.write("[")
for i in range(self.size - 1):
if self._data[i]:
writer.write(self._data[i].value(), ", ")
else:
writer.write("None", ", ")
if self._data[self.size - 1]:
writer.write(self._data[self.size - 1].value())
else:
writer.write("None")
writer.write("]")
fn take(inout self, index: Int) -> Optional[T] as output:
output = self.read(index)
self.write(index, OptionalT)
return
fn process_buffersT: CollectionElement:
debug_assert(buffer1.size == buffer2.size, "buffer sizes much match")
for i in range(buffer1.size):
buffer2.write(i, buffer1.read(i))
struct NotStringableNorFormattable(CollectionElement):
fn __init__(inout self):
...
fn __copyinit__(inout self, existing: Self):
...
fn __moveinit__(inout self, owned existing: Self):
...
def main():
buffer1 = SafeBuffer[UInt8].initialize_with_value(size=10, value=UInt8(128))
buffer2 = SafeBufferUInt8
# process_buffers(buffer1, buffer1) # <-- argument exclusivity detects such errors at compile time
process_buffers(buffer1, buffer2)
# testing conditional conformance
print(buffer2.__str__())
print(buffer2.take(0).value())
print(buffer2.__str__())
sbuffer1 = SafeBuffer[String].initialize_with_value(size=10, value=String("hi"))
print(sbuffer1.take(5).value())
print(sbuffer1.__str__())
## uncomment to see the compiler error:
# buf = SafeBufferNotStringableNorFormattable
# buf.__str__()
Summary 总结
In this blog post, we explored several new features introduced in Mojo 24.5, including the unified UnsafePointer type, relaxed variable declaration in fn functions, named result bindings, argument exclusivity, and conditional conformance.
在这篇博文中,我们探讨了 Mojo 24.5 中引入的几项新功能,包括统一的 UnsafePointer 类型、fn 函数中的松散变量声明、命名结果绑定、参数独占性和条件一致性。
We began by discussing the dramatic reduction of auto-imported modules, which improves code clarity by requiring explicit imports. We then dived into the unification of pointer data structures into UnsafePointer, simplifying pointer usage while retaining essential functionalities.
我们首先讨论了自动导入模块的大幅减少,它通过要求显式导入来提高代码的清晰度。然后,我们深入研究了将指针数据结构统一到 UnsafePointer 中,简化了指针的使用,同时保留了基本功能。
We examined how to properly use UnsafePointer, highlighting the importance of initializing allocated memory to avoid Undefined Behavior. Through the UnsafeBuffer example, we demonstrated the potential pitfalls of accessing UnsafePointer directly.
我们研究了如何正确使用 UnsafePointer,强调了初始化分配的内存以避免 Undefined 行为的重要性。通过 UnsafeBuffer 示例,我们演示了直接访问 UnsafePointer 的潜在陷阱。
By introducing the SafeBuffer example, we showcased how to use lifetimes of the underlying UnsafePointer can be tied to an instance of SafeBuffer through the write and read methods, preventing premature deallocation and Undefined Behavior.
通过介绍 SafeBuffer 示例,我们展示了如何利用底层 UnsafePointer 的生命周期,通过 write 和 read 方法将其绑定到 SafeBuffer 的实例,从而防止过早的释放和 Undefined Behavior。
We explored the benefits of named result bindings showing how it allows for efficient construction and returning of objects without unnecessary copying or moving.
我们探讨了命名结果绑定的好处,展示了它如何允许在没有不必要的复制或移动的情况下有效地构造和返回对象。
We discussed argument exclusivity, emphasizing how Mojo's compile-time checks prevent aliasing of mutable references, enhancing code safety and correctness.
我们讨论了参数独占性,强调了 Mojo 的编译时检查如何防止可变引用的别名,从而提高代码的安全性和正确性。
We incorporated the Formattable trait to enable efficient and flexible string formatting of our SafeBuffer, and demonstrated how to implement the format_to and __str__ methods.
我们合并了 Formattable trait 以实现 SafeBuffer 的高效灵活的字符串格式化,并演示了如何实现 format_to 和 __str__ 方法。
Finally, we extended our SafeBuffer implementation to be generic, leveraging conditional conformance to ensure that methods like __str__ are only available when the type parameter meets certain trait constraints. This allows the compiler to enforce type safety and catch errors at compile time.
最后,我们将 SafeBuffer 实现扩展为泛型,利用条件一致性来确保 __str__ 等方法仅在类型参数满足某些 trait 约束时可用。这允许编译器在编译时强制执行类型安全并捕获错误。
Overall, Mojo 24.5 brings significant enhancements that improve code safety, performance, and developer experience. We encourage you to explore these new features and consider how they can benefit your own projects. For more details, please check out the changelog for Mojo 24.5.
总体而言,Mojo 24.5 带来了重大增强功能,提高了代码安全性、性能和开发人员体验。我们鼓励您探索这些新功能,并考虑它们如何使您自己的项目受益。有关更多详细信息,请查看 Mojo 24.5 的更新日志。