😊

Mojo の borrowed, inout, owned

2023/09/09に公開
1

Pythonと互換性がありながら、高速化された新言語 Mojoが一般公開されたので、色々コンパイラで遊んでみました。
その研究記録をここに残します!!

borrowed, inout, owned について

Mojo には関数の引数として borrowed, inout, owned を取ることができます。それぞれ引数がどのように関数内で扱われるかが異なります。
RustやC++の参照・ポインタを思い出します。

borrowed

まず基本的なのが、borrowedです。これは Mojo の引数のデフォルトの設定であり、省略が可能です
つまり

fn add(borrowed x: Int, borrowed y: Int) -> Int:
    return x + y

と書こうと、

fn add(x: Int, y: Int) -> Int: # 型アノテーション
    return x + y

と書こうと等価です。以降では、borrowedは書かない書き方で説明します。

borrowed な変数は不変参照 (immutable references)です。したがって次のような変数の変更は行えません。Rust でいう &Tに近いです。

fn add_y_to_x(x: Int, y: Int):
  x += y # error: expression must be mutable for in-place operator destination

inout

borrowed が不変参照だったのに対し、inoutは可変参照です。上のadd_y_to_xxinoutにすることでエラーが消えます。Rustでいう&mut Tに近いです。

fn add_y_to_x(inout x: Int, y: Int):
  x += y

fn main():
  var x = 1
  let y = 2
  add_x_to_y(x, y)
  print(x)

inout な引数にはvarまたは Python的な変数しか渡せません。

fn main():
  let x = 1 # ここが var でないといけない
  let y = 2
  add_x_to_y(x, y) # error: invalid call to 'add_x_to_y': argument #0 must be mutable in order to pass as a by-ref argument
  print(x)

Pythonとの相互運用がどこまで可能なのかというと、以下は動作します。

fn add_y_to_x(inout x: Int, y: Int):
  x += y

def main():
  x = 1 # let でも var でもない、非効率な変数。
  let y = 2
  add_y_to_x(x, y)
  print(x)

owned

owned は(後ほど説明するtransfer 演算子を使わない場合は)引数に渡された場合、コピーを作成します。RustでいうTに近いです。

fn set_fire(owned text: String) -> String:
    text += "🔥"
    return text

fn main():
    let a: String = "mojo"
    let b = set_fire(a) # a のコピーが作成され、bに代入される。aには"🔥"が結合されない。
    print(a)
    print(b)

コピーがいらないという場合はtransfer演算子を使えば、aという変数の寿命をそこで終わらせることで、コピーなしでset_fire関数を呼べます。また、a^を呼び出した後にaを使えないことをコンパイラが処理してくれます。

fn main():
    let a: String = "mojo"
    let b = set_fire(a^) # a から set_fire 内の変数にtransfer
    print(a) # error: use of uninitialized value 'a'
    print(b)

ちなみに、a^した後は、let a = ""と再宣言することもできません。

fn main():
    let a: String = "mojo"
    let b = set_fire(a^)
    let a = "" # error: invalid redefinition of 'a'
    print(b)

ここでクイズ!

次はどんな挙動になるでしょう。

fn main():
  let a = 0
  var b = 1
  b = a
  b += 1
  print(a)
正解
0

が出力されます。
b = a の時には必ずコピーが発生するようです。
__copyinit__が実装されていない場合には、cannot be copied into its destinationというエラーが出ます。

struct における inout など

struct でも selfinout 指定をすることがあります。これはただ単純に構造体のインスタンスがselfに代入されたと考えればいいです。Rustでも&self &mut self self があったのと同様です。

struct MyPair:
    var first: Int
    var second: Int

    fn __init__(inout self, first: Int, second: Int):
        self.first = 1
        self.second = second
    
    fn dump(self):
        print(self.first, self.second)
    
    fn add_second_to_first(inout self):
        self.first += self.second

var p = MyPair(1, 2) # __init__の時にはselfに何らか変更を加える。
p.dump() # dump(a) のイメージ
p.add_second_to_first() # add_second_to_first(p) のイメージ
p.dump() # 3, 2

なお、__init__では必ず、inoutselfを渡す必要があります。

struct MyPair:

    fn __init__(self): #  error: 'self' in struct '__init__' must be passed as mutable reference
        pass

struct は __copyinit__ を実装していないとコピーできない (a = b のような代入もできない)ので、ownedとして引数に呼ばれることもできません。
__copyinit__ が実装された状態では、以下のように動作します。

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)
    
    fn add_second_to_first(inout self):
        self.first += self.second
    
    fn copy_and_update_first(owned self, first: Int) -> Self:
        self.first = first
        return self
    
    fn __copyinit__(inout self, rhs: Self): # これがいないとそもそもcopyができない
        self.first = rhs.first
        self.second = rhs.second

fn main():
    var p = MyPair(1, 2)
    p.dump() # 1, 2
    let copied = p.copy_and_update_first(-100)
    p.dump() # 1, 2
    copied.dump() # -100, 2
    let moved = (p^).copy_and_update_first(100) # __moveinit__ の実装は不要
    moved.dump() # 100, 2

より詳細な __copyinit__ __moveinit__ 周りの話は以下の記事を参照してください。
https://zenn.dev/ttttkkkkk/articles/295089fcf26129

structでも同様のクイズ!

fn main():
  let a = MyPair(1, 2)
  var b = MyPair(3, 4)
  b = a
  b.add_second_to_first()
  a.dump()

これはどうなるでしょう?そもそもコンパイルは通るでしょうか?

正解

コンパイルが通り、

1, 2

が出力されます。つまり不変な aは変更されません
代入操作ではコピーが作成されます。

今までの知識でMyIntを実装

__copyinit__というコピーの時に利用されるコンストラクタと、__add__ __iadd__のようなPythonにあった演算子オーバーロードを使うことでこのように、Intのラッパーを作ることが可能です。

struct MyInt:
    var value: Int
    
    fn __init__(inout self, v: Int):
        self.value = v

    fn __copyinit__(inout self, other: MyInt):
        self.value = other.value
        

    fn __add__(self, rhs: MyInt) -> MyInt:
        return MyInt(self.value + rhs.value)

    fn __iadd__(inout self, rhs: MyInt):
        self = self + rhs

Discussion