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

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

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

关键字的修改

关键字的修改实际上和语言的特性并无直接关系,但是对语义和概念的理解会有很大的帮助。从目前对关键字重命名的提案来看[1],目前的 borrowed,inout 以及 owned 对于后续生命周期的加入、引用类型的设计都不是很合适,所以这个提案中想使用 ref, mutref 来代替 borrowed 和 inout。另外,对于 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 的引用、生命周期还不完善,borrowed,inout,owned 实际上只是用于传参(Argument Convention)。但是之后引用等特性的加入可能会出现例如 List[ref A] 这样的类型,于是引用就从传参的方式成为了类型系统的一部分,这时候 ref 和 mutref 好像确实看着更舒适一些(而且打得字少)。

引用类型

除了以上关键字替换的提案,最近还有一个新的关于 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 Variance 及 Trait and lifetime bounds ↩︎

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

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

2 Likes