Rustでグローバルに影響するような状態を持ったプログラムを作る時はどうするか

具体的にはメインループを回して、ユーザーからの入力を受け付ける必要があって、且つ状態をいくつか持ちたい場合、Rust(0.8相当)ではどうするのが得策かという話。割とテキトー。

便宜上、グローバルと呼んでいるけど、「メインループに類するもので変更されうる、広範囲に影響する状態」程度に考えてもらえばOK。

Rustに限った話とは思ってないけどね。

アプローチ1: unsafeにグローバル変数を作る

安直だし、可能だけど、ダウト。並列時の安全性の観点でダウト。バグを作り込む要因なのでダウト。unsafeは最終手段なのでダウト。

アプローチ2: main関数内に変数を作り、適宜、関数ごとに必要な引数を渡す

安直なアプローチその2。ただし、unsafeでないのは魅力的。変数にmutable属性を設定することで、一度生成したら変わらない変数と、しょっちゅう書き換わる変数を静的に区別できるので、触ってはいけない変数に触った場合はコンパイルエラーにできる。

とはいえ、入力などのイベントハンドリング個所を外に出すと、ハンドル用の関数に引数のほぼすべてを渡さないといけないし、かといってループ内でmatchでハンドルするようにすると、ループがどんどん巨大になる。あんまりうれしくない。

ついでに変更する変数が参照なのか実体なのかをいちいち考えるのが面倒くさくなってくる。

アプローチ3: クロージャを作り、クロージャ内部から状態を持った変数を参照する

アプローチ2の改良版(後述の理由で退化かも)。Rustだとクロージャが作れるので採用できる。

ただし、状態を参照するクロージャが巨大になればなるほど、コードの可読性は下がる。出来の悪いjQueryのプラグインみたいなコードになる。変数を探して関数内を上がったり下がったりと、正直ろくでもない。おまけに変数の生存区間は伸びるし。全部インラインに展開してクロージャも使わない方がまだマシなんじゃないかという気分になる。最初簡単だった分のコストをツケでまかなっておいて、コード規模が増大すると払わされる感じがある。

Servoのcompositorのコードがまさにこんな感じで、読む側からすると辛かった。

アプローチ4: オブジェクトのインスタンスのメンバとして状態を持つ

オブジェクティブなアプローチ。スコープが肥大化する問題も、変数をいちいち渡す問題も、一挙両得で解決できる。

欠点として、状態を変えるので、常にselfをmutableにする必要がある。&selfのようにborrowed boxとして渡すと、borrowedに伴う参照先のfreezeの関係から、実際には状態を変更しないメソッドですら、&mut selfとして渡す必要があったりする。これはこれで精神衛生上、ちょっと気になる。とはいえ、他の言語ではデフォルトがmutableなので、諦め所かもしれない。メソッドを丁寧に分割すれば、1メソッド辺りの行数が減るので、値の変更箇所の見通しもつくようになるしね。 ちなみに、selfと表記してmoveして渡す事で必要な箇所だけmutableにすることはできるけれども、ループを回す場合に後続(次ループ)のメソッドではmoveされた値を参照する事になってしまい、コンパイルエラーになったりする。

どのアプローチを使うか?

ある程度規模が膨らむ前提なら、最初からアプローチ4で組んだ方が楽ですが、まあスピード感考えると最初はクロージャにした方が楽ですね。

ちなみにServoのcompositorのコードでは、最初はクロージャを使っていて、あとでオブジェクトを作るようにした