読者です 読者をやめる 読者になる 読者になる

#rustlang における構造体のmutabilityと`Cell/RefCell`

Rust

この記事はRust 0.10を基準に書かれている

前提

Rustの基本として、明示的にmutを付けて値の変更を可能にした(mutableにした)データ以外は変更することができない(デフォルトimmutableの原則とでも呼ぶべきかな)。これを構造体に適用した場合、構造体のフィールドのmutabilityは、フィールドを保持する構造体のそれを引き継ぐ。つまり、親のmutabilityを子は引き継ぐという原則がある。

なので、こういう感じのコード(疑似コードです)はコンパイルエラーになる。

let bar = Bar {
  bar: Hoge {
     hoge: 0
  },
  barbar: 0,
};

// `bar`はimmutable
bar.barbar = 1;

// `bar.hoge`は`bar`のmutabilityを継承するので変更できない
bar.hoge.hoge = 1;

まあ詳しくはチュートリアル読んでください。mutまわりのセマンティクスはそのうち変わるかもしれないし(そういう議論がある)。

この厳格なmutabilityの継承規則は、コードを触る側からすれば安心感がある。メソッドの第一引数で&mut selfなどとしない限り、そのメソッドの空間内では、絶対にデータ構造の子孫方向の変更ができないからだ。コードが長期的ないし大人数で保守されているほど、この規則はありがたい。

とはいえ、欠点も存在している。それは、引数Aをmutableな状態で呼び出さなければならないメソッドを、引数Aをimmutableに受け取っているメソッドから呼び出せないということ。具体的にはこんな感じで、selfがらみで頻発し易い。

fn foo(&self) {
    self.bar(); //selfはimmutableなのでコンパイルエラーになる
}
fn bar (&mut self) {
    …
}

/// 逆なら問題ない
fn bar_2(&mut self) {
    self.hoge_2();
}
fn hoge_2(&self) {
    ….
}

unsafeにして、cast::transmuteなどで強制的にmutableに属性変更するという手口もあるが、それはRustの目指す方向性に反するので、何度も繰り出していい技ではない。

とはいえ、呼び出しのネストが3世代くらい先のメソッドだけがmutableに引数を受けなければいけないのに、不必要なものまでmutableに受けるというのは気持ち悪い。デフォルトimmutableの恩恵が得られない。ましてや孫にあたるフィールドを変更したいだけなのに、こんなことをしたくはない。超地獄っぽい。

std::cell::{Cell, RefCell}を用いた解決

というわけで、このmutabilityの連鎖を断ち切るために、Rustには標準ライブラリstd::cell::{Cell, RefCell}を用いた抜け道が用意されている。

これらのデータ型を用いることにより、親のmutabilityを気にせずに中身を変更できる領域を作ることができる。どうやってこの構造を実現しているのかは簡単なんでコードを読んでくださいな。

CellRefCellもできることに大差はないのだけれども、名前の通り、前者はPod型(Plain old data)を入れるのに使い、後者は構造体などのコピーに時間がかかるデータを入れるのに使う。

// Cell<T>
struct Hoge {
    hoge: Cell<int>,
    bar: 0,
}
let hoge = Hoge {
    hoge: Cell::new(0),
    bar: 0
};
let hoge_hoge = hoge.hoge.get(); // 中身はPODなので値のコピー
hoge.hoge.set(1); // `hoge`はimmutableだけど、`Cell.set()`を使って値の変更が出来る
struct Fuga {
    fuga: RefCell<Hoge>
}

let fuga = Fuga {
    fuga: RefCell::new(Hoge {
        …// 初期化
    });
};
let fuga_fuga = fuga.fuga.borrow(); //基本的にborrowして中身にアクセス

fuga.fuga.borrow_mut().bar = 1; //mutable borrowなので変更できる

let copy_fuga_fuga =fuga.fuga.get(); //RefCell<T>が`Clone`を実装してれば、中の値をclone()して取得する.

このようにすることで、mutability chainを断ち切ることができるので、ネストのために不必要なmutable受けを消すことができるようになり、人は幸せになれる。

Servoでの適用事例

色々あるんですが、trickyな前提の上でこれを使っているので、説明がややこしい。ので、気が向いたら(上手く説明できる自身がついたら)そのうち書く。

関係ないけど

Rustってググラビリティ低いので、せめてTwitterでは#rustlangってハッシュタグ付けてる