Learning Rust - Traits

Syntax and Semantics

Traits

Traitは、Haskellでいう型クラスであり、 C++でいうコンセプト(C++14時点では提案のみっぽいですが)です。 ジェネリクスに制限をくわえ、あるメソッドをもった型しか認めないように束縛するのが、traitです。 これによって、ジェネリクス関数やstructがうけとる型に、 そのメソッドが実行できることを保証できます。

Trait bounds on generic functions

例えば、面積を表示する、以下のジェネリックな関数を作りたいとします。

fn print_area<T>(shape: T) {
    println!("This shape has an area of {}", shape.area());
}

これはコンパイルエラーになります。 print_area()関数が受け取る型Tに、area()メソッドが定義されていないからです。 現行のC++では、実際にprint_area()関数をある型について使ったとき、 それがarea()メソッドをもたないならエラーになりましたが、 Rustではジェネリクス関数を定義しただけでエラーをおこします。

そこで登場するのがtraitです。 area()メソッドをもつことを保証するtraitをつくり、 型Tを、そのtraitをもつものに制限します。 Syntaxは次のとおりです。

trait HasArea {
    fn area(&self) -> f64;
}

struct Circle {
    x: f64,
    y: f64,
    radius: f64,
}

impl HasArea for Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * (self.radius * self.radius)
    }
}

fn print_area<T: HasArea>(shape: T) {
    println!("This shape has an area of {}", shape.area());
}

HasAreatraitをつくり、それにarea()メソッドを含めます。 print_area()関数がCirclestructに適用するため、 CircleHasAreatraitを実装します。 そしてprint_area()が受け取る型Tを、HasAreatraitをもつという条件で束縛します。

ほかのstrctにHasAreatraitをimplすることもできます。

struct Square {
    x: f64,
    y: f64,
    side: f64,
}

impl HasArea for Square {
    fn area(&self) -> f64 {
        self.side * self.side
    }
}

Trait bounds on generic structs

ジェネリックなstructについても、traitが使えます。

struct Rectangle<T> {
    x: T,
    y: T,
    width: T,
    height: T,
}

impl<T: PartialEq> Rectangle<T> {
    fn is_square(&self) -> bool {
        self.width == self.height
    }
}

let mut r = Rectangle {
    x: 0,
    y: 0,
    width: 47,
    height: 47,
};

PartialEqは標準ライブラリにあるtraitで、 対称律と推移律を満たす関係をもつ型にimplされているようです。 i32はこのtraitをもっているので、Rectangle<i32>がつくれます。 しかし、PartialEqをもっている型に限定しないと、==がつかえないのでエラーになります。

Rules for implementing traits

Traitの作成・使用には、いくつかルールがあります。

まず、他のスコープでつくられたtraitは、明示的にuseしないと使えません。

let mut f = std::fs::File::open("foo.txt").ok().expect("Couldn’t open foo.txt");
let buf = b"whatever"; // byte string literal. buf: &[u8; 8]
let result = f.write(buf);

これはコンパイルエラーです。 f.write()のためには、fWritetraitをもっている必要があります。

use std::io::Write;

と書いておけばOKです。 知らないところで変なtraitが定義されて、それを使ってしまうのを防ぐための仕様です。

もう一つ、Rustのプリミティブな型に、標準で提供されているtraitをimplすることはできません。 例えば、i32ToStringをimplすることはできません (そもそもすでにimplされています)。

Multiple traits bounds

複数のtraitをもっている型に制限したいときは、+で重ねることができます。

use std::fmt::Debug;

fn foo<T: Clone + Debug>(x: T) {
    x.clone();
    println!("{:?}", x);
}

std::fmt::Debugtraitをもっていると、 {:?}というフォーマットですこしくわしい情報が得られるようです。

Where clause

ジェネリックパラメータが増えたり、multiple traits boundsしたりすると、 <>のなかが長くなりがちです。

use std::fmt::Debug;

fn foo<T: Clone, K: Clone + Debug>(x: T, y: K) {
    x.clone();
    y.clone();
    println!("{:?}", y);
}

これくらいだとまだいいですが、これ以上長くなってくると読みにくいですね。 そんなときのためにwhereキーワードがあります。

fn bar<T, K>(x: T, y: K) where T: Clone, K: Clone + Debug {
    x.clone();
    y.clone();
    println!("{:?}", y);
}

where節をかくと、型制約を引数リストのあとに書くことができます。 関数名と引数が近くなるのでわかりやすいですね。 また、改行をいれたりもできるので、それによっても可読性が上がります。

実は、where節でしかかけない型制約があります。 それは、型制約が、型パラメータに直接かかる制約ではなく、 型パラメータへの間接的な制約であるときです。 ことばでいってもわかりにくいので、例を見てみます。

trait ConvertTo<Output> {
    fn convert(&self) -> Output;
}

impl ConvertTo<i64> for i32 {
    fn convert(&self) -> i64 { *self as i64 }
}

// can be called with T == i32
fn normal<T: ConvertTo<i64>>(x: &T) -> i64 {
    x.convert()
}

// can be called with T == i64
fn inverse<T>() -> T
        // this is using ConvertTo as if it were "ConvertTo<i64>"
        where i32: ConvertTo<T> {
    42.convert()
}

normal()関数は、TConvertTo<i64>traitをもっていることが型制約ですが、 inverse()関数は、TConvertTo<T>できるという、間接的な型制約になっています。

Default method

Traitがもつメソッドにデフォルト実装を書くことができます。 そのtraitをimplする型でoverrideしない場合、デフォルト実装がつかわれることになります。

fn main() {
    trait Validation {
        fn is_valid(&self) -> bool { !self.is_invalid() }
        fn is_invalid(&self) -> bool { !self.is_valid() }
    }

    impl Validation for i32 {
        fn is_valid(&self) -> bool {
            *self >= 0
        }
    }

    impl Validation for f64 {
        fn is_invalid(&self) -> bool {
            *self < 0.0
        }
    }

    assert!(1.is_valid());
    assert!((-1).is_invalid());
    assert!((1.1).is_valid());
    assert!((-1.1).is_invalid());
}

この例では、Valiadationtraitのもつメソッドは、お互いの否定をとるような実装になっています。 したがって、i32などにimplするとき、 is_valid()is_invalid()のどちらか一方のみ実装すれば十分です。

Inheritance

あるtraitをimplするとき、他のtraitのメソッドが必要になる場合があります。 AというtraitがBというtraitを継承すると、Aをimplする型はBもimplされていることが要請できます。

fn main() {
    trait Printable : std::fmt::Display {
        fn print(&self) {
            println!("{}", self)
        }
    }

    impl Printable for i32 {}
    1.print();

    struct S;
    impl Printable for S {}
    let s = S;
    s.print();
}

この例では、Printabletraitがstd::fmt::Displayを継承しているので、 println!("{}", self)が実行できることが保証されます。

i32std::fmt::Displayをimplされているので、問題なくPrintableもimplできます。

一方、structSstd::fmt::Displayをimplしていないので、Printableもimplできません。

Deriving

基本的には、traitは型ごとにimplしないと使えませんが、 DebugとかDefaultとかのよく使う標準ライブラリのtraitをいちいちimplするのは面倒ですね。 そこで、#[derive(Debug)]と書くと、自動的にそれがimplされるようになっています。

#[derive(Debug)]
struct Foo;

fn main() {
    println!("{:?}", Foo);
}

ただし、このattributeがつかえるのは、以下のtraitに限られています。

  • Clone
  • Copy
  • Debug
  • Default
  • Eq
  • Hash
  • Ord
  • PartialEq
  • PartialOrd

Drop

最後に、標準ライブラリにある便利なtraitを紹介しておきます。

Droptraitは、C++でいうデストラクタです。 Dropをimplした型のVariable bindingがスコープを抜けて破棄されるとき、 drop()メソッドが呼ばれます。

struct HasDrop;

impl Drop for HasDrop {
    fn drop(&mut self) {
        println!("Dropping!");
    }
}

fn main() {
    let x = HasDrop;

    // do stuff

} // x goes out of scope here

これを実行すると、プログラムが終了するときに、Dropping!と出力されます。

Variable bindingは、宣言と逆順に破棄されていきます。 StackのLIFOということですね。

struct Firework {
    strength: i32,
}

impl Drop for Firework {
    fn drop(&mut self) {
        println!("BOOM times {}!!!", self.strength);
    }
}

fn main() {
    let firecracker = Firework { strength: 1 };
    let tnt = Firework { strength: 100 };
}

tntfirecrackerより後に宣言されているので、出力は逆順の、

BOOM times 100!!!
BOOM times 1!!!

となります。