Charaken 技術系ブログ

技術系に関して学んだことや、気になったことを記事にしていく。

【Rust】がばがばRust独学 - 11. Tests - 1 How to Write Tests

f:id:charaken:20191223210559p:plain

Rustの公式ドキュメントを通して、Rustを独学していく記事になります。(がば要素あり)

doc.rust-lang.org

今回は、Testについて学びつつ、気になった点をコード確認した結果を記事にしています。

  • versions
$ rustc --version
rustc 1.40.0 (73528e339 2019-12-16)


How to Write Tests

公式ドキュメント(How to Write Tests - The Rust Programming Language)によると、テストは3つのアクションを持っているそうです。

  1. Set up any needed data or state.
  2. Run the code you want to test.
  3. Assert the results are what you expect.

必要なデータ・状態を設定し、テストコードを実行し、結果が期待通りで有ることをアサートします。

テスト関数の構造

テスト関数を実施するために、とりあえず新しいライブラリプロジェクトを作成しました。

github.com

$ cargo new demo-test --lib
$ demo-test

テストコードについては、src/lib.rstraining-rust/lib.rs at master · KentaHara/training-rust · GitHub)を参照ください。

まず、初期で記載されているテストを実行すると、正常動作が確認できました。

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        assert_eq!(2 + 2, 4);
    }
}
running 1 test
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

別名関数を作成して実行すると、各々 tests::xxxxx としてチェックされていることが確認できました。

    #[test]
    fn exploration() {
        assert_eq!(2 + 2, 4);
    }
running 2 tests
test tests::exploration ... ok
test tests::it_works ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

panicを起こすと FAILED となり、異常検知が出来ていることが確認できます。

    #[test]
    fn another() {
        panic!("Make this test fail");
    }
running 3 tests
test tests::exploration ... ok
test tests::it_works ... ok
test tests::another ... FAILED

failures:

---- tests::another stdout ----
thread 'tests::another' panicked at 'Make this test fail', src/lib.rs:24:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.


failures:
    tests::another

test result: FAILED. 2 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
assert! マクロ利用によるテスト結果確認

デモコード(training-rust/lib.rs at master · KentaHara/training-rust · GitHub)のように、 Rectangle の構造体および can_hold 関数を定義して、 assert! マクロによるチェックを実施しました。 assert! マクロは bool によるチェックになります。

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

// ...

#[cfg(test)]
mod tests {
    use super::*;

    // ...

    #[test]
    fn larger_can_hold_smaller() {
        let larger = Rectangle {
            width: 8,
            height: 7,
        };
        let smaller = Rectangle {
            width: 5,
            height: 1,
        };

        assert!(larger.can_hold(&smaller));
    }
    #[test]
    fn smaller_cannot_hold_larger() {
        let larger = Rectangle {
            width: 8,
            height: 7,
        };
        let smaller = Rectangle {
            width: 5,
            height: 1,
        };

        assert!(!smaller.can_hold(&larger));
    }
}
running 5 tests
...
test tests::larger_can_hold_smaller ... ok
test tests::smaller_cannot_hold_larger ... ok
assert_eq!assert_ne! によるテスト

it_adds_two では 4 == 4it_adds_two_negative では 5 == 4 としてテストを実施しました。

pub fn add_two(a: i32) -> i32 {
    a + 2
}

// ...

#[cfg(test)]
mod tests {
    use super::*;

    // ...

    #[test]
    fn it_adds_two() {
        assert_eq!(4, add_two(2));
    }

    #[test]
    fn it_adds_two_negative() {
        assert_ne!(5, add_two(2));
    }
}
running 7 tests
...
test tests::larger_can_hold_smaller ... ok
test tests::smaller_cannot_hold_larger ... ok
テスト失敗時のメッセージ変更

assert! の第2引数と第3引数に対してフォーマットの利用が可能です。これは便利...

    #[test]
    fn change_error_message() {
        let result = String::from("Hello Rust!");
        assert!(
            result.contains("Hello New World!"),
            "result did not contain name, value was `{}`",
            result
        );
    }
    #[test]
    fn change_error_message_use_eq() {
        let result = String::from("Hello Rust!");
        assert_eq!(
            result,
            String::from("Hello New World!"),
            "result did not contain name, value was `{}`",
            result
        );
    }
    #[test]
    fn change_error_message_use_ne() {
        let result = String::from("Hello Rust!");
        assert_ne!(
            result,
            String::from("Hello Rust!"),
            "result did not contain name, value was `{}`",
            result
        );
    }
running 8 tests
...
test tests::change_error_message ... FAILED
...

failures:

---- tests::change_error_message stdout ----
thread 'tests::change_error_message' panicked at 'result did not contain name, value was `Hello Rust!`', src/lib.rs:98:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.


failures:
    tests::change_error_message

test result: FAILED. 7 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
panicのチェック

シンプルにパニックを起こす関数 simple_panic を定義して確認すると、 should_panic である場合はpanicが見つけられなければ FAILED となる。

pub fn simple_panic(flg: bool) -> bool {
    if flg {
        panic!("Panic!!");
    }
    true
}
#[cfg(test)]
mod tests {
    use super::*;
    // ...

    #[test]
    #[should_panic]
    fn should_panic_test_panic() {
        simple_panic(true);
    }

    #[test]
    #[should_panic]
    fn should_panic_test_nopanic() {
        simple_panic(false);
    }
}
running 12 tests
...
test tests::should_panic_test_nopanic ... FAILED
test tests::should_panic_test_panic ... ok

failures:

...

---- tests::should_panic_test_nopanic stdout ----
note: test did not panic as expected

failures:
    tests::change_error_message
    tests::change_error_message_use_eq
    tests::change_error_message_use_ne
    tests::should_panic_test_nopanic

test result: FAILED. 8 passed; 4 failed; 0 ignored; 0 measured; 0 filtered out

上記の場合は、 note: test did not panic as expected として注意書きがされる。

Result<T,E> の利用

Resultを返り値として返すこともでき、Errの場合にはエラーメッセージを出すことも可能です。

    #[test]
    fn it_works_use_result_ok() -> Result<(), String> {
        Ok(())
    }

    #[test]
    fn it_works_use_result_err() -> Result<(), String> {
        Err(String::from("ERROR MESSAGE"))
    }
---- tests::it_works_use_result_err stdout ----
Error: "ERROR MESSAGE"
thread 'tests::it_works_use_result_err' panicked at 'assertion failed: `(left == right)`
  left: `1`,
 right: `0`: the test returned a termination value with a non-zero status code (1) which indicates a failure', src/libtest/lib.rs:196:5

まとめ

github.com

  • mod には #[cfg(test)] を付与してtestということをconfigとして設定
  • テスト用の関数には #[test] を利用
    • 必ずpanicになるか否かを確認するためには #[should_panic] を利用
  • 各種マクロによりテスト結果への対応可能
    • assert_eq!(a, b)a == b
    • assert_en!(a, b)a != b
    • assert!(x)x == true
  • 各種マクロの引数を利用してテスト失敗時のメッセージを変更可能
  • Result<T,E> による成否のチェックも可能

最後に

シンプルなテストの記述方法で書きやすそうですね。 Result<T,E> を使える点がとてもありがたいです。

次回も引き続き、Testsに関して進めていきます。

【Rust】がばがばRust独学 - 10. Generic Types/Traits/Lifetimes - 3 Lifetimes

f:id:charaken:20191223210559p:plain

Rustの公式ドキュメントを通して、Rustを独学していく記事になります。(がば要素あり)

doc.rust-lang.org

今回も、Generic Typesについて学びつつ、気になった点をコード確認した結果を記事にしています。

  • versions
$ rustc --version
rustc 1.40.0 (73528e339 2019-12-16)


Lifetimes

Rustの特徴的なownershipのLifetimesに関してになります。

Validating References with Lifetimes - The Rust Programming Language

ぶら下がり参照の防止

公式ドキュメントのコードより、下記を実行すると、 {} ブロック内部で x のスコープが外れます。

そのため、x が開放されて参照先を無くした r はエラーとなるべきですので、エラーとして

x dropped here while still borrowed

となります。

fn main() {
    {
        let r;
        {
            let x = 5;
            r = &x;
        }
        println!("r: {}", r);
    }
}
error[E0597]: `x` does not live long enough
 --> src/demo.rs:6:4
  |
6 |             r = &x;
  |             ^^^^^^ borrowed value does not live long enough
7 |         }
  |         - `x` dropped here while still borrowed
8 |         println!("r: {}", r);
  |                           - borrow later used here

実際のスコープは Validating References with Lifetimes - The Rust Programming Language

公式ドキュメントのボローチェッカー(Validating References with Lifetimes - The Rust Programming Language)がとても分かりやすいです。

関数の一般的な寿命

関数も同様に式(ブロック)内部でのみのLifetimeとなります。

そのため、公式ドキュメントのデモコードのように、longest(x: &str, y: &str) -> &str とした場合、longest(string1.as_str(), string2) における string1.as_str() および string2

  • どちらが選択されるか返り値(-> &str)から分からない
  • &x&y の具体的なLifetimeが longest 関数から判断が付かない

であるため、コンパイラからの判断が出来ず、エラーとなります。

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {}", result);
}
fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
error[E0106]: missing lifetime specifier
 --> src/demo.rs:8:33
  |
8 | fn longest(x: &str, y: &str) -> &str {
  |                                 ^ expected lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`

Lifetime Annotation Syntax

参照の有効期間を変更させないために、アノテーション ' を利用することが出来ます。

デモコードより、関数で利用する場合、 longest 関数において、 xystr 全てのライフタイムが同一でなければならない状態とした場合、 xy どちらが来たとしても、コンパイラから -> &str のLifetimeは必ず xy と同様であることが分かるため、Lifetimeによるエラーはなくなります。

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {}", result);
}

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
The longest string is abcd

また、下記のように異なるLifetime( longest(string1.as_str(), string2.as_str()) )を渡そうとした場合、 string1string2 よりの長いLifetimeであり、 result のLifetimeは string2 と同一であるためエラーとはなりません。

fn main() {
    let string1 = String::from("long string is long");
    {
        let string2 = String::from("xyz");
        let result = longest(string1.as_str(), string2.as_str());
        println!("The longest string is {}", result);
    }
}

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
The longest string is long string is long

しかし、下記のように、 result のLifetimeを string1 と同様にした場合、resultstring2 のLifetime外となってしまうパターンがあるため、エラーとなります。

fn main() {
    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str());
    }
    println!("The longest string is {}", result);
}

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
error[E0597]: `string2` does not live long enough
 --> src/demo.rs:6:38
  |
6 |         result = longest(string1.as_str(), string2.as_str());
  |                                            ^^^^^^^ borrowed value does not live long enough
7 |     }
  |     - `string2` dropped here while still borrowed
8 |     println!("The longest string is {}", result);
  |                                          ------ borrow later used here

Lifetime Elision

全てのリファレンスにはLifetimeが存在しますが、アノテーション無しでコンパイルできるパターンが存在します。

fn main() {
    let s = "Hello Rust!";
    let fw = first_word(&s);
    println!("first word -> {}", fw);
}

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }
    &s[..]
}
first word -> Hello

公式ドキュメント(Validating References with Lifetimes - The Rust Programming Language)で背景がしっかりと記載されていますが、ざっくりまとめると以下になります。

  • Rustの初期バージョン(1.0以前)は対応していなかった
  • 同一パターンでアノテーションしていたので、特定パターンを省略可能にした

    • 省略できるが、完全な推論ではない
  • 関数におけるLifetimeパラメータ名

    • 引数: input lifetime parameter
    • 返り値: output lifetime parameter
  • 3つルールで推論できる場合、省略可能

    • ルール1:input lifetime parameter が独自のLifetimeを持つ
      • 例1:fn foo<'a>(x: &'a i32)
      • 例2:fn foo<'a, 'b>(x: &'a i32, y: &'b i32)
    • ルール2:input lifetime parameter が1つだけの場合は output lifetime parameter は同一Lifetimeになる
      • 例: fn foo<'a>(x: &'a i32) -> &'a i32
    • ルール3:input lifetime parameter が複数の場合はそのうちの一つが &self&mut self 、 関数のどれかである場合

上記のルールより、 first_word(s: &str) -> &str はルール1、2を満たし、ルール3は適応外のため省略可能となります。

また、 impl に対してもルール1、3を満たし、ルール2は適応外のため省略可能となります。

impl<'a> ImportantExcerpt<'a> {
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {}", announcement);
        self.part
    }
}

The Static Lifetime

'static は特別なLifetimeであり、Lifetimeはプログラムの全期間で存続可能です。

let s: &'static str = "I have a static lifetime.";

上記を利用を意識した場合はLifetimeが不要なのか確認する必要があります。長期間保持する必要があるのかどうか・・・

最後に

Lifetimeは省略系を除けば、Ownershipの深堀り的な内容ですね!

charaken.hatenablog.com

Stringusize のLifetimeに気をつけたりすれば、かなり有効的に使えますね。

体に染み込ませるまでに時間がかかりそうではありますが、使いこなせるようになって、Rustのメリットを活かしたいですね。

次はテスト。DevOpsでも重要になってくるテストですね。

【Rust】がばがばRust独学 - 10. Generic Types/Traits/Lifetimes - 2 Traits

f:id:charaken:20191223210559p:plain

Rustの公式ドキュメントを通して、Rustを独学していく記事になります。(がば要素あり)

doc.rust-lang.org

今回も、Generic Typesについて学びつつ、気になった点をコード確認した結果を記事にしています。

  • versions
$ rustc --version
rustc 1.40.0 (73528e339 2019-12-16)


Traits

Traits: Defining Shared Behavior - The Rust Programming Language

型にTraitsの実装

公式ドキュメントのデモコードでは src/lib.rs に対して実装されていましたが、簡略化のために src/main.rs に実装致します。

下記のように、 trait により関数の定義を実施し、 impl により実体の定義を NewsArticleTweet に付与することにより、 summarize を実行することが可能です。

#[derive(Debug)]
struct NewsArticle {
    headline: String,
    location: String,
    author: String,
    content: String,
}

#[derive(Debug)]
struct Tweet {
    username: String,
    content: String,
    reply: bool,
    retweet: bool,
}

trait Summary {
    fn summarize(&self) -> String;
}

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

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

fn main() {
    let article = NewsArticle {
        headline: String::from("headline"),
        location: String::from("location"),
        author: String::from("author"),
        content: String::from("content"),
    };

    let tweet = Tweet {
        username: String::from("username"),
        content: String::from("content"),
        reply: false,
        retweet: true,
    };

    println!("article -> {:?}", article);
    println!("tweet -> {:?}", tweet);

    println!("article summary -> {}", article.summarize());
    println!("tweet summary -> {}", tweet.summarize());
}
article -> NewsArticle { headline: "headline", location: "location", author: "author", content: "content" }
tweet -> Tweet { username: "username", content: "content", reply: false, retweet: true }
article summary -> headline, by author (location)
tweet summary -> username: content

trait の初期実装

下記のように trait Summary に対して初期実装をし、impl Summary for NewsArticle することで、利用可能になります。

#[derive(Debug)]
struct NewsArticle {
    headline: String,
    location: String,
    author: String,
    content: String,
}

trait Summary {
    fn summarize(&self) -> String {
        String::from("(Read more...)")
    }
}

impl Summary for NewsArticle {}

fn main() {
    let article = NewsArticle {
        headline: String::from("headline"),
        location: String::from("location"),
        author: String::from("author"),
        content: String::from("content"),
    };

    println!("article -> {:?}", article);

    println!("article summary -> {}", article.summarize());
}
article -> NewsArticle { headline: "headline", location: "location", author: "author", content: "content" }
article summary -> (Read more...)

また、初期実装については、 impl Summary for NewsArticle により上書きが可能です。

#[derive(Debug)]
struct NewsArticle {
    headline: String,
    location: String,
    author: String,
    content: String,
}

trait Summary {
    fn summarize(&self) -> String {
        String::from("(Read more...)")
    }
}

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

fn main() {
    let article = NewsArticle {
        headline: String::from("headline"),
        location: String::from("location"),
        author: String::from("author"),
        content: String::from("content"),
    };

    println!("article -> {:?}", article);

    println!("article summary -> {}", article.summarize());
}
article -> NewsArticle { headline: "headline", location: "location", author: "author", content: "content" }
article summary -> headline, by author (location)

上書きや初期実装は、一部だけ上書き・初期実装をすることが可能です。

#[derive(Debug)]
struct NewsArticle {
    headline: String,
    location: String,
    author: String,
    content: String,
}

trait Summary {
    fn summarize_author(&self) -> String;

    fn summarize(&self) -> String {
        String::from("(Read more...)")
    }
}

impl Summary for NewsArticle {
    fn summarize_author(&self) -> String {
        format!("@{}", self.author)
    }
}

fn main() {
    let article = NewsArticle {
        headline: String::from("headline"),
        location: String::from("location"),
        author: String::from("author"),
        content: String::from("content"),
    };

    println!("article -> {:?}", article);

    println!("article summary -> {}", article.summarize());
    println!("article author -> {}", article.summarize_author());
}
article -> NewsArticle { headline: "headline", location: "location", author: "author", content: "content" }
article summary -> (Read more...)
article author -> @author

パラメータとしての特性

Summary を利用して、 fn notify(item: impl Summary)item を定義することにより、利用可能です。

#[derive(Debug)]
struct NewsArticle {
    headline: String,
    location: String,
    author: String,
    content: String,
}

trait Summary {
    fn summarize(&self) -> String {
        String::from("(Read more...)")
    }
}

impl Summary for NewsArticle {}

fn notify(item: impl Summary) {
    println!("Breaking news! {}", item.summarize());
}

fn main() {
    let article = NewsArticle {
        headline: String::from("headline"),
        location: String::from("location"),
        author: String::from("author"),
        content: String::from("content"),
    };

    println!("article -> {:?}", article);
    notify(article);
}
article -> NewsArticle { headline: "headline", location: "location", author: "author", content: "content" }
Breaking news! (Read more...)

Generic型による利用

fn notify<T: Summary>(item: T) とすることにより、 Summary のTraitを利用することが可能になります。

#[derive(Debug)]
struct NewsArticle {
    headline: String,
    location: String,
    author: String,
    content: String,
}

trait Summary {
    fn summarize(&self) -> String {
        String::from("(Read more...)")
    }
}

impl Summary for NewsArticle {}

fn notify<T: Summary>(item: T) {
    println!("Breaking news! {}", item.summarize());
}

fn main() {
    let article = NewsArticle {
        headline: String::from("headline"),
        location: String::from("location"),
        author: String::from("author"),
        content: String::from("content"),
    };

    println!("article -> {:?}", article);
    notify(article);
}
article -> NewsArticle { headline: "headline", location: "location", author: "author", content: "content" }
Breaking news! (Read more...)

上記は単一引数であるためメリットが薄いですが、例えば

pub fn notify(item1: impl Summary, item2: impl Summary) {

とするよりも、

pub fn notify<T: Summary>(item1: T, item2: T) {

として、両方のパラメータを縛ることが可能になり、見通しも良くなります。

+ による複数の特性境界の指定

前回記事で指定していたように、 largest<T: PartialOrd + Copy> により複数の特性境界をもたせることが可能です。

where による特性境界の指定

例えば、前回記事で作成した largest 関数を2つ同時に求めるような largests を作成したとします。(実装的に実運用的ではないですが)

fn main() {
    let list1: Vec<usize> = vec![1, 2, 3, 99, 5];
    let list2: Vec<char> = vec!['a', 'b', 'z', 'd'];

    let result = largests(&list1, &list2);
    println!("largests -> {:#?}", result);
}

fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
    let mut largest: T = list[0];
    for &number in list.iter() {
        if number > largest {
            largest = number;
        }
    }
    largest
}

fn largests<T: PartialOrd + Copy, U: PartialOrd + Copy>(list1: &[T], list2: &[U]) -> (T, U) {
    let largest1 = largest(list1);
    let largest2 = largest(list2);
    (largest1, largest2)
}
largests -> (
    99,
    'z',
)

この時、下記の見通しがとても悪くなります。

fn largests<T: PartialOrd + Copy, U: PartialOrd + Copy>(list1: &[T], list2: &[U]) -> (T, U)

そのため、 where を利用することにより、

fn largests<T, U>(list1: &[T], list2: &[U]) -> (T, U)
where
    T: PartialOrd + Copy,
    U: PartialOrd + Copy,
{

とすることができ、見通しが良くなります。

fn main() {
    let list1: Vec<usize> = vec![1, 2, 3, 99, 5];
    let list2: Vec<char> = vec!['a', 'b', 'z', 'd'];

    let result = largests(&list1, &list2);
    println!("largests -> {:#?}", result);
}

fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
    let mut largest: T = list[0];
    for &number in list.iter() {
        if number > largest {
            largest = number;
        }
    }
    largest
}

fn largests<T, U>(list1: &[T], list2: &[U]) -> (T, U)
where
    T: PartialOrd + Copy,
    U: PartialOrd + Copy,
{
    let largest1 = largest(list1);
    let largest2 = largest(list2);
    (largest1, largest2)
}
largests -> (
    99,
    'z',
)

impl による返り値の方指定

fn return_summary() -> impl Summary とすることにより、 NewsArticle を返すことが可能です。

#[derive(Debug)]
struct NewsArticle {
    headline: String,
    location: String,
    author: String,
    content: String,
}

trait Summary {
    fn summarize(&self) -> String;
}

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

fn main() {
    let article = return_summary();

    // `impl Summary` cannot be formatted using `{:?}` because it doesn't implement `std::fmt::Debug`
    // println!("article -> {:?}", article);

    println!("article summary -> {}", article.summarize());
}

fn return_summary() -> impl Summary {
    NewsArticle {
        headline: String::from("headline"),
        location: String::from("location"),
        author: String::from("author"),
        content: String::from("content"),
    }
}

ただし、 impl Summary は単一型を返すことしか許容されて院内ため、 Tweet を返すことはできません。異なる型を返す場合は、公式ドキュメントのChapter 17に記されているそうです。

#[derive(Debug)]
struct NewsArticle {
    headline: String,
    location: String,
    author: String,
    content: String,
}

#[derive(Debug)]
struct Tweet {
    username: String,
    content: String,
    reply: bool,
    retweet: bool,
}

trait Summary {
    fn summarize(&self) -> String;
}

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

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

fn main() {
    let article = return_summary(true);
    let tweet = return_summary(false);
    println!("article summary -> {}", article.summarize());
    println!("tweet summary -> {}", tweet.summarize());
}

fn return_summary(switch: bool) -> impl Summary {
    if switch {
        NewsArticle {
            headline: String::from("headline"),
            location: String::from("location"),
            author: String::from("author"),
            content: String::from("content"),
        }
    } else {
        Tweet {
            username: String::from("username"),
            content: String::from("content"),
            reply: false,
            retweet: true,
        }
    }
}
error[E0308]: if and else have incompatible types
  --> src/demo.rs:60:3
   |
52 |         if switch {
   |    _____-
53 |   |         NewsArticle {
   |  _|_________-
54 | | |             headline: String::from("headline"),
55 | | |             location: String::from("location"),
56 | | |             author: String::from("author"),
57 | | |             content: String::from("content"),
58 | | |         }
   | |_|_________- expected because of this
59 |   |     } else {
60 |   |         Tweet {
   |  _|_________^
61 | | |             username: String::from("username"),
62 | | |             content: String::from("content"),
63 | | |             reply: false,
64 | | |             retweet: true,
65 | | |         }
   | |_|_________^ expected struct `NewsArticle`, found struct `Tweet`
66 |   |     }
   |   |_____- if and else have incompatible types
   |
   = note: expected type `NewsArticle`
              found type `Tweet`

impl への特性境界の利用

公式のデモコードを利用して、 impl<T: Display + PartialOrd> Pair<T> のように特性境界を与えることが可能です。

use std::fmt::Display;

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

impl<T> Pair<T> {
    fn new(x: T, y: T) -> Self {
        Self { x, y }
    }
}

impl<T: Display + PartialOrd> Pair<T> {
    fn cmp_display(&self) {
        if self.x >= self.y {
            println!("The largest member is x = {}", self.x);
        } else {
            println!("The largest member is y = {}", self.y);
        }
    }
}

fn main() {
    let p1 = Pair { x: 1, y: 0 };
    let p2 = Pair { x: 0, y: 1 };

    p1.cmp_display();
    p2.cmp_display();
}
The largest member is x = 1
The largest member is y = 1

最後に

trait の定義を見ていきました。

公式ドキュメントにも記載されているように、interfacesとして考えると飲み込みやすいですね。

Note: Traits are similar to a feature often called interfaces in other languages, although with some differences.

また、 impl A for BB に対して A による実装ができるのは本当にありがたいです。まれにですが、特定の型に対して関数をつけたい場合があるので。

CloneCopy などがどのように実装されているか、traitimpl が分かればかなり読めるものになりますね。 charaken.hatenablog.com

次回は引き続き、Generic Typesで、Lifetimesになります。