飴屋

WebAssembly/JavaScriptのプロパティみたいにしたい

便利さにコストはつきもの

JavaScritptは深く型のことは理解せずとも書ける敷居が低い言語だなぁと改めて思ったこの頃です。クラスのプロパティに何を持たせようが、ただちに問題になるわけではなくて、Personクラスのageプロパティに数値で16と入れようが、文字列で"二十歳"と入れようが、['3',2,'years old']と配列を入れようが、100歳クラスのインスタンスを入れようが、null値を入れようが好きにすればよいのです。(それを扱う側は大変そうだけど。)先の例では配列やインスタンスなんかは実体が別にあって、その実体をプロパティが参照することになっている仕組みなので、同じ配列やインスタンスを別のプロパティや別のオブジェクトのプロパティから参照することも簡単にできちゃいます。代入演算子(=)は偉大なり。

Rustで似たようなことをしようとするとすごい大変だったので、ここに書いておこうと思います。JavaScriptは裏でコストをかけて頑張っていたんだ。

Rc

まずRustにはデータの所有権なる概念があるので、複数の構造体で同じデータを所持できません。サンプルに人間関係を表すPerson構造体を考えてみます。

struct Person {
  child: Person,
}

AさんとBさんは結婚して子供Cができた場合、それぞれのchildメンバにCを持ちたいのですが、Rustが親権(所有権)を一人にしか認めてくれないので、AさんとBさんはとても気まずい関係になります。子供Cをcloneして同じ子供を二人に増やしてそれぞれ所持することもできるかもしれませんが、クローン人間に倫理的な問題が・・・じゃなくて、人間関係を表すという当初の目的を考えれば、人間を増やして管理工数を増やしてどうするんだって話です。

実データは所有できなくとも、ポインタならcloneしても差支えないだろうってことで、よく見かけたのはRcというスマートポインタを使った実装でした。ポインタの参照された回数をカウントしてくれるので、カウンタが0になった段階で参照がなくなったとして、メモリから消えてくれるようです。

struct Person {
  child: Rc<Person>,
}

Rc::newでポインタを作って、Rc::cloneで同じポインタを複製して、AさんBさんそれぞれにchildはCですって渡してあげることができるようになりました。ポインタって物で例えるなら何がいいですかね?名札とか出生証明書とか本人確認のできる書類みたいなものか。今ならマイナンバーとか。

Option

でも、子供がいないとPersonになれないというのもちょっと問題ですよね。子なし家庭も表現したいかもしれない。というわけで、JavaScriptのときは子供をnullと表現していましたが、Rustでは代替的によく使われるOption型にしてみます。

struct Person {
  child: Option<Rc<Person>>,
}

列挙型なので、子供がいるときはSome、いないときはNoneになるわけですね。Optionに限らず、if let操作で値を取り出す文化にもちょっとなれてきましたね。実際の実装時は子供が要ることが事前に決まっていたのでunwrap操作で済ませましたが。

あっ、例題では子供Cがそもそもまだ子持ちではなかったので、これで表現できるようになったのかな?

RefCell

ただ、ポインタの指す子供の情報を変更したい場合、Rcスマートポインタは中身を書き換えられません。不変参照っていうらしいです。そこで中身を改変できるRefCell構造体でラッピングしてあげるのがよさそうです。内部可変性っていうらしいです。

struct Person {
  child: Option<Rc<RefCell<Person>>>,
}

型の入れ子表現がだいぶ激しくなってきました。Rcの中身のRefCell自体は不変だけど、RefCellの中身は変更できるということですかね。mutキーワードが躍るRustのことを知るには可変性のことをもっと深く知っている必要がありそうです。

RefCellの中身を取り出す場合、borrowメソッドで不変な借入、borrow_mutメソッドで可変な借入ができるようです。借入は無制限というわけではなく、複数のborrowもしくは一つのborrow_mutが許されているとのことで、この貸出制限はランタイムでチェックされるみたいです。知らずに借入しまくって何度も「Already borrowed」って怒られました。ご利用は計画的に。

Weak

さて、ここまで進んだところで、参考記事で「循環参照によるメモリリーク」というトピックが目につくようになりました。C++で何か作ってた時も、ガベージコレクションを持っている言語の記事のこぼれ話でも似たような話をよく聞きましたね。

struct Person {
  partner: Option<Rc<RefCell<Person>>>,
  child: Option<Rc<RefCell<Person>>>,
}

Person構造体をちょっと変更して配偶者を表すpartnerメンバを追加してみました。AさんとBさんは互いのスマートポインタを交換するように互いのpartnerメンバに持ち合うことになります。互いに互いを参照しあうこの状態が循環参照ですね。互い(のpartnerのRc)が互いを所有しあうので、メモリ内で二人の愛は消えることなく刻み込まれ続けます。すなわち、メモリリーク!

適切なスコープ内で二人の寿命を消尽して欲しいので、RcをWeakに変更するのがいいと書いてありました。世にいう弱参照ってやつですか。Rcは参照の回数をカウントして自分の寿命を知りますが、Weakで参照した場合は別途カウントしているそうです。それぞれstrong_count,weak_countメソッドでカウント数を調べられるとのこと。たくさん弱参照されてても適切な寿命がきたら(強参照がなくなったら)メモリからは消えることができます。これで循環参照によるメモリリークは解決します。循環弱参照なら問題ないと。

ついでに親より先に子供が死ぬかもしれないので、childの参照もWeakに変更しました。

struct Person {
  partner: Option<Weak<RefCell<Person>>>,
  child: Option<Weak<RefCell<Person>>>,
}

それでWeakの場合、参照先が寿命で先に消えてしまいリンク切れみたいな状態になることが普通に起こり得るので、Rcとは少し扱い方が変わります。Weakの参照先にアクセスすると、参照先がまだ生きているかどうかを確認するためのOptionが返ってきます。Aさんの連れ合いBさんが先に亡くなった場合、partnerメンバからBさんを辿ろうとしても、Bさんは既にNoneという結果が返ってきます。逆にこのリンク切れみたいな状態を意図的に起こすことも可能で、Weak::newメソッドを呼べばNoneな中身のWeakが返ってきます。

となると、そのリンク切れみたいな状態を使って、「子供がいない」「未婚(または死別)」を表せそうなので、最初につけたOptionは不要になりそうです。

struct Person {
  partner: Weak<RefCell<Person>>,
  child: Weak<RefCell<Person>>,
}

最終的にこんな感じの構造体で実装を進めることになりました。いろんなことがわからなくて、ここに至るまで結構時間がかかりました。

そうそうWeakの使い方はRc::newで作ったり、Rc::cloneで複製したポインタをRc::downgradeを使ってWeakポインタに降格させるところから始まります。実際にWeakの中身にアクセスする場合は、まずWeak::upgradeメソッドを使ってRcポインタに昇格させます。その際返ってくるOptionでリンク切れでないかどうかを確認します。昇格したらRcと使い方は一緒です。

new, clone, downgrade, upgradeそれと任意のタイミングで参照を消し込むdropメソッドあたりが発生すると、参照カウントが増減してくれるわけですね。

全部弱参照だと誰の所有かわからなくなるので、全てのPerson(へのRc参照)を所有するデータも用意してあります。神構造体(スコープ)というか住民台帳構造体(スコープ)というか。

これでJavaScriptのプロパティに近づいたと思います。列挙体とかを使ってもっといろんなデータを持てるようにすると、さらに近づけるのかな。大変だからやんないけど。JavaScriptはそれをやっているわけで、こういうところで計算コストと動作スピードを秤にかけることになるんでしょう。