コンテンツにスキップ

Rustの高度なトレイト

トレイトのページでは、トレイトを定義して実装する方法を学びました。ジェネリクスやよく使うトレイトのページでは、Display、From、Add などのトレイトも使いました。普段のコードなら、これでほとんど困りません。でも標準ライブラリや人気のクレートを読むと、最初は少し不思議に見えるトレイトの機能に出会います。type Item、Add<Rhs = Self>、trait Pretty: Display、impl<T: Display> ... for T といったものです。

このページでは、それらを一つずつ説明します。どれも魔法ではありません。それぞれが小さな、現実的な問題を解決するためのもので、問題がわかれば構文も自然に理解できます。急がなくて大丈夫です。一つの節を読んでコードを試し、また後で戻ってくるのでもかまいません。

関連型にはもう出会っています。Iterator の type Item です。関連型(associated type)は、トレイトの中に置く型のプレースホルダーです。それぞれの実装が、一度だけそれを埋めます。

小さな Container トレイトを見てみましょう。プレイリストは String を、スコアのリストは u32 を持つので、それぞれが自分の Item を選びます。

use std::fmt::Debug;
trait Container {
type Item;
fn get(&self, index: usize) -> Option<&Self::Item>;
fn size(&self) -> usize;
fn first(&self) -> Option<&Self::Item> {
self.get(0)
}
}
struct Playlist {
songs: Vec<String>,
}
struct Scores {
values: Vec<u32>,
}
impl Container for Playlist {
type Item = String;
fn get(&self, index: usize) -> Option<&String> {
self.songs.get(index)
}
fn size(&self) -> usize {
self.songs.len()
}
}
impl Container for Scores {
type Item = u32;
fn get(&self, index: usize) -> Option<&u32> {
self.values.get(index)
}
fn size(&self) -> usize {
self.values.len()
}
}
// 要素を {:?} で表示できるContainerなら何でも受け取れる
fn describe<C: Container>(c: &C)
where
C::Item: Debug,
{
println!("要素は{}個、最初は {:?}", c.size(), c.first());
}
fn main() {
let playlist = Playlist { songs: vec![String::from("Intro"), String::from("Outro")] };
let scores = Scores { values: vec![90, 72, 85] };
describe(&playlist);
describe(&scores);
}

実行結果:

要素は2個、最初は Some("Intro")
要素は3個、最初は Some(90)
▶ 実際に試してみよう

トレイトの中の Self::Item は「この実装が選んだ型」という意味です。describe の中の C::Item も、型 C について同じ意味です(where の行は境界を書くためのすっきりした場所にすぎません。詳しくはこのページの最後で説明します)。

ジェネリック引数ではだめなの? trait Container<T> と書くこともできます。でもそうすると、一つの型がそれを何度も実装できてしまいます。Container<String> と Container<u32> の両方です。そして呼び出す側は毎回どちらのことか指定しなければなりません。プレイリストにとってそれは意味がありません。プレイリストが持つのは曲、それだけです。

Iterator が type Item を使っているのは、まさにこの理由です。Vec<i32> のイテレータが返すのは i32 だけなので、for x in v.iter() に型注釈は要りません。目安はシンプルです。

  • 型ごとに自然な答えが一つ → 関連型(Iterator::Item、Add::Output)。
  • 型ごとに正しい答えがいくつもある → ジェネリック引数(From<T>:String は &str からも char からも Box<str> からも作れます)。

よく使うトレイトのページでは、Add を実装して Point で + が使えるようにしました。標準ライブラリでの Add の本当の定義はこうです。

trait Add<Rhs = Self> {
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
}

ここには二つの機能が出てきます。Output は関連型です(+ の結果の型)。そして Rhs = Self はデフォルト型引数(default type parameter)で、「特に指定しなければ、右辺は左辺と同じ型」という意味です。impl Add for Point と書けばデフォルトが使われます。別の型を足したいときは impl Add<OtherType> for Point と書きます。

use std::ops::Add;
#[derive(Debug, Clone, Copy)]
struct Meters(f64);
#[derive(Debug, Clone, Copy)]
struct Centimeters(f64);
// デフォルトの Rhs = Self を使うので、Meters + Meters になる
impl Add for Meters {
type Output = Meters;
fn add(self, other: Meters) -> Meters {
Meters(self.0 + other.0)
}
}
// デフォルトを上書きする:Meters + Centimeters
impl Add<Centimeters> for Meters {
type Output = Meters;
fn add(self, other: Centimeters) -> Meters {
Meters(self.0 + other.0 / 100.0)
}
}
fn main() {
let a = Meters(2.0);
let b = Meters(3.5);
let c = Centimeters(50.0);
println!("{:?}", a + b);
println!("{:?}", a + c);
}

実行結果:

Meters(5.5)
Meters(2.5)
▶ 実際に試してみよう

Add が両方の道具を使っていることに注目してください。右辺にはジェネリック引数(正しい答えがいくつもある。Meters も Centimeters も足せる)、結果には関連型(組み合わせごとに答えは一つ)です。デフォルト型引数が役に立つのは、主にトレイトを設計するときです。よくあるケースを短く書けるようにしつつ、まれなケースも妨げません。

スーパートレイト:「これも実装してください」

Section titled “スーパートレイト:「これも実装してください」”

自分のトレイトが、型がすでに別のことをできる場合にしか意味を持たないことがあります。スーパートレイト(supertrait)は「このトレイトを実装するには、あのトレイトも実装していなければならない」と宣言するものです。境界と同じようにコロンで書きます。

use std::fmt;
// Pretty を実装する型は Display も実装していなければならない
trait Pretty: fmt::Display {
fn pretty(&self) -> String {
format!("*** {} ***", self)
}
}
struct Temperature {
celsius: f64,
}
impl fmt::Display for Temperature {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:.1}°C", self.celsius)
}
}
impl Pretty for Temperature {}
fn main() {
let t = Temperature { celsius: 21.456 };
println!("{}", t);
println!("{}", t.pretty());
}

実行結果:

21.5°C
*** 21.5°C ***
▶ 実際に試してみよう

デフォルトの pretty メソッドが self に {} を使えるのは、スーパートレイトが Display があることを約束しているからです。: fmt::Display がなければ、型が表示できるかどうかわからないので、Rustはその format! を受け付けません。impl Pretty for Temperature {} は空っぽです。「はい、デフォルトのメソッドを使います」と言っているだけです。

標準ライブラリの実例としては、Copy: Clone(Copy な型はすべて Clone でもなければならない)や Eq: PartialEq があります。

ブランケット実装:一つのimplで多くの型に

Section titled “ブランケット実装:一つのimplで多くの型に”

ブランケット実装(blanket implementation)は、条件を満たすすべての型に対して、一度にトレイトを実装するものです。構文は impl<T: SomeBound> MyTrait for T です。

use std::fmt::Display;
trait Shout {
fn shout(&self) -> String;
}
// Display を実装しているすべての型が、Shout を無料で手に入れる
impl<T: Display> Shout for T {
fn shout(&self) -> String {
format!("{}!", self.to_string().to_uppercase())
}
}
fn main() {
println!("{}", "hello".shout());
println!("{}", 42.shout());
println!("{}", 'a'.shout());
println!("{}", String::from("rust is fun").shout());
}

実行結果:

HELLO!
42!
A!
RUST IS FUN!
▶ 実際に試してみよう

(この例の文字列が英語のままなのは、to_uppercase() で大文字にする様子を見せるためです。日本語には大文字・小文字がありません。)

impl Shout for i32 も impl Shout for char も書いていません。ブランケット実装がそれらをすべてカバーしています。まだ存在しない Display 型まで含めてです。

実は、あなたはもう何度もブランケット実装を使っています。標準ライブラリには(おおまかに言って)impl<T: Display> ToString for T があります。だから Display を持つすべての型に .to_string() メソッドがあるのです。From を実装すると Into が無料でついてくるのも、同じ仕組みです。

一つ制限があります。ブランケット実装があると、それがすでにカバーしている型に対して、もっと具体的な二つ目の実装(たとえば特別な impl Shout for i32)を追加することはできません。答えが二つになってしまうので、Rustは「conflicting implementations(実装の衝突)」エラーで拒否します。

孤児ルールとニュータイプによる回避策

Section titled “孤児ルールとニュータイプによる回避策”

誰もが一度は驚くルールがあります。Vec<String> にきれいな Display をつけようとしてみましょう。

use std::fmt;
impl fmt::Display for Vec<String> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "[{}]", self.join(", "))
}
}
error[E0117]: only traits defined in the current crate can be implemented for types defined outside of the crate
--> src/main.rs:3:1
|
3 | impl fmt::Display for Vec<String> {
| ^^^^^^^^^^^^^^^^^^^^^^-----------
| |
| `Vec` is not defined in the current crate
|
= note: define and implement a trait or new type instead

エラーは「現在のクレートで定義されたトレイトしか、クレートの外で定義された型に実装できない」と言っています。これが孤児ルール(orphan rule)です。型にトレイトを実装できるのは、トレイトか型のどちらかが自分のクレートで定義されているときだけです。Display は標準ライブラリのもので、Vec もそうなので、どちらもあなたのものではありません。

なぜこのルールがあるのでしょうか。依存している二つのクレートが、どちらも独自の impl Display for Vec<String> を書いていたと想像してください。Rustはどちらを使えばいいでしょう? よい答えはありません。だからRustは、そういう状況そのものが起こらないようにしているのです。

回避策はニュータイプパターン(newtype pattern)です。外部の型を、自分の小さな構造体で包みます。こうすればその型は自分のものなので、何でも実装できます。

use std::fmt;
// 外部の型を包む、ローカルなラッパー
struct Shopping(Vec<String>);
impl fmt::Display for Shopping {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "[{}]", self.0.join(", "))
}
}
fn main() {
let list = Shopping(vec![String::from("牛乳"), String::from("卵"), String::from("パン")]);
println!("買うもの: {}", list);
println!("全部で{}個です", list.0.len());
}

実行結果:

買うもの: [牛乳, 卵, パン]
全部で3個です
▶ 実際に試してみよう

self.0 で中の Vec にアクセスします。ニュータイプには実行時のコストがありません。メモリ上では、Shopping はまさに Vec<String> そのものです。欠点は、Vec のメソッドが Shopping に直接はないことです。そのため list.0.len() のように書く(または必要なメソッドを追加する)ことになります。ニュータイプは孤児ルール以外にもいろいろ役に立ちます。デザインパターンも見てみてください。

完全修飾構文:名前がぶつかったとき

Section titled “完全修飾構文:名前がぶつかったとき”

二つのトレイトが同じ名前のメソッドを持つことがあり、型自身も同じ名前のメソッドを持つことがあります。robot.greet() はどれを呼ぶのでしょうか?

trait English {
fn greet(&self) -> String;
fn language() -> String;
}
trait Spanish {
fn greet(&self) -> String;
fn language() -> String;
}
struct Robot;
impl Robot {
fn greet(&self) -> String {
String::from("BEEP BOOP")
}
}
impl English for Robot {
fn greet(&self) -> String { String::from("Hello!") }
fn language() -> String { String::from("English") }
}
impl Spanish for Robot {
fn greet(&self) -> String { String::from("¡Hola!") }
fn language() -> String { String::from("Spanish") }
}
fn main() {
let r = Robot;
println!("{}", r.greet()); // 型自身のメソッドが優先される
println!("{}", English::greet(&r)); // トレイトを名前で選ぶ
println!("{}", Spanish::greet(&r));
// ここには `self` がないので、型とトレイトの両方が必要:
println!("{}", <Robot as English>::language());
println!("{}", <Robot as Spanish>::language());
}

実行結果:

BEEP BOOP
Hello!
¡Hola!
English
Spanish
▶ 実際に試してみよう

(ここでは英語とスペイン語のトレイトの違いを見せたいので、あいさつの文字列はそのままにしています。)

短い書き方から完全に明示的な書き方まで、三段階あります。

  • r.greet():型自身の(固有の)メソッドが最初に選ばれます。
  • English::greet(&r):「English 版」という意味です。型は &r からRustが判断します。
  • <Robot as English>::language():完全修飾構文(fully qualified syntax)です。self のない関数ではこれが必要です。Rustが手がかりにできる値がないからです。English::language() だけだと、error[E0790]: cannot call associated function on trait without specifying the corresponding impl type(対応する実装の型を指定せずにトレイトの関連関数は呼べない)というエラーになります。

これが必要になることはあまりありません。必要なときは、たいていコンパイラのエラーがそのままの修正方法を提案してくれます。

引数と戻り値の impl Trait、そして where

Section titled “引数と戻り値の impl Trait、そして where”

impl Trait は二つの場所に書けて、それぞれ少し意味が違います。

  • 引数の位置:fn f(x: impl Trait) では、呼び出す側が型を選びます。ジェネリックな fn f<T: Trait>(x: T) の短い書き方です。
  • 戻り値の位置:fn f() -> impl Trait では、関数が具体的な型を一つ選んで、それを隠します。呼び出す側にわかるのは「Trait を実装している何か」ということだけです。
// 呼び出す側が選ぶ:u32 のイテレータなら何でもよい
fn print_all(items: impl Iterator<Item = u32>) {
let all: Vec<u32> = items.collect();
println!("{:?}", all);
}
// 関数が選ぶ:本当の型は長い Filter<...> だが、書かずに済む
fn evens_up_to(limit: u32) -> impl Iterator<Item = u32> {
(0..=limit).filter(|n| n % 2 == 0)
}
fn main() {
print_all(vec![3, 1, 4].into_iter());
print_all(evens_up_to(10));
}

実行結果:

[3, 1, 4]
[0, 2, 4, 6, 8, 10]
▶ 実際に試してみよう

戻り値の位置の impl Trait は、クロージャやイテレータと組み合わせると特に便利です。evens_up_to の結果の本当の型にはクロージャの型が含まれますが、クロージャには書ける名前がありません。impl Iterator なら、それを書かずに済みます。

ジェネリック引数がいくつもあって、それぞれに境界がいくつもあると、山かっこの中がごちゃごちゃしてきます。where 節を使うと境界をシグネチャの後ろに移せるので、関数の形が読みやすくなります。

use std::fmt::{Debug, Display};
// ごちゃごちゃ: fn report<T: Display, U: Debug + PartialOrd>(label: T, a: U, b: U) -> String
// すっきり:
fn report<T, U>(label: T, a: U, b: U) -> String
where
T: Display,
U: Debug + PartialOrd,
{
let bigger = if a > b { a } else { b };
format!("{}: 大きいほうは {:?}", label, bigger)
}
fn main() {
println!("{}", report("点数", 7, 12));
println!("{}", report(String::from("名前"), "ann", "bob"));
}

実行結果:

点数: 大きいほうは 12
名前: 大きいほうは "bob"
▶ 実際に試してみよう

コンパイラにとって、どちらの書き方もまったく同じ意味です。where は、このページの最初の例の C::Item: Debug のような、関連型に対する境界を書く場所としても自然です。

トレイトを実装したのに、そのスーパートレイトを忘れる。スーパートレイトの例から impl fmt::Display for Temperature のブロックを消すと、こうなります。

error[E0277]: `Temperature` doesn't implement `std::fmt::Display`
--> src/main.rs:13:17
|
13 | impl Pretty for Temperature {}
| ^^^^^^^^^^^ unsatisfied trait bound
|
note: required by a bound in `Pretty`
--> src/main.rs:3:15
|
3 | trait Pretty: fmt::Display {
| ^^^^^^^^^^^^ required by this bound in `Pretty`

「Temperature は Display を実装していない」というエラーです。直し方は、スーパートレイト(ここでは Display)も実装することです。note が、それを要求している : fmt::Display の部分をちゃんと指してくれています。

-> impl Trait から二つの違う型を返す。impl Trait が隠すのは一つの型です。「あるときはこれ、あるときはあれ」にはできません。

fn numbers(evens: bool) -> impl Iterator<Item = u32> {
if evens {
(0..10).filter(|n| n % 2 == 0)
} else {
vec![1, 3, 5].into_iter()
}
}
error[E0308]: `if` and `else` have incompatible types
--> src/main.rs:5:9
|
2 | / if evens {
3 | | (0..10).filter(|n| n % 2 == 0)
| | ------------------------------
| | | |
| | | the expected closure
| | expected because of this
4 | | } else {
5 | | vec![1, 3, 5].into_iter()
| | ^^^^^^^^^^^^^^^^^^^^^^^^^ expected `Filter<Range<{integer}>, {closure@...}>`, found `IntoIter<{integer}>`
6 | | }
| |_____- `if` and `else` have incompatible types
|
= note: expected struct `Filter<std::ops::Range<{integer}>, {closure@src/main.rs:3:24: 3:27}>`
found struct `std::vec::IntoIter<{integer}>`
help: you could change the return type to be a boxed trait object
|
1 - fn numbers(evens: bool) -> impl Iterator<Item = u32> {
1 + fn numbers(evens: bool) -> Box<dyn Iterator<Item = u32>> {
|
help: if you change the return type to expect trait objects, box the returned expressions
|
3 ~ Box::new((0..10).filter(|n| n % 2 == 0))
4 | } else {
5 ~ Box::new(vec![1, 3, 5].into_iter())
|

「if と else の型が合わない」というエラーです。実行時に本当に型が変わるなら、コンパイラの提案どおり Box<dyn Trait> を返しましょう。トレイトオブジェクトを見てください。

  • 関連型(type Item)は、実装ごとに答えを一つ決めます。一つの型がトレイトを何通りにも実装できるなら、ジェネリック引数を使います。
  • Add<Rhs = Self> のようなデフォルト型引数は、よくあるケースを短く書けるようにします。impl Add<Other> for T で上書きできます。
  • スーパートレイト(trait Pretty: Display)は別のトレイトを要求し、デフォルトメソッドの中でそれを使えるようにします。
  • ブランケット実装(impl<T: Display> MyTrait for T)は、条件に合うすべての型を一度にカバーします。.to_string() もこの仕組みです。
  • 孤児ルールにより、外部のトレイトを外部の型に実装することはできません。型をニュータイプで包めば回避できます。
  • 名前がぶつかったら完全修飾構文 <Type as Trait>::function() を使います。戻り値の位置の impl Trait は具体的な型を一つ隠します。where は長い境界を読みやすくします。

理解度チェック

1. Iterator が Iterator<T> ではなく type Item を使うのはなぜですか?

2. trait Add<Rhs = Self> の Rhs = Self はどういう意味ですか?

3. impl Display for Vec<String> と書いたらRustに拒否されました。よくある直し方はどれですか?

4. self 引数のない関数の English 版を呼ぶのはどの行ですか?

5. fn evens() -> impl Iterator<Item = u32> で、具体的なイテレータの型を決めるのは誰ですか?

スコア: 0 / 5