30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

Rust のトレイトの初歩

トレイトを使うと、任意の振る舞いを抽象化し、それを複数の型に持たせることができる。
この記事では、トレイトの基本的な使い方を見ていく。

Rust のバージョンは1.55.0、Edition は2018で動作確認している。

基本的な書き方

例として、IceCreamEnglishClassという 2 つの構造体を用意した。
共通するフィールドはひとつもないし、それぞれに独立して存在する異なる型だが、トレイトを使うことで「ある同じ振る舞いを持ったグループ」として抽象化できる。

struct IceCream {
    unit_price: f64,
    flavor: String,
}

struct EnglishClass {
    hourly_price: f64,
    hour: f64,
    difficulty_level: String,
}

今回の例では、小計価格を計算する振る舞いを持たせることにする。

まずはトレイトを定義する。

trait Purchasable {
    fn get_subtotal_price(&self) -> f64;
}

トレイトの名前はPurchasableとした。そしてPurchasableトレイトを実装している型はget_subtotal_priceメソッドを持ち、このメソッドが小計価格を計算して返す。
上記を見れば分かるが、get_subtotal_priceについて定義しているのは引数と返り値の型だけで、実装については書いていない。
実装はトレイトを実装する型(今回の場合はIceCreamEnglishClass)が定義する。型さえあっていれば、メソッドの中身は問われない。

早速、IceCreamEnglishClassPurchasableトレイトを実装する。

impl Purchasable for IceCream {
    fn get_subtotal_price(&self) -> f64 {
        self.unit_price
    }
}

impl Purchasable for EnglishClass {
    fn get_subtotal_price(&self) -> f64 {
        &self.hourly_price * &self.hour
    }
}

impl トレイトの名前 for トレイトを実装する型の名前と記述し、そのなかでメソッドの定義を行う。
IceCreamEnglishClassget_subtotal_priceの実装は異なるが、(&self) -> f64という型を満たしているので問題ない。

これで、どちらのインスタンスでもget_subtotal_priceメソッドを使えるようになった。

fn main() {
    let vanilla_ice = IceCream {
        unit_price: 400.0,
        flavor: String::from("vanilla"),
    };
    let some_class = EnglishClass {
        hourly_price: 2000.0,
        hour: 3.0,
        difficulty_level: String::from("hard"),
    };
    println!("{}", vanilla_ice.get_subtotal_price()); // 400
    println!("{}", some_class.get_subtotal_price()); // 6000
}

トレイトを使うことで、「IceCreamEnglishClassget_subtotal_priceという共通の振る舞いを持っている」という意味を持たせることができる。たまたま名前が同じで型も同じのメソッドを持っていたわけではない。

例えば、単にget_subtotal_priceを実装したいだけなら以下のように書くこともできる。

struct IceCream {
    unit_price: f64,
    flavor: String,
}

struct EnglishClass {
    hourly_price: f64,
    hour: f64,
    difficulty_level: String,
}

impl IceCream {
    fn get_subtotal_price(&self) -> f64 {
        self.unit_price
    }
}

impl EnglishClass {
    fn get_subtotal_price(&self) -> f64 {
        &self.hourly_price * &self.hour
    }
}

この場合、同じ名前のメソッドを偶然持っていたというだけで、IceCreamEnglishClassには何の共通性もないし、何かグルーピングされているわけでもない。
トレイトを使うことで初めて、抽象化されたある振る舞い(この例だとget_subtotal_price)を持っている型として意味づけることができる。そしてそのような型のことを、「あるトレイト(この例だとPurchasable)を実装している型」と表現する。

この機能によって、「特定のトレイトを実装している型のみを引数として受け入れる関数」などを記述することができる。これについては後述する。

デフォルト実装とそのオーバーライド

トレイトに、メソッドの実装を書くこともできる。その場合、その実装がデフォルトとなり、トレイトを実装するそれぞれの型にメソッドの実装を書かなくて済むようになる。

例として、税込価格を計算するget_tax_included_priceメソッドを実装してみる。

const DEFAULT_CONSUMPTION_TAX_RATE: f64 = 10.0;

trait Purchasable {
    fn get_subtotal_price(&self) -> f64;
    fn get_tax_included_price(&self) -> f64 {
        let subtotal = &self.get_subtotal_price();
        subtotal / DEFAULT_CONSUMPTION_TAX_RATE + subtotal
    }
}

トレイトにget_tax_included_priceの実装を書いたので、Purchasableを実装している型は、何もしなくてもget_tax_included_priceを使えるようになる。

fn main() {
    let vanilla_ice = IceCream {
        unit_price: 400.0,
        flavor: String::from("vanilla"),
    };
    let some_class = EnglishClass {
        hourly_price: 2000.0,
        hour: 3.0,
        difficulty_level: String::from("hard"),
    };
    println!("{}", vanilla_ice.get_tax_included_price()); // 440
    println!("{}", some_class.get_tax_included_price()); // 6600
}

トレイトを実装している型にメソッドの実装を書くと、デフォルトの実装をオーバーライドできる。

アイスクリームは軽減税率の対象なので、8% をかける必要があったので、以下のように修正する。
こうすると、IceCreamのインスタンスでget_tax_included_priceを実行したときは8%を使って計算するようになる。EnglishClassについてはオーバーライドを行っていないので、デフォルトの実装がそのまま使われる。

struct IceCream {
    unit_price: f64,
    flavor: String,
}

struct EnglishClass {
    hourly_price: f64,
    hour: f64,
    difficulty_level: String,
}

const DEFAULT_CONSUMPTION_TAX_RATE: f64 = 10.0;
const REDUCED_CONSUMPTION_TAX_RATE: f64 = 8.0;

trait Purchasable {
    fn get_subtotal_price(&self) -> f64;
    fn get_tax_included_price(&self) -> f64 {
        let subtotal = &self.get_subtotal_price();
        subtotal / DEFAULT_CONSUMPTION_TAX_RATE + subtotal
    }
}

impl Purchasable for IceCream {
    fn get_subtotal_price(&self) -> f64 {
        self.unit_price
    }
    fn get_tax_included_price(&self) -> f64 {
        let subtotal = &self.get_subtotal_price();
        subtotal * ((100.0 + REDUCED_CONSUMPTION_TAX_RATE) / 100.0)
    }
}

impl Purchasable for EnglishClass {
    fn get_subtotal_price(&self) -> f64 {
        &self.hourly_price * &self.hour
    }
}

fn main() {
    let vanilla_ice = IceCream {
        unit_price: 400.0,
        flavor: String::from("vanilla"),
    };
    let some_class = EnglishClass {
        hourly_price: 2000.0,
        hour: 3.0,
        difficulty_level: String::from("hard"),
    };
    println!("{}", vanilla_ice.get_tax_included_price()); // 432
    println!("{}", some_class.get_tax_included_price()); // 6600
}

特定のトレイトを実装している型のみを引数として受け入れる関数

関数を定義するときにfn 関数名(仮引数: &impl トレイト名)と書くと、指定したトレイトを実装している型のみを引数として受け入れるようになる。

例えば以下のget_tax_amountは、Purchasableトレイトを実装している型ならどんな型でも受け入れる。

fn get_tax_amount(arg: &impl Purchasable) -> f64 {
    let tax_included_price = arg.get_tax_included_price();
    let subtotal = arg.get_subtotal_price();
    tax_included_price - subtotal
}

fn main() {
    let vanilla_ice = IceCream {
        unit_price: 400.0,
        flavor: String::from("vanilla"),
    };
    let some_class = EnglishClass {
        hourly_price: 2000.0,
        hour: 3.0,
        difficulty_level: String::from("hard"),
    };
    println!("{}", get_tax_amount(&vanilla_ice)); // 32
    println!("{}", get_tax_amount(&some_class)); // 600
}

PointPurchasableトレイトを実装していないので、get_tax_amountに渡すとコンパイルエラーになる。

fn main() {
    struct Point {
        x: i32,
        y: i32,
    }
    let p = Point { x: 1, y: 2 };
    println!("{}", get_tax_amount(&p)); // the trait `Purchasable` is not implemented for `main::Point`
}

fn 関数名(仮引数: &(impl トレイト名1 + トレイト名2))と書くと、両方のトレイトを実装している型のみを受け入れる。

以下のshow_purchasable_food_infoに渡す引数はPurchasableFoodの両方を満たしている必要があるため、EnglishClassを渡すとコンパイルエラーになる。

trait Food {
    fn get_flavor(&self) -> &String;
}

impl Food for IceCream {
    fn get_flavor(&self) -> &String {
        &self.flavor
    }
}

fn show_purchasable_food_info(arg: &(impl Purchasable + Food)) {
    println!(
        "This is {}. Flavor is {}. ",
        arg.get_tax_included_price(),
        arg.get_flavor()
    );
}

fn main() {
    let vanilla_ice = IceCream {
        unit_price: 400.0,
        flavor: String::from("vanilla"),
    };
    let some_class = EnglishClass {
        hourly_price: 2000.0,
        hour: 3.0,
        difficulty_level: String::from("hard"),
    };
    show_purchasable_food_info(&vanilla_ice); // This is 432. Flavor is vanilla.
    show_purchasable_food_info(&some_class); // the trait `Food` is not implemented for `EnglishClass`
}

トレイトを使った引数の制約は、ジェネリックを使って表記することもできる。
その場合、get_tax_amountshow_purchasable_food_infoはそれぞれ、以下のようになる。

fn get_tax_amount<T: Purchasable>(arg: &T) -> f64 {
    let tax_included_price = arg.get_tax_included_price();
    let subtotal = arg.get_subtotal_price();
    tax_included_price - subtotal
}

fn show_purchasable_food_info<T: Purchasable + Food>(arg: &T) {
    println!(
        "This is {}. Flavor is {}. ",
        arg.get_tax_included_price(),
        arg.get_flavor()
    );
}

特定のトレイトを実装している型を返す関数

引数ではなく返り値に対してトレイトを指定することもできる。

fn get_vanilla_ice() -> impl Purchasable {
    IceCream {
        unit_price: 400.0,
        flavor: String::from("vanilla"),
    }
}

注意しなければならないのは、上記のget_vanilla_iceの返り値の型はimpl Purchasableであり、IceCreamではない。
そのため、show_purchasable_food_infoに渡すとコンパイルエラーになる。vanilla_iceFoodトレイトを実装していると見做されないため、このような挙動になる。

fn main() {
    let vanilla_ice = get_vanilla_ice(); // vanilla_ice の型は impl Purchasable
    show_purchasable_food_info(&vanilla_ice); // the trait `Food` is not implemented for `impl Purchasable`
}

以下のようにすると返り値がimpl Purchasable + Foodになり、その値はshow_purchasable_food_infoに渡せるようになる。

fn get_vanilla_ice() -> impl Purchasable + Food {
    IceCream {
        unit_price: 400.0,
        flavor: String::from("vanilla"),
    }
}

型引数が特定のトレイトを実装をしているときにのみ、特定のメソッドを使えるようにする

ジェネリックとトレイトを組み合わせることで、より複雑なことを表現できるようになる。

以下のMyBoxは要素をひとつ持つタプル構造体で、describeメソッドを持っている。
Tにはどのような型でも渡すことができる。

struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn describe(&self) {
        println!("MyBox is tuple structs.");
    }
}

fn main() {
    let int = MyBox(1); // MyBox<i32>
    int.describe(); // MyBox is tuple structs.
    let str = MyBox("abc"); // MyBox<&str>
    str.describe(); // MyBox is tuple structs.
    let bool = MyBox(true); // MyBox<bool>
    bool.describe(); // MyBox is tuple structs.
}

要素を二乗するsquareメソッドをMyBoxに実装したい場合、以下のように書くとコンパイルエラーになってしまう。

impl<T> MyBox<T> {
    fn describe(&self) {
        println!("MyBox is tuple structs.");
    }
    fn square(self) -> T {
        self.0 * self.0 // cannot multiply `T` by `T`
    }
}

Tにはどのような型が入るか分からず、乗算できるとは限らない。そのためコンパイルエラーとなる。例えばT&strが入った場合、乗算できない。

Tに入る型が特定のトレイトを実装していたときにのみsquareメソッドを使えるようにすることで、このコンパイルエラーを回避できる。

以下のMyBoxでは、TMulトレイトとCopyトレイトを実装していた場合にのみ、squareメソッドを使えるようにしている。
そのため、問題なくコンパイルできる。describeは引き続き、Tがどんな型であっても使用できる。

struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn describe(&self) {
        println!("MyBox is tuple structs.");
    }
}

use std::ops::Mul;

impl<T: Mul + Copy> MyBox<T> {
    fn square(self) -> <T as std::ops::Mul>::Output {
        self.0 * self.0
    }
}

i32u32f64はいずれもMutトレイトとCopyトレイトを実装しているため、squareメソッドを実行できる。
&strは条件を満たしていないためsquareメソッドを使えず、実行しようとするとコンパイルエラーになる。

fn main() {
    let x = MyBox(1); // MyBox<i32>
    println!("{}", x.square()); // 1
    let x = MyBox(5u32); // MyBox<u32>
    println!("{}", x.square()); // 25
    let x = MyBox(3.0); // MyBox<f64>
    println!("{}", x.square()); // 9
    let x = MyBox("abc"); // MyBox<&str>
    println!("{}", x.square()); // method cannot be called on `MyBox<&str>` due to unsatisfied trait bounds
}

ブランケット実装

以下のコードをコンパイルしようとすると、エラーになる。
MyStructのインスタンスであるabaseメソッドを呼び出そうとしているため。MyStructFooトレイトを実装しているがBaseトレイトは実装していないのだから、当然である。

fn main() {
    struct MyStruct {}

    trait Base {
        fn base(&self) {
            println!("base!");
        }
    }
    trait Foo {
        fn foo(&self) {
            println!("foo!");
        }
    }

    impl Foo for MyStruct {}

    let a = MyStruct {};
    a.foo(); // foo!
    a.base(); // method not found in `main::MyStruct`
}

abaseメソッドを使わせたい場合、MyStructBaseトレイトを実装する必要がある。
難しいことは何もなくimpl Base for MyStruct {}と書けばそれで済むのだが、impl<T: Foo> Base for T {}と書くこともできる。
こうすると、Fooトレイトを実装している全ての型に対してBaseトレイトも実装することになる。このような記法をブランケット実装と呼ぶ。

fn main() {
    struct MyStruct {}

    trait Base {
        fn base(&self) {
            println!("base!");
        }
    }
    trait Foo {
        fn foo(&self) {
            println!("foo!");
        }
    }

    impl Foo for MyStruct {}
    impl<T: Foo> Base for T {}

    let a = MyStruct {};
    a.foo(); // foo!
    a.base(); // base!
}