こんにちは!最近セミナーに向けてバイオリンの練習を加速させているバックエンドコースの ama です。4月になり、進級できて安心しています。
実現したいこと
Rustで以下のようなパターンを実現したいです。
struct Master {
/// 変更可能な状態
state: State,
}
let mut master = Master::new();
let user1: User = master.login("Ex4mp13_70k3N");
user1.execute(ActionA {
// アクションに必要なデータ
});
master
に変更可能な状態 state
を持たせ、 state
をログインした User
のインスタンスから遠隔的に変更することを目指します。
User
はメソッド execute
によって親の Master
内にある state
を変更します。変更方法はトレイト Action
を実装した構造体を渡すことで指定します。
💡 変更方法は1種類ではなく複数種類を定義できるようにしておきたいので、複数の構造体に共通のトレイト:Action
を実装することによって実現することにしました。Action
トレイトは実際の変更動作を定義するメソッドfn modify(self, state: &mut State)
の実装を強制し、構造体自体の中身にはそれぞれの変更に必要なデータを持たせておきます。
考えるべき実装内容は、構造体 User
のメンバーおよびメソッドです。
実装
execute
はトレイトを実装した構造体全般を受けとるので impl Trait
を使えば以下のように定義できるはずです。
impl User {
fn execute(self, action: impl Action) {
// action.modifyをここで呼び出す
}
}
よって、 action.modify
に渡すミュータブル参照(またはミュータブル参照を得られるもの)を User
はメンバとして持っておく必要があります。
Stateを複数回参照する
execute
の中で action.modify
のみを呼びだす場合はこれでよいですが、 execute
の前後にも State
を参照したり変更を行いたい場合は問題が生じます。例えばデコレータのように、 action.modify
の前後で State
の変更に応じた動作を加えたい場合などです。
素直に実装すると以下のようになりますが、これは Borrow Checker に引っかかります。
struct User<'state> {
state: &'state mut State,
}
fn execute(self, action: impl Action) {
action.modify(self.state); // ここで self.state が消費される。
callback_after_action(self.state); // ドロップ済みの self.state は使えない。
}
少なくとも action.modify
での State
変更のために State
のミュータブル参照を得る必要がありますが、ミュータブル参照を得ている間はミュータブル・イミュータブル関わらず参照を得られないからです。
ミュータブル参照をクローンすることはできないため、再参照を用いて必要に応じてミュータブル参照を生成できるようにすれば解決できます。これは std::cell::Cell
によって実現できます。
struct User<'state> {
state: &'state mut Cell<State>,
}
fn execute(self, action: impl Action) {
action.modify(self.state.get_mut()); // self.state.get_mut() のライフタイムは modify の中。
callback_after_action(self.state.get_mut()); // 前の参照が破棄されているので再参照できる。
}
// Master のメンバも変更する必要がある。
struct Master {
state: Cell<State>,
}
結果
ここまでをまとめると以下のようになります。
use std::cell::Cell;
struct State(String);
trait Action {
fn modify(self, state: &mut State);
}
struct ActionA {
new_value: String,
}
impl Action for ActionA {
fn modify(self, state: &mut State) {
state.0 = self.new_value;
}
}
struct Master {
state: Cell<State>,
}
impl Master {
fn new() -> Self {
Self {
state: Cell::new(State(String::from("initial"))),
}
}
// 今回はテーマ外のため認証しない。
fn login(&mut self, _token: &str) -> User {
User {
state: &mut self.state,
}
}
}
struct User<'master> {
state: &'master mut Cell<State>,
}
impl<'master> User<'master> {
fn execute(self, action: impl Action) {
action.modify(self.state.get_mut());
// 変更時に出力する。ここで新たに state への参照が必要になる。
println!("State updated: '{}'", self.state.get_mut().0);
}
}
fn main() {
let mut master = Master::new();
// 確認のため初期状態の State の出力
println!("Initial State: '{}'", master.state.get_mut().0);
let user1: User = master.login("Ex4mp13_70k3N");
// ActionA の実行。ActionA と user1 はドロップされる。
user1.execute(ActionA {
new_value: String::from("new value"),
});
}
実行結果は以下のようになります。
Initial State: 'initial'
State updated: 'new value'
最後に
ポイントになったのは、ミュータブル参照を得ている間は、その参照を得ているオブジェクトに対してミュータビリティに関わらず他の参照を得ることができないということです。今回は Cell
から必要に応じてミュータブル参照を生成することでこの制約を回避しました。
Rustは最初のうちは Borrow Checker を通すことが1つのネックになると思います。ですが、慣れると個々の仕様がどんな問題をどのようにブレイクスルーしているのかに面白みが出てくるので固有な中毒性が湧いてきます。パズルが好きな人や、技術的な事項の探求に興味がある人におすすめの言語です。安全性や速度については言うまでもありません。
関係ない話
最近次世代言語モデルが流行っていますね。筆者も Notion AI を用いてみました。この記事では、記事の内容を踏まえた全体ソースコード (「結果」の項を参照) を生成するのに使っています。便利です。