Rust勉強中。
ジェネリクスとトレイトという概念を学びはしたものの、いまいち使い所が腑に落ちていないので、簡単なサンプルを書いてボトムアップ的に理解してみようという話。
まずは普通のコード
サンプルとして、2つの数字を受け取って同じ値ならtrueを、そうでなければfalseを返す関数を考えてみる。これはもちろん普通に動く。
fn is_equal(a: i32, b: i32) -> bool {
if a == b {
true
} else {
false
}
}
fn main() {
println!("{}", is_equal(1, 1)); // true
}
ジェネリクスを使ってみる
上記では2つの数字を数値(i32)の型で受け取ったが、文字列(&str)として受け取っても比較できるようにしたい。数値だろうが文字列だろうが数字は数字じゃないか、という安易な考え方だ。
こういうときにジェネリクスを使うと、とてもスッキリ書くことができる。
fn is_equal<T: Eq>(a: T, b: T) -> bool {
if a == b {
true
} else {
false
}
}
fn main() {
println!("{}", is_equal(1, 1)); // true
println!("{}", is_equal("1", "1")); // true
}
Eqトレイトを実装している型であれば何でも受け付けますよ、という宣言にすることによって、数値の「1」も文字列の「"1"」も同じ関数で比較できるようになった。Eqトレイトの詳細な定義は公式ドキュメントなどに任せるとして、とりあえずは「同じだったら同じと言えます!」という属性を持っている、くらいに考えればいいのではないだろうか。数値と文字列は、直感的にもそういった属性を持っていそうだ。
Wikipediaによると、こういった書き方を「パラメトリックポリモーフィズム」とか、「ジェネリックプログラミング」とか言ったりもするらしい。
トレイトを使ってみる
ここまでで、数値も文字列も同じ感覚で比較することができた。では、数値と文字列の比較はできないだろうか。
fn main() {
println!("{}", is_equal(1, 1)); // true
println!("{}", is_equal("1", "1")); // true
println!("{}", is_equal(1, "1")); // ★これも比較したい
}
今のis_equalでは、数値も文字列も比較することができるが、それはあくまで「数値としての値」と「文字列としての値」をそれぞれ比較しているに過ぎない。ここでやりたいのは、数値だろうが文字列だろうが、「数字」として比較することだ。どんな型だろうが統一された形式の結果を取り出せる、という独自の属性を数値と文字列に持たせ、それぞれのやり方を定義しよう。ここでは結果をi32に統一する。
トレイトを使うことにより、異なる型に共通の属性を持たせることができる。
trait Calculatable {
// 結果を取り出すことができますよ、という属性。
// この属性を持つには、その属性を持ちたい型がresultという
// メソッドを実装している必要がある。
fn result(self) -> i32;
}
impl Calculatable for i32 {
fn result(self) -> i32 {
self // 数値はそのまま返す
}
}
impl Calculatable for &str {
fn result(self) -> i32 {
self.parse().unwrap() // 文字列は変換して返す
}
}
fn is_equal<T: Calculatable, U: Calculatable>(a: T, b: U) -> bool {
if a.result() == b.result() {
true
} else {
false
}
}
fn main() {
println!("{}", is_equal(1, 1)); // true
println!("{}", is_equal("1", "1")); // true
println!("{}", is_equal(1, "1")); // true
}
これで数値も文字列も「数字」として比較することができるようになった。
最初、関数の宣言はis_equal<T: Calculatable>(a: T, b: T)
という書き方でもいけるかなと思ったが、これはコンパイルエラーになる。トレイトの条件は同じでも、内部ではちゃんと違う型として渡されているようだ。(しかもそれをコンパイラが見つけられている。)
このような書き方は「アドホックポリモーフィズム」とか言ったりするらしい。
注意
ジェネリクスは考え方で、ジェネリクスを「使う」という言い方は正確でないかもしれない。もう少しちゃんと言うと、型パラメータとトレイト境界を設定して関数をジェネリクスにする、とかかなぁ。
トレイトを説明するのに「属性」という言葉を用いたが、「属性」という言葉はRustの他の概念を説明するときに使われたりもするので、トレイトの説明に用いるのはあまりよろしくないかもしれない。あくまでイメージとしてとらえて下さい。
言葉は難しいですね。
まとめ
ジェネリクスとトレイトを使うことにより、値の型によらない直感的な比較を行う関数を作ることができた。
出来上がったmain関数を見ると、なんだかプログラミングを習いたてのときにやってしまいそうな書き方だと思った。特にCとかを書いていると、どうしても型というものを意識するようになるが、Rustでは型を意識する必要のないインターフェースを作ることができる。もちろんそのような関数の裏側を実装するには型に対する理解が不可欠ではあるのだが、流れを理解しやすいコードを書くという観点では、大きな意味をもつのではないだろうか。