WebAssembly/Rustで実装
WebAssemblyとしてブラウザを操作する方法がおおまかに掴めたので、あとは設計して実装すればよさそうなものなんですが、Rust自体が初めて触れているのでまだ感覚が慣れません。丁寧にコンパイルエラーを吐いてくれるコンパイラがありがたいのですが、何を言っているのかわからないときもあって、ググりながら正解を探して実装してはエラーとにらめっこという感じです。やはりRustの特徴である「所有・借用・lifetime」「必ずしもオブジェクト指向ではないジェネリクス・トレイト」の辺りで躓くことが多そうです。新しく、進化も激しい言語なので古い情報を鵜呑みにするのも事故の元になりそうです。
モジュール
Rustにはクレートというパッケージシステムがあるのですが、とりあえず動かしたいだけなのでクレートをまとめる前にモジュール化に挑戦しました。src/lib.rsに実装を進めていたのですが、いい加減分量が大きくなってきてファイルを複数にわけたかったのです。
mod hoge;
↑src/lib.rsの頭にこんな一行を追加すると、src/hoge.rs(もしくはsrc/hoge/mod.rs)を探して読み込んでくれるみたいです。C言語でいう#includeみたいなものかな。とりあえずファイルを分冊して増えてきた構造体(struct,trait,impl)やら定数(const,static)やらを整理していきたいです。traitはどこに書こう?
(追記)開発を続けるうちに内容がどんどん増えてきて、モジュールの中にサブモジュールも作りたくなってきました。
src/lib.rs └ src/hoge.rs └ hoge/sub_hoge.rs
↑こんな階層構造を持たせることにします。
mod sub_hoge;
src/hoge.rsの先頭にサブモジュールの存在を明記して、いくつかのstructやimplやtraitをhoge/sub_hoge.rsに移動させてみました。結果、コンパイルエラーが大量に発生しました。どうやらモジュールとサブモジュールは互いに独立していて、親子のような関係に見えても勝手にアクセスはできないようです。自分以外の外側からアクセスできるものにはpubで修飾すればいいのですが、pubをつけると逆に誰からでもみえてしまって、セキュリティ的にはリスクができてしまいそうです。と思ったら、pubにアクセス範囲を限定するキーワードがつけられるそうです。
- pub(in hoge)
- 指定のパスのモジュールに公開
- pub(crate)
- 同じクレートの中で公開
- pub(super)
- 親モジュールに公開(pub(in super)と同等)
- pub(self)
- 自身のモジュールに公開?(pub(in self)と同等)
とりあえずサブモジュールに移動した構造体やメソッドなどに、必要に応じてpub(crate)をつけることにしました。
構造体(SubHoge)などはサブモジュールに移動したことで所属モジュールが変わってしまったので、hogeモジュールではuseを使って修正を最低限に抑えます。
use self::sub_hoge::SubHoge;
// use crate::hoge::sub_hoge::SubHoge; みたいに絶対パスっぽくも書ける
sub_hogeモジュールからは逆にhogeモジュールの構造体をuseしたりしました。
use super::Hoge;
// use crate::hoge::Hoge; みたいに絶対パスっぽくも書ける
selfが自身で、superが親と。
オブジェクト指向
Rustでは他の言語でクラスと呼んでいたものはなく、疑似的に構造体(struct)とメソッドの実装(impl)を使ってオブジェクト指向っぽいことを実現するようです。ただ、ちょっと勝手が異なります。
最初、「カメラ(Camera)」を表すクラスを作ろうとしました。カメラには「奥行方向に移動できるカメラ(ZCamera)」「上下方向に移動できるカメラ(YCamera)」の二種類が必要です。全てのカメラには「撮影する」機能があります。まずは基底クラスとしてCameraクラスを実装して、それを継承して・・・あれ、継承ってないのか・・・。structはデータ構造を定義するだけなので、CameraとZCameraとYCameraとで共通する「撮影する」メソッドを、それぞれの構造体のimplに別個に実装するの?いや、そんなわけないよな・・・と、ここでしばし悩みました。
struct Camera {} impl Camera { fn shoot(&self) {} } struct ZCamera {} impl ZCamera { fn shoot(&self) {} } struct YCamera {} impl YCamera { fn shoot(&self) {} } // 同じこと3回も書きたくないよなー
Rustにはtraitという「メソッドを実装する制約」みたいなものがあるので、これを使って共通するメソッドを定義するのかなと、試してみますが、traitは制約でしかないので使い方としては間違っていると感じました。traitにデフォルトメソッドを用意したとして、デフォルトメソッドにはどんな構造体が渡ってくるのかわからないので、プロパティにアクセスできないのですよね。それじゃ実装は無理ですよね。
trait Camera { fn shoot(&self){ // Cameraをtraitにしてみたけど、traitはデータ構造を持たないので、selfで操作できることは少ない } } struct ZCamera {} impl Camera for ZCamera {} struct YCamera {} impl Camera for YCamera {}
最終的に同じメソッドは同じ構造体で表せばいいのだ、ということに気が付きました。撮影するメソッド(shoot)はレンズを表す構造体(Lens)に実装して、各カメラにレンズを持たせればいいと。オブジェクトの振る舞いがメソッドであるように、構造体の振る舞いは構造体として持たせるのですね。これで何となく設計の方針が立ちそうな気がしてきました。
struct Lens {} impl Lens { fn shoot(&self){ // 同じことをするのは同じ部品(構造体)でよくて、カメラ毎に機能が異なる部分はカメラ毎に実装すればいい } } struct Camera { lens:Lens, } struct ZCamera { lens:Lens, } struct YCamera { lens:Lens, }
列挙型
設計が進むにつれて構造体では表せないデータ構造がどうしても出てきて、行き詰まりを感じるようになりました。前述のトレイトオブジェクトはやっぱり構造体のデータ構成が未知なので、使い勝手が悪いときがありました。
struct Circle { radius:f64, } struct Rectangle { width:f64, height:f64, } trait Shape { fn draw(); }
円(Circle)と長方形(Rectangle)の構造体に図形(Shape)トレイトをつけて、図形の描画メソッドをそれぞれ実装できるけど、円と長方形を同列に扱いたいときにちょっとややこしくなります。私はベクター(Vec)とか配列に入れてこれらの図形を一元管理したいのですが、データ型が異なるのでボックス化(Box<dyn Shape>)したトレイトオブジェクトとしてベクターにプッシュしてきました。順番に図形を描画(draw)する分にはいいけど、図形の半径プロパティにちょっとアクセスするにも、その図形が円なのか長方形なのかわからないので、円にも長方形にも半径を調べるメソッドを用意せざるをえません。長方形には半径なんてないのに!
enum Shape { Circle {radius:f64}, Rectangle {width:f64, height:f64} } fn draw(shape:Shape) { match shape { Shape::Circle{radius} => { // 円を書く }, Shape::Rectangle{width,height} => { // 長方形を書く }, } }
そんなときは列挙型を使うみたいです。私は列挙型って事前に決まった選択肢から一つ選ぶだけのデータ型としてしか認識してなかったのですが、そんな一意の名前がついたユニット型選択肢のほかに、タプル型やフィールド型の選択肢も用意できるようです。すなわち半径3cmの円も半径10cmの円も縦横2cm×5cmの長方形もShape列挙型で表現できちゃうんですね。match文を使えば図形の種類もサイズも自在に調べられます。これを知らずに長々書いてきたソースをガッと書き直したら、シンプルで見やすくて短いプログラムに生まれ変わりました!
列挙型って内部でどういう構造になっているのかちょっと不思議な感じがします。