NF

地方で働くプログラマ

「並行プログラミング入門」読みすすめ その2

「並行プログラミング入門」読みすすめ その1 - NF
2回目、トレイトを理解する

トレイトとは

何はともあれ公式ドキュメント。Docが充実してて助かります。
doc.rust-jp.rs
 
10.2. トレイト:共通の振る舞いを定義する
理解のために適当に書いてみる。基本文法理解も兼ねてるので、凄い冗長な感じになります。
まずは2つの自作の型(struct)を定義。C++で言う構造体・クラス?

pub struct MyInteger {
    pub i: i32,
}

pub struct MyChar {
    pub c: char,
}

fn main() {
    let int : MyInteger = MyInteger{i:1};
    let chr : MyChar    = MyChar{c:'a'};

    println!("Finish.");
}

 
次に、メソッドrep()を持つ自作のRepetitionトレイトを定義して、前述の2つの型に実装する。
rep()は、引数に渡した数だけフィールドの値を繰り返し出力する(謎の)関数とする。

pub trait Repetition {
    fn rep(&self, n: i32) -> bool;
}

impl Repetition for MyInteger {
    fn rep(&self, n: i32) -> bool {
        if n > 0 {
            for i in 0..n {
                print!("{}", self.i);
            }
        }
        true
    }
}

impl Repetition for MyChar {
    fn rep(&self, n: i32) -> bool {
        if n > 0 {
            for i in 0..n {
                print!("{}", self.c);
            }
        }
        true
    }
}

main関数と実行結果。

fn main() {
    let int : MyInteger = MyInteger{i:1};
    let chr : MyChar    = MyChar{c:'a'};
    println!(" {}", int.rep(3));
    println!(" {}", chr.rep(5));
    println!("Finish.");
}
111 true
aaaaa true
Finish.

取り合えず、実装の仕方は分かりました。
インタフェースを決めて、実装はそれぞれの型に任せるって事ですね。
C++に例えると、基底クラスの無い仮想関数って感じでしょうか。

何がうれしいのか

トレイトがあると何が嬉しいのか?
ジェネリックプログラミングで必要になるらしい。
 

ジェネリックとは

と言う事で、公式のジェネリックの説明を読む。(こっちの方が項番先でしたね…)
10.1. ジェネリックなデータ型
C++で言うテンプレートかな?というか、テンプレートがジェネリックプログラミングという概念を実現するために存在する、という感じでしょうか。
 
サンプルに載ってるlargest_xxx()は配列を受け取って最大要素を返す関数。i32版とchar版がある。

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

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

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

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

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

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

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

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

2つの関数は型が違うだけで同じような処理なので、共通化したい。
ここで、largest()をジェネリックな関数に変更するという事ができる。ただし、下のサンプルだと、コンパイルエラーになる。largestが全ての型に対応してない、というエラーらしい。

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

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

 
余談:C++で書くとこんな感じだと思います(Tの返し方が分からなかったので、関数内で結果をプリントしてます)。C++のテンプレートは、Rustと違い定義だけではエラーにならない。対応できない型で呼び出す実装を書いて、初めてコンパイルエラーになる。この辺がちょっと違うんですね。

#include <iostream>
#include <vector>

template<class T>
void largest(T&& list) {
    using Type = typename std::remove_cv_t<std::remove_reference_t<T>>::value_type;
    Type largest = list[0];
    for(const auto& item : list) {
        if(item > largest) {
            largest = item;
        }
    }
    std::cout << largest << std::endl;
}

int main()
{
    std::vector<int> number_list = {34, 50, 25, 100, 65};
    largest(number_list);
    
    std::vector<char> char_list = {'y', 'm', 'a', 'q'};
    largest(char_list);
}

 

組み込みトレイトをジェネリック関数に適用(?)

本題に戻って、このエラーを解決するためにトレイトの概念を使う。(10.2.に戻る)
具体的には、以下のようにする。説明は後述。

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

    for &item in list {
        if item > largest {
            largest = item;
        }
    }
    largest
}

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

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

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

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

説明。
ジェネリックな引数Tを持つlargest()関数を、特定のトレイトを持つ型だけが使えるように限定することで、エラーにならずに使える。ちなみに、最初に使ったMyIntegerの配列をlargestに渡すと、当然PartialOrd が定義されてないので、コンパイルエラーになる。

   = help: the trait `PartialOrd` is not implemented for `MyInteger`

PartialOrdトレイトは、演算子オーバーロードのために必要らしいが、なんかPartialとかOrdとか数学のアレなようなので理解は後回しにする。このサイト様が参考になりそうでした。
Rust勉強中 - その18 -> 演算子オーバーロード - Qiita
とにかく演算子使うためのおまじないのトレイトで、i32やcharに組み込みで定義されてるので、ユーザが実装しなくて良いみたい。
Copyトレイトは、ローカル変数largestの更新でコピーが発生するので必要。これも同じく組み込みで定義されてる。
 

まとめ

取り合えず、最低限は分かったような気がします。
最初に仮想関数っぽいと書きましたが、Rustもデフォルト実装やオーバライドも可能らしい。サンプルを書き散らして構文が多少身に付いて来た気がします。これ書くだけに結構時間掛かりましたが、手を動かすのが一番ですね…


参考文献