今までRustは使いまわすことのない適当なスクリプト的にしか使ってこなかったので、実際のところ本質的に難しいことは何もしてこなかった。その間は非常に楽で、言われるほど難しくないのではと思っていた。が、最近コードを使いまわそうと思って書き始めたところ、即死してしまった。
例えば、ジェネリックな構造体を作ろう。名前と座標を持つ質点ということにしよう。すると、名前は文字列でいいとして、座標にはf64
やf32
のどちらも使い得るし、そもそも座標の値は特定の型に結びついたものではない。なのでジェネリクスを使うのが妥当だ。なので以下のようなコードを書き始める。
#[derive(Debug)] pub struct Particle<T> { pub name : std::string::String, pub pos : nalgebra::Vector3<T>, } fn main() { let p = Particle::<f64> { name: "C".to_string(), pos: nalgebra::Vector3::new(1.0, 1.0, 1.0) }; println!("{}", p); }
これでもうだめだ。C++のtemplate的な思考で行くと、T
にf64
が入り、その結果Particle<f64>
が実体化され、結果、型の代入が成功するので問題なく動く。と予想される。
だがRustはそれを許さない。例えば以下のようなエラーがでる。
error[E0277]: the trait bound `T: std::marker::Copy` is not satisfied --> src/main.rs:3:5 | 3 | pub pos : nalgebra::Vector3<T>, | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `std::marker::Copy` is not implemented for `T` |
これは、nalgebra::Vector3
がT
に対してstd::marker::Copy
トレイトを実装していることを要求していることに起因している(同期曰く「Rustでは構造体の型パラメータにトレイト境界を入れるのは非推奨のはずだが……」と言うことらしいのでライブラリ側が微妙な実装をしているだけと言う可能性はある)。ここでT
にCopy
トレイトを実装していない型が代入される可能性があるということをRustコンパイラは指摘している。
使っているのはf64
で、当然Copy
トレイトを持っているだろう? と思うC++脳の人たちには、これがRustであると言うことを思い出してもらわなければならない。C++のテンプレートはコードジェネレータなので、実際にジェネレートされたコードが正しければ何の問題もない。だが、Rustのジェネリクスは、今のところ代入されていない型に対してもエラーを出し得る。「あとで誰かがT
にコピー不能型を入れたらどうなる? T
はnalgebra::Vector3
に入れられ、そこでCopy
が要求されているので失敗することになる。つまり、このコードは壊れているということだ!」
そんなもん金輪際入れねえよ、という心の声は飲み込んで、std::marker::Copy
をトレイト境界に追加してやらねばならない。確かに、人は最初の設計を忘れる。いずれ、当初は想像もしていなかった謎の何かを型パラメータにぶち込むだろう。それは使い方を知らない他人かもしれないし、未来の自分かもしれない。そうなってからでは遅い、というのがRustの言い分だ。今のうちに考えられるバグの芽は全て潰しておくのがRust流ということだ。
確かにC++だと、あとで奇妙な型を入れた時に、型パラメータの代入途中で出たあらゆるエラーが報告され、エラーメッセージが膨れ上がって大変なことになる。Rustはそういうことが起きる前に先にエラーメッセージを大変なことにしておいてくれるというわけだ。Rustの方がエラーがわかりやすい分(トレイト境界について知っていて、いくつかの頻出トレイトについての知識があり、落ち着いて考えてあるいは経験から上のような話に考えが至ればだが)多少ましという意見は間違いなくある。が、まあとりあえず動くという状態に持っていきにくいので学習に鉄の意志が必要なのも確かだ。
ちなみにこのコードでは、Copy
トレイトを追加しても指摘は終わらない。最終的に、nalgebra::Vector3
が要求するトレイト境界(nalgebra::base::Scalar
)をそのまま持ってくる必要があった。ライブラリで定義されているジェネリクス構造体を中に持つジェネリクス構造体を作る時は、先にライブラリ側のトレイトを確認するのが一番の近道っぽい。