Mojo 的所有权、生命周期和引用特性现状

Mojo 的所有权之前主要体现在传参时候的 borrowed, inout 以及 owned 这三个关键字保证原先传入的值不会被修改,但是从目前 Nightly 版本的发展状况和一系列已有的提案来看,这些关键字、引用的语法和语义可能都会发生比较大的变化。

从目前写 Mojo 的感受来看,很多特性实际上都是半成品的状态,很多时候都得使用 DTypePointer 或者 UnsafePointer 这两种不安全的类型来实现一些功能。这显然不是一个好的设计。所以我希望从目前的提案和 stdlib 中的一些新修改中总结一下 Mojo 目前所有权、生命周期和引用的状况。

关键字的修改

关键字的修改实际上和语言的特性并无直接关系,但是对语义和概念的理解会有很大的帮助。从目前对关键字重命名的提案来看[1],目前的 borrowedinout 以及 owned 对于后续生命周期的加入、引用类型的设计都不是很合适,所以这个提案中想使用 ref, mutref 来代替 borrowedinout。另外,对于 owned,提案中想使用 var 来代替。

People on the forums have pointed out that the “owned” keyword in argument lists is very analogous to the var keyword. It defines a new, whole, value and it is mutable just like var. Switching to var eliminates a concept and reduces the number of keywords we are introducing.

这个关键字的修改是比较合理的,毕竟之前 Mojo 的引用、生命周期还不完善,borrowedinoutowned 实际上只是用于传参(Argument Convention)。但是之后引用等特性的加入可能会出现例如 List[ref A] 这样的类型,于是引用就从传参的方式成为了类型系统的一部分,这时候 refmutref 好像确实看着更舒适一些(而且打得字少)。

引用类型

除了以上关键字替换的提案,最近还有一个新的关于 ref 以及 Reference 类型的提案[2]。这个提案实际上包括了四个部分。首先是将 ref 引入函数调用中,作为一个新的 Argument Convention,并且在使用 ref [<lifetime>] 的方式指定生命周期。另外,在返回值的地方也引入 ref,并且提供 auto-dereference 这样的特性[3]以方便使用。但是这个提案同时也保留 Reference 类型,作为强制手动解引用的一个选择。

基于以上这两种新特性,这个提案认为可以直接移除 __refitem__ 这个之前在 stdlib 中使用的 dunder method, 而直接使用 __getitem__。下面是提案中给出的新代码:

fn __getitem__(ref [_] self, index: Int) -> ref [__lifetime_of(self)] Self.ElementType:
    ...

最后这个提案还建议将 Reference 类型重命名成 Pointer/SafePointer,因为之前 stdlib 里面的 Pointer 已经被改为了 LegacyPointer,且目前解引用时使用的 ptr[] 这个看着很像是一个没有实现好的特性。

Because there is no automatic dereference, the existing Reference type currently behaves like a pointer, not a reference. You need to explicitly dereference it with ptr just like an unsafe pointer… but it adds safety through lifetime management and semantic checking.

Let’s just rename it to Pointer: The consequence of this is that explicit dereference syntax is a feature, not a bug. Furthermore, this entire feature area becomes more explainable and separate from the existing Python reference types. This allows a consistent story about how Mojo supports both Pointer (for use when building data structures) and UnsafePointer (for interacting with C code).

这个提案之后还有一些 Syntax Alternative 的内容,包括重命名之前的 borrowed 等关键词,但是似乎目前这些更改还没有最终决定,毕竟 Argument Convention 和生命周期的标记方式等特性还暂时没有确定

Regardless, we should postpone any final decisions until argument conventions and lifetimes have fully settled.

值得注意的是,这个提案中明确指出 ref 并非类型的一部分,而是指调用时对于参数的约定。而在生命周期特性较为完善的 Rust 中,&&mut 以及生命周期标注实际上是类型系统的一部分[4],而在 Mojo 目前的提案中,ref 只能够在函数参数或者返回中出现,而 Reference 则是作为类型标记中可以使用的应用类型:

The purpose of Reference is to allow references to be stored in data structures. Furthermore, it supports nesting such as Reference[Reference[Reference[Int], ...], ...], ...] without implicit promotions interfering.

这也就使得返回一个 Tuple 的时候,内部的类型不能是 ref,而只能是 Reference,所以也就不能直接使用 auto-dereference 的特性了[5]

总的来说,之后的发展方向应该会包括以下几个方面:

  1. 生命周期参数,可能之后会在 Parameter 中指定(如 [life: Lifetime]
  2. ref[_] 关键字用于确定传参方式和返回值返回时的 auto-dereference
  3. Reference 类型(或者命名为 Pointer)作为一个可以用于类型标记的引用类型
  4. 更加明确的 Argument Convention 关键字,可能改变目前的 borrowed 等关键字

一些想法

截止目前(mojo 2024.5.3005 (0c465b6a))stdlib 中还没有完全实现参数里面使用 ref [_] self 这样的做法,但是返回一个引用以及 ref [__lifetime_of(self)] 已经被支持了,以下的代码也能够运行并且得到正确的结果:

struct Pair:
    var first: Int
    var second: Int

    fn __init__(inout self, first: Int, second: Int):
        self.first = first
        self.second = second

    fn get_first_ref(inout self) -> ref [__lifetime_of(self)] Int:
        return self.first

fn main():
    var somePair = Pair(3, 4)
    print(somePair.first)  # 3
    somePair.get_first_ref() = 1
    print(somePair.first)  # 1

目前 Mojo 对于引用、生命周期的支持还是不完善的(甚至还没有一个明确的对生命周期参数/标记的支持),但是可以期待的是下一个 Stable 版本的 Mojo 编译器应该就会支持诸如 ref 这样的语法特性了(毕竟已经在 Mojo unreleased changelog 中提到了)。另外,尽管目前编译器对生命周期的支持还不完善[6],但诸如 StringSlice 这样的带生命周期保证的类型已经能正确编译了:

fn main():
    var s = String("Hello, World!")

    var s_ref = s._strref_dangerous()
    print(s_ref)

    var s_slice = s.as_string_slice()
    print(s_slice)

此处 s_ref 这样直接指向字符串内部的类型也能够正确输出,说明 ASAP 应该是被延后到了 s_slice 之后。

此处对生命周期和这一系列提案主要内容的总结和最后 Mojo 的完整解决方案可能还会存在较大的差别,等最终完整的解决方案定稿之后或许可以再和 Rust 和 C++ 进行一些对比。


  1. Keyword naming and other topics to discuss. ↩︎

  2. Mojo references, an alternate take. ↩︎

  3. Auto def-Reference for Mojo ↩︎

  4. Subtyping and VarianceTrait and lifetime bounds ↩︎

  5. 见提案中 Multiple results wouldn’t get automatic dereference 这一节的内容 ↩︎

  6. 关于生命周期的提案见 Provenance Tracking and Lifetimes in Mojo ↩︎

2 Likes