対象読者
- C言語のザックリとして文法くらいはわかる
- オブジェクト指向な言語を何か触ったことがある
例
fn map<F, T, A>(option: Option<T>, f: F) -> Option<A> where F: FnOnce(T) -> A {
match option {
None => None,
Some(value) => Some(f(value)),
}
}
Rustは難しい言語と言われます。GoやJavaは他のオブジェクト指向をやってくればすんなりと理解できるのに対し、Rustはモダンな言語仕様が多く、所有権などの特有の概念があるため難しいと言われています。このコードをみて最初全く理解できなかったので言語化します。
この関数はOption型のメソッドとして定義されています。
ジェネリクス
下に書いたのはジェネリクスパラメーターをを受け取る関数です。
fn takes_anything<T>(x: T) {
// xで何か行う
}
<T> は「この関数は1つの型、 T に対してジェネリックである」ということであり、 x: T は「xは T 型である」という意味です。Tにはi64をいれてもいいし、stringをいれてもいいということです。
fn takes_two_things<T, U>(x: T, y: U) {
// ...
}
もちろん二つ取ることもできます。二つ取ると上の例ではTにstring、Uにi64と別の型を代入できるようになります。
ここで例に戻ると、
fn map<F, T, A>(option: Option<T>, f: F) -> Option<A> where F: FnOnce(T) -> A {
match option {
None => None,
Some(value) => Some(f(value)),
}
}
<F, T, A>はジェネリックパラメータであることがわかります。
トレイト
さて、ジェネリックパラメータがどんな型でも受け入れるなら区別する必要なくないですか?実はどんな型でも受け入れてしまうと、足し算をする関数に文字列を渡されるようなときに困ります。そこで、トレイト境界を用いて受け入れる型を制限します。その前にトレイトについて説明しましょう。まずは次を見てください。
struct Circle {
x: f64,
y: f64,
radius: f64,
}
impl Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * (self.radius * self.radius) // ;がないときはreturn文と同じ意味になります
}
}
implはオブジェクト指向言語のメソッドやメンバ関数に近い役割を持っています。そして、ここにトレイトを加えてみましょう
struct Circle {
x: f64,
y: f64,
radius: f64,
}
trait HasArea {
fn area(&self) -> f64;
}
impl HasArea for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * (self.radius * self.radius)
}
}
HasAreaは指定された型が面積を持っているということです。二次元図形であれば面積を持っているので、次のように
struct Square {
x: f64,
y: f64,
side: f64,
}
impl HasArea for Square {
fn area(&self) -> f64 {
self.side * self.side
}
}
正方形にも同じような実装を施すことができます。ここで、二つの図形の面積を足し合わせる関数を作ってみましょう。
fn add_two_figures<T, F>(shape1: T, shape2: F)-> f64{
shape1.area() + shape2.area()
}
しかしこれでは怒られます
error[E0599]: no method named `area` found for type `T` in the current scope
--> main.rs:30:12
|
30 | shape1.area() + shape2.area()
| ^^^^
|
Tには数値でも文字列でもなんでも受け取ってしまうので、数値を代入されるとお手上げです。そこで、トレイト境界を追加します。
fn add_two_figures<T : HasArea, F : HasArea>(shape1: T, shape2: F)-> f64{
shape1.area() + shape2.area()
}
こうすれば。HasAreaトレイトを持つCircleとSquareしか受け入れられないので、正しくコンパイルされます。さて、ここで今までのソースコードをまとめた例を載せましょう
struct Circle {
x: f64,
y: f64,
radius: f64,
}
trait HasArea {
fn area(&self) -> f64;
}
impl HasArea for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * (self.radius * self.radius)
}
}
struct Square {
x: f64,
y: f64,
side: f64,
}
impl HasArea for Square {
fn area(&self) -> f64 {
self.side * self.side
}
}
fn add_two_figures<T : HasArea, F : HasArea>(shape1: T, shape2: F)-> f64{
shape1.area() + shape2.area()
}
fn main(){
let circle = Circle{x: 1.0, y: 1.0, radius: 1.0};
let square = Square{x: 1.0, y: 1.0, side: 1.0};
println!("二つの図形の面積の和は{}", add_two_figures(circle ,square ));
}
// RUSULT
// 二つの図形の面積の和は4.141592653589793
当たり前ですが、面積を持たない型を代入するとコンパイルエラーになります。
add_two_figures(5, 10);
// ERROR
// error: the trait `HasArea` is not implemented for the type `_` [E0277]
また、このトレイト境界は{の直前にwhere句で表現することができます。二つの関数は同じ意味です。
fn add_two_figures<T : HasArea, F : HasArea>(shape1: T, shape2: F)-> f64{
shape1.area() + shape2.area()
}
fn add_two_figures<T, F>(shape1: T, shape2: F)-> f64 where T: HasArea, F: HasArea {
shape1.area() + shape2.area()
}
ここで例に戻りましょう
fn map<F, T, A>(option: Option<T>, f: F) -> Option<A> where F: FnOnce(T) -> A {
match option {
None => None,
Some(value) => Some(f(value)),
}
}
どうもジェネリックパラメーターのFとAに関しては、「FnOnce(T) -> A」という制約がついていそうですね。
次は「FnOnce(T) -> A」周りの理解を深めるため、クロージャについての説明を行います