Rustの参照の規則に文句を言いたくなったら設計を見直してみよう

先日、プログラミング言語Rustについての以下の投稿を目にした。

https://twitter.com/tanakahisateru/status/1694364197407396116

以下に省略したコードを載せる。

```rust:最初のコード
struct CanvasOps {
    canvas_rc: Rc<RefCell<Canvas>>,
}

impl CanvasOps {
    fn cls(&self) {
        let mut canvas = self.canvas_rc.borrow_mut();
        canvas.set_draw_color(...);
        canvas.clear();
    }

    fn tofu(&self) {
        let mut canvas = self.canvas_rc.borrow_mut();
        canvas.set_draw_color(...);
        canvas.fill_rect(...);
    }
}

...

let canvas_rc = Rc::new(RefCell::new(canvas));
let op = CanvasOp { canvas_rc: Rc::clone(&canvas_rc) };
op.cls();
op.show();
canvas_rc.borrow_mut().present();
```

`CanvasOps`は`Canvas`に対する操作を行うための構造体のようだ。このコードでは、`CanvasOps`は操作対象の`Canvas`を所有する。しかし、一連の操作を終えたあとは`CanvasOps`ではなく`Canvas`自体が持つメソッドを呼び出す必要がある。

# 単一書き込み原則

PythonやJavaのように、すべてのオブジェクトを参照を通して制御し、かつ可変性の指定をまともに行えない言語では、`Canvas`への参照を誰が持っていようと関係なく、ほぼ好きなタイミングで`Canvas`を操作できる。(Javaの `final` やPythonの `dataclass(frozen=True)` は子に伝播せず、限定的にしか機能しない)

しかし、Rustでは参照の規則として「一つの可変参照か不変な参照いくつでものどちらかを行える」が徹底されているため、オブジェクトを操作する場合は、そのオブジェクトを誰が持っているかが重要となる。

https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html#the-rules-of-references

この規則を破りたい場合は、`Rc`や`Arc`といった参照カウンタと`RefCell`や`Mutex`による内部可変性パターン (interior mutability) を使用しなければならない。しかし、参照カウンタや内部可変性パターンは実行時オーバーヘッドが発生するため、まずは`RefCell`や`Mutex`を使用せずに実装できないか検討すべきだろう。

# 解決策

以下では内部可変性を用いない実装を示す。

## CanvasOps構造体を用いない

最初のコードでは、`CanvasOps`に`Canvas`を持たせた後に、構造体の外側で`Canvas`を操作するため、`Canvas`を`RefCell`で包む必要があった。しかし、そもそも`CanvasOps`は`Canvas`を持つ必要があるだろうか。

`CanvasOps`は`Canvas`を操作するためのユーティリティーとしての機能しか持っていないため、操作の度に`Canvas`の可変参照を引数で与えた方が単純である。また、`CanvasOps`に設定が要らないのであれば、`CanvasOps`構造体を無くし、各メソッドはモジュールの関数にすれば十分だろう。

```rust:CanvasOpsの各メソッドをモジュールの関数にしたコード
fn cls(canvas: &mut Canvas) {
    canvas.set_draw_color(...);
    canvas.clear();
}

fn tofu(canvas: &mut Canvas) {
    canvas.set_draw_color(...);
    canvas.fill_rect(...);
}

...

cls(&mut canvas);
show(&mut canvas);
canvas.present();
```

## すべての操作をラッパーを通して行う

最初のコードのように`Canvas`をラップした構造体を作りたいのであれば、すべての操作をラッパーを通して行うようにしたほうが良いだろう。
一部の操作のみ直接`Canvas`に対して行うということは、`CanvasOps`の詳細が外に漏れているとも言えるので、あまり良い設計とは言えないだろう。

```rust:すべての操作をラッパー経由で行うコード
struct CanvasOps(Canvas);

impl CanvasOps {
    fn cls(&mut self) {
        self.0.set_draw_color(...);
        self.0.clear();
    }

    fn tofu(&mut self) {
        self.0.set_draw_color(...);
        self.0.fill_rect(...);
    }

    fn present(&mut self) {
        self.0.present();
    }
}

...

let mut wrapper = CanvasOps(canvas);
wrapper.cls();
wrapper.tofu();
wrapper.present();
```

## トレイトによる構造体の拡張

SNSで複数の人が挙げていた解決策として、トレイトを用いて`Canvas`を拡張する方法がある。`CanvasOps`トレイトを作り、各メソッドを`Canvas`に実装すると以下のようになる。

```rust:トレイトによる構造体の拡張
trait CanvasOps {
    fn cls(&mut self);
    fn tofu(&mut self);
}

impl CanvasOps for Canvas {
    fn cls(&mut self) {
        self.set_draw_color(...);
        self.clear();
    }

    fn tofu(&mut self) {
        self.set_draw_color(...);
        self.fill_rect(...);
    }
}

...

canvas.cls();
canvas.tofu();
canvas.present();
```

ただし、この方法は私はあまり好みではない。`Canvas`構造体が持つメソッドと`CanvasOps`が持つメソッドの粒度が異なるからだ。トレイトを用いるなら、上述のラッパー構造体に対して実装したほうが良いように思う。

# まとめ

Rustの参照の規則に文句を言いたくなったら設計を見直してみよう。