桐生映司(仮)開発日記

開発の日常を書いていきます

Rustの初見殺し感がある

目次

  1. Rustの初見殺し感がやばい
  2. 初見殺しポイント
  3. なれる方法(対処療法)

1. Rustの初見殺し感がやばい

Rustは初見殺しが多い。Rustについて「難しい」、「難解」という意見がありますがこれらはおそらくRustの初見殺しだと思います。

逆に初見殺しが少ない言語の一例として、Rubyが挙げられます。私も仕事で使っていて、あまり初見殺しのような仕様はないと思います。

ゲームでいうとマリオみたいで、見ればある程度までは書けます。少なくとも別の言語で関数やクラスの使い方を知っていれば使えるようになっています。マリオもチュートリアルから詰むことはまずないです。そのように設計されているし、Rubyも読みやすいように書きやすいように設計されているからできる技と言えます。

Rustはどうでしょう。ゲームでいうと、フロムゲーだと思います。Rustの初見殺しとして「借用」、「所有権」、「ライフタイム」、「<>の表記」、「異なる配列の設定方法」、「Boxの使い方」がある。私もそこで躓きました。

Rustはこれらの使い方を知ればめちゃくちゃ便利なんです。

上記のうち「所有権」はこれがあるとコンパイル時に変なポインタの使い方を防いでくれてメモリの開放忘れを防ぐことができます。

メモリの開放忘れとは、使わないデータをメモリの中に溜め込んでしましす。こうなると、メモリにいらないデータが溜まってPCがフリーズします。

現に描画プログラムを作ったときにメモリが逼迫させてしまってフリーズを何回もしました。それを防いでくれるんです。

フロムゲーのBloodborneでもこの知らないがための初見殺しがあります。ゲーム内で「内蔵攻撃」という攻撃方法があるのですが、これを知らないと苦戦するんです。ひるませて相手の内蔵に直接攻撃をして大ダメージを与えます。知っていれば楽しいのですが、知らないとなんで死んだかわからないんです。

Rustの場合、「借用」、「所有権」、「ライフタイム」、「<>の表記」、「異なる配列の設定方法」、「Boxの使い方」を知っていれば、通常の書き方ができます。知らないとなぜコンパイルからエラーが出てくるのかも、その意味もわからないままゲームオーバーになってしまうんです。

Rubyの場合このようなところで躓くことはまずなです。そもそもRubyの書き方は今までのプログラミング言語をうまく配合したような作りをしているので分かりやすいです。

エラーが出たとしても基本的な機能なら読めばどこでエラーが出ているがわかったり、静的型付け言語ならエラーになるところをRubyがうまく回避してくれます。つまり、詰みにくいように設計されているのです。

マリオでも、それほど詰む人がないはずです。それと同じで、Rubyは詰む部分が比較的少ないと思います。

2. 初見殺しポイント

配列のコード

trait Animal {
    fn talk(&self) {
        println!("souzou ");
    }
}

#[derive(Clone, PartialEq, Debug)]
struct Status <'a> {
    pub hash: &'a str,
    pub state: f64,
}


struct TestStruct {
}

impl Animal for TestStruct {}

fn main() {
    let mut v: Vec<Box<Animal>> = Vec::new();
    v.push(Box::new(Status{hash:"test" ,state: 1.0}));
    v.push(Box::new(TestStruct{}));
}

すぐに出せるコードとしては、この異なる型を配列で持たせる方法です。

Rubyの場合はなんの苦労もなく入れることができるんですよ。Rubyでうまく解釈をしてくれて配列に異なる型を入れることができるんです。Cでもちょっと強引ですけど、ポインタ変数の配列を作っておいて配列を作ることができます。

Rustの場合は型がしっかりしているから、どのアプローチも使えないんです!Rustは配列をIntならInt, FloatならFloatで決められたら変換をしない限り、Float配列にIntを入れることができないんです。逆も同様です。

これはコンパイル時にすべて型をRustが定めてくれて、型が間違っていたらコンパイル時にエラーを起こしてくれるんです。面倒かもしれないんですけど、事前に「ここバグりそうなので直して!」といってくれると致命的なバグを回避させてくれるんです。

それで異なる配列を作ろうとすると、Rustのコンパイルを通さないといけないんです!Rustには継承に相当するシステムはありますが、従来のオブジェクト指向のように扱うように作られていないように見えました。traitはinterfaceのような動きをしているように見えたので「これ継承のように使うん?」と思って却下しました。

やっていくうちにわかったんですけど、このtraitとBoxを使えば行けることがわかったです。

let mut v: Vec<Box<Animal>> = Vec::new();

Boxの部分に注目をしてください。Vecの型にBoxを指定しました。<>の部分にはAnimalのtraitを指定しました。するとBoxの中に入っているAnimalのTraitが入った構造体なら入れられるようになりました。

すると、Animalのtraitを継承させれば配列に組み込むことができます。あとは関数型でもオブジェクト指向でもいいので配列に命令を送ることができます。

3. 慣れる方法(対処療法)

慣れる方法ですが、おそらくRustの型システムを理解するようにしたほうがいいと思います。やってみてわかったのが、Rustの難しいところはいわゆる型システムが今までと毛色が違うところです。

CはRustと比べて型システムが緩いんです。Cだと選択肢として「ポインタ変数を使う」とかがすっと出るんですけど、Rustではそれが一切なかった。Boxを使えば値をヒープに移しておいて必要なときに使うようにする。Boxに値を入れておいてあとは中にAnimalがいればよし!とすればいい。

Rustは型システムのおかげで低レイヤーで起こる問題を解決しようとしています。逆をいえば型システムの動き方さえわかれば書きやすくなります。

この型システムの説明はまだ私はできませんが、ここさえ説明できれば従来の方法の通りの教えることができます。