Mojo の borrowed, inout, owned
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_xはxをinoutにすることでエラーが消えます。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 でも self の inout 指定をすることがあります。これはただ単純に構造体のインスタンスが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__では必ず、inoutなselfを渡す必要があります。
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__ 周りの話は以下の記事を参照してください。
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
その他をまとめた記事