2017年7月19日水曜日

The Rust Programming Language 2nd 13日目 GenericとTraits

https://doc.rust-lang.org/book/second-edition/
Apache License Version 2.0

Generic Types, Traits, and Lifetimes

同じロジックでも,扱う変数の型が違えばこれまで学んだ関数の定義方法では,型ごとに似たような関数をいくつも書かなければならない.このようなロジックの重複を解消するのがgenericで,変数の型に関係なく関数やstructを定義できる.
genericはすでにChap 6でOption<T>を, Chap. 8でVec<T>HashMap<K, V>を,Chap. 9でResult<T, E>を使った.これを一般化した用法をこの章では学ぶ.
まず,単純に同じロジックだが引数や返り値の型が違う二つの関数を見て,それをgenericによって一本化する.次に単純に一本化できない場合の解決法を学び,最後にlifetimeという,reference同士の関係性を記述するgenericの一種を導入し,リ方法を学ぶ.

Generic Data Types

Using Generic Data Types in Function Definitions

src/main.rs listing 10-4a

fn largest(list: &[i32]) -> i32 {
    let mut largest = list[0];

    for &item in list.iter() {
        if item > largest {
            largest = item;
        }
    }

    largest
}

という関数を考える.これはi32のリストのreferenceを取ってその最大値(i32)を返す関数で,['y', 'm', 'a', 'q']と言うようなリストを引数に取ることはできないので,型だけを(list: &[char]) -> charとしたような関数を更に定義しなければならない.これは明らかに無駄なので,genericによって一本化することを考える.
引数や返り値をgeneric型にするには,そのgeneric型の変数の型名と変数自体の名を関数宣言の際に定義し,関数の内部でその変数名を使ってロジックを書く.変数の型名にはほとんど必ずTを使う.具体的な記法は
fn largest<T>(list: &[T]) -> T {
のようになる.改めてlargestを書き直したのがlisting 10-5aである.しかしこれはコンパイルできない.

src/main.rs listing 10-5a

fn largest<T>(list: &[T]) -> T {
  let mut largest = list[0];

  for &item in list.iter() {
    if item > largest {
      largest = item;
    }
  }
  largest
}

shell

error[E0369]: binary operation `>` cannot be applied to type `T`
  |
5 |         if item > largest {
  |            ^^^^
  |
note: an implementation of `std::cmp::PartialOrd` might be missing for `T`

とエラーが出る.これは任意の型Tに不等号の演算が定義されていないことが原因で,Tの取りうる値をプログラマが制限しなければならない.そのため,標準ライブラリのもつtrait std::cmp::PartialOrdを使うようにする(後述).

Using Generic Data Types in Struct Definitions

structの定義にもgenericを使える.
src/main.rs listing 10-6a

struct Point<T> {
  x: T,
  y: T,
}

fn main() {
  let integer = Point {x: 5, y: 10};
  let float = Point {x: 1.0, y: .40};
}

listing 10-6では,generic型の型名をTしか決めておらず,x, yは1つのinstanceでは必ず同じTの型しか持てない.x, yがそれぞれ異なる型の値を持てるようにするには,generic型の名前を予め二つ用意する.
src/main.rs listing 10-8

struct Point<T, U> {
  x: T,
  y: U,
}

fn main() {
  let both_integer = Point {x: 5, y: 10};
  let both_float = Point {x: 1.0, y: 4.0};
  let integer_and_float = Point{x: 5, y: 4.0};
}

は正常にコンパイルできる.generic型の型名はいくつあってもいいが,コードを読んで把握しづらくなるほど多いようならロジック自体を考え直すべき.

Using Generic Data Types in Enum Definitions

structと同様に,enumもgeneric型をそのvariantsに持てる.Option<T>がこれを行っているのはすでに見た.Rustは変数を取らないときにはgeneric型をvariantに入れないことを許しているから

enum Option<T> {
  Some(T),
  None,
}

というふうにOption<T>を定義できる.また,あるvariantに入る型と別のvariantに入る型が異なっている場合にも

enum Result<T, E> {
  Ok(T),
  Err(E),
}

と記述できる.

Using Generic Data Types in Method Definitions

Chap.5 でやったように,struct やenumにmethodを定義できるが,これにもgenericが使える.例えば
src/main.rs listing 10-9

struct Point<T> {
  x: T,
  y: T,
}

impl<T> Point<T> {
  fn x(&self) -> &T {
    &self.x
  }
}

fn main() {
  let p = Point {x: 5, y: 10};
  println!("p.x = {}", p.x());
}

また例えば
src/main.rs listing10-10

struct Point<T, U> {
  x: T,
  y: U,
}

impl<T, U> Point<T, U> {
  fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
    Point {
      x: self.x,
      y: self.y,
    }
  }
}

fn main () {
  let p1 = Point {x: 5, y: 10.4} ;
  let p2 = Point {x: "Hello", y: 'C'};  
  let p3 = p1.pixup(p2);
}

は正常に実行できる.

Performance of Code using Generics

Rustはコンパイル時にgenericを具体的な型のコードたちに変換するので,genericが実行時のオーバーヘッドになることはない.Rustのこの働きをmonomorphizationという.
たとえば

let integer = Some(5);
let float = Some(5.0);

をコンパイルするとき

enum Option_i32 {
    Some(i32),
    None,
}

enum Option_f64 {
    Some(f64),
    None,
}

fn main() {
    let integer = Option_i32::Some(5);
    let float = Option_f64::Some(5.0);
}

のように内部で変換する.

Traits: Defining Shared Behavior

traitは型の振る舞いを抽象化する.つまり複数の型に同時にメソッドを定義できる.また関数の引数などにgeneric型を使っているときtraitによってその引数が取れる型の範囲を制限して,Using Generic Data Types in Function Definitionsでみたエラーに対処することができる.

Defining a Trait

型のふるまいは型に実装されているメソッドたちによって決まる.異なった型たちが同じ名前のメソッドを持っているとき,その型たちは振る舞いを共有していると考えることが出来る.traitによって複数の型に同じ名前のメソッドを同時に定義できる.例えばNewsArticle型とTweet型を考える.両者ともに,そのインスタンスの要約を返すメソッドsummaryを,summarizable traitによって持たせる.traitはmoduleのように定義するが,body blockにはsignatureだけ書く.
src/lib.rs listing 10-11

pub trait Summarizable {
  fn summary(&self) -> String;  // method signatureのみ書く
  fn author(&self) -> String;   // 複数のメソッドも書ける.
  fn content(&self) -> String;  // 1行に一つのmethod signatureを書き,セミコロンを打つ.
}

Implementing a Trait on a Type

Summarizable traitを定義したところで,型にtraitを実装する.通常のメソッド定義は
impl NewsArticle { fn summary...}と書くが,traitを実装するときは
impl Summarizable for Newsarticle { fn summary signature { }} と書く.fn summary signature { }の中に実際のロジックをコーディングする.

具体的な例:
lib.rs listing 10-12

pub struct NewsArticle {
  pub headline: String,
  pub location: String,
  pub author: String,
  pub content: String,
}

impl Summarizable for NewsArticle {
  fn summary(&self) -> String {
    format!("{}, by {} ({})", self.headline, self.author, self.location)
  }
}

pub struct Tweet {
  pub username: String,
  pub content: String,
  pub reply: bool,
  pub retweet: bool,
}

impl Summarizable for Tweet {
  fn summary(&self) -> String {
    format!("{}: {}", self.username, self.content)
  }
}

こうしてSummarizableNewsArticleTweetに実装できた.それぞれの型のinstanceにドット記法でSummarizableの中のメソッドを呼べる.

let tweet = Tweet {
    username: String::from("horse_ebooks"),
    content: String::from("of course, as you probably already know, people"),
    reply: false,
    retweet: false,
};

println!("1 new tweet: {}", tweet.summary());

1 new tweet: horse_ebooks: of course, as you probably already know, people.を出力する.

これまではすべてをlib.rsに書いてきた.これらをaggregatorというcrateにして,他の場所にあるWeatherForecast structにSummarizable traitを実装したい場合,Summarizableをまずインポートする.
lib.rs listing 10-13 例

extern crate aggregator;

use aggregatro::Summarizable;

struct WeatherForecast {
  high_temp: f64,
  low_temp: f64,
  chance_of_precipitation: f64
}

impl Summarizable for WeatherForecast {
  fn summary(&self) -> String {
    format!("The high will be {}, and the low will be {}. The chance of precipitation is{}%", self. high_temp, self.low_temp, self.chance_of_precipitation)
  }
}

traitとtypeがともにexternalであるとき,そのtypeにtraitを新たに実装することはできない.例えば,Vecはexternal traitでDisplayはexternal traitだから,VecDisplayを実装することはできない.こうしたルールをOrphan ruleという.

Default Implementations

traitを定義するとき,予めロジックを決めておいて,改めて型にtraitのメソッドを実装しない限りそのデフォルトのロジックをその型のメソッドとすることが出来る.Default implementationという.そのためには,listing 10-11ではセミコロンで止めていおいたメソッドのsignatureを,実際のロジックまで書くようにし,
lib.rs listing 10-14

pub trait Summarizable {
  fn summary(&self) -> String {
    String::from("(Read more...)");
  }
}

さらに型への実装で{ }を空白にする. impl Summarizable for NewsArticle {}仮にここでtraitの定義とは別のロジックを書いたら,新しいロジックが優先される.

default implementationはそのtraitの他のメソッドを,デフォルトが定義されていなくても,呼ぶことが出来る.例えば

pub trait Summarizable {
  fn author_summary(&self) -> String;

  fn summary(&self) -> String {
    format!("(Read more from {}...)", self.author_summary())
  }
}

このSummarizableを使うときは,author_summaryを型に実装する.

impl Summarizable for Tweet {
    fn author_summary(&self) -> String {
        format!("@{}", self.username)
    }
}

Trait Bounds

traitをgeneric type parameterと使うことも出来る.generic typeは野放図に使うとUsing Generic Data Types in Function Definitionsのようなエラーを生じることがあるので,そのgeneric typeを取れる型が特定のtraitを実装されている型であると制限して,その制限下のどの型でも動くとコンパイラが判断すれば,コンパイルしてくれる.こうしてgeneric typeの型を制限することを,”generic typeにtrait boundsを指定する”という

例えばlisting 10-12でsummarizableNewsArticleTweetに実装したので,NewsArticleTweetを引数に取るnotifyという関数をgenericを使って定義する.generic type parameter TSummarizable traitが実装されている型に制限するには,定義時に<T: Summarizable>とすれば良い.例えば

pub fn notify<T: Summarizable>(item: T) {
  println!("Breaking news! {}", item.summary())
}

また,SummarizableDisplayを同時に実装している型にgeneric typeを制限したいときには<T: Summarizable + Displayとする.
fn some_function<T: Display + Clone, U: Clone + Debug>(t: T, u: U) -> i32 {
というふうに複数の引数にそれぞれのtrait boundsを設定することが可能だが,読みづらいのでwhereキーワードを使って

fn some_function<T, U>(t: T, u: U) -> i32
  where T: Display + Clone,
        U: Clone + Debug
{

というふうに定義することも可能である.

Fixing the largest Function with Trait Bounds

Using Generic Data Types in Function Definitionsで見たエラーを実際に修正しよう.

error[E0369]: binary operation `>` cannot be applied to type `T`
  |
5 |         if item > largest {
  |            ^^^^
  |
note: an implementation of `std::cmp::PartialOrd` might be missing for `T`

不等号演算子が定義されている型がTに来るかもしれないというのでエラーメッセージが出たので,不等号を定義する標準ライブラリのtrait, std::cmp::PartialOrdをtrait boundとしてみる.
fn largest<T: PartialOrd>(list: &[T]) -> T {
しかし,これでもエラーが出る.

error[E0508]: cannot move out of type `[T]`, a non-copy array
 --> src/main.rs:4:23
  |
4 |     let mut largest = list[0];
  |         -----------   ^^^^^^^ cannot move out of here
  |         |
  |         hint: to prevent move, use `ref largest` or `ref mut largest`

error[E0507]: cannot move out of borrowed content
 --> src/main.rs:6:9
  |
6 |     for &item in list.iter() {
  |         ^----
  |         ||
  |         |hint: to prevent move, use `ref item` or `ref mut item`
  |         cannot move out of borrowed content

cannot move out of type [T], a non-copy array.に着目する.TCopy traitを実装していないため,largest = list[0]が実行できなかったことを示している.よってtrait boundにCopyを加えることで,コンパイルが可能になる.

src/main.rs listing 10-15

use std::cmp::PartialOrd;

fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
    let mut largest = list[0];

    for &item in list.iter() {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn main() {
    let numbers = vec![34, 50, 25, 100, 65];

    let result = largest(&numbers);
    println!("The largest number is {}", result);

    let chars = vec!['y', 'm', 'a', 'q'];

    let result = largest(&chars);
    println!("The largest char is {}", result);
}

Copyをtrait boundに加えたくない場合,かわりにCloneをtrait boundsに加えても良いが,Cloneはheap構造を使うので,性能が落ちる可能性がある.

0 件のコメント:

コメントを投稿