2
0
お題は不問!Qiita Engineer Festa 2024で記事投稿!
Qiita Engineer Festa20242024年7月17日まで開催中!

Rust 100 Ex 🏃【9/37】 Orphan rule (孤児ルール)・演算子オーバーロード・derive ~Empowerment 💪 ~

Last updated at Posted at 2024-07-17

前の記事

全記事一覧

100 Exercise To Learn Rust 演習第9回になります!

今回の関連ページ

[04_traits/02_orphan_rule] Orphan Rule (孤児ルール)

問題はこちらです。

lib.rs
// TODO: this is an example of an orphan rule violation.
//  We're implementing a foreign trait (`PartialEq`, from `std`) on
//  a foreign type (`u32`, from `std`).
//  Look at the compiler error to get familiar with what it looks like.
//  Then delete the code below and move on to the next exercise.

impl PartialEq for u32 {
    fn eq(&self, _other: &Self) -> bool {
        todo!()
    }
}

"Orphan Rule" (訳すなら孤児ルール...?あるいは Coherence (一貫性) とも呼ばれてる) の制約上、上記のソースコードはコンパイルが通らないことを確認して次に進めとのことらしいです。

解説

"Orphan Rule" は次のようなルールです。

Things get more nuanced when multiple crates are involved. In particular, at least one of the following must be true:

  • The trait is defined in the current crate
  • The implementor type is defined in the current crate

This is known as Rust's orphan rule. Its goal is to make the method resolution process unambiguous.

わかりやすさのために以下の例を考えます。

Rust
struct Hoge;

trait Fuga {}

impl Fuga for Hoge {}

上記のコードにおいて、次のうちどちらかを満たしていなければならない:

  • トレイト( Fuga )が自前のクレートで定義されたものである
  • 構造体( Hoge )が自前のクレートで定義されたものである

上記コードそのままは両方を満たしているため問題ありません。

一方、問題のコードは PartialEq トレイトも u32 型も自分で定義したものではありません。そのためエラーになります。例えるなら、「他人の褌で相撲は取れない」みたいな感覚でしょうか...?

そして以下はOrphan Ruleを回避できている回答例です。既存の型に対して、「ニュータイプパターン」という技法を用いる(単に対象の型をラップしてるだけ)ことで、「構造体を自前で用意した」ことにして制約を回避しています(後のエクササイズで登場してたかな...?)。

lib.rs
#[derive(Clone, Copy, Debug)]
pub struct MyU32(pub u32);

impl PartialEq for MyU32 {
    fn eq(&self, other: &Self) -> bool {
        let res = self.0 == other.0;
        println!("Eq method called. {:?} == {:?} is {}", self, other, res);
        res
    }
}

[04_traits/03_operator_overloading] 演算子オーバーロード

問題はこちらです。

lib.rs
use std::cmp::PartialEq;

struct Ticket {
    title: String,
    description: String,
    status: String,
}

// TODO: Implement the `PartialEq` trait for `Ticket`.

impl PartialEq for Ticket {}
テストを含めた全体
lib.rs
use std::cmp::PartialEq;

struct Ticket {
    title: String,
    description: String,
    status: String,
}

// TODO: Implement the `PartialEq` trait for `Ticket`.

impl PartialEq for Ticket {}

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

    #[test]
    fn test_partial_eq() {
        let title = "title";
        let description = "description";
        let status = "To-Do";
        let ticket1 = Ticket {
            title: title.to_string(),
            description: description.to_string(),
            status: status.to_string(),
        };
        let ticket2 = Ticket {
            title: title.to_string(),
            description: description.to_string(),
            status: status.to_string(),
        };
        assert!(ticket1 == ticket2);
    }

    #[test]
    fn test_description_not_matching() {
        let title = "title";
        let status = "To-Do";
        let ticket1 = Ticket {
            title: title.to_string(),
            description: "description".to_string(),
            status: status.to_string(),
        };
        let ticket2 = Ticket {
            title: title.to_string(),
            description: "description2".to_string(),
            status: status.to_string(),
        };
        assert!(ticket1 != ticket2);
    }

    #[test]
    fn test_title_not_matching() {
        let status = "To-Do";
        let description = "description";
        let ticket1 = Ticket {
            title: "title".to_string(),
            description: description.to_string(),
            status: status.to_string(),
        };
        let ticket2 = Ticket {
            title: "title2".to_string(),
            description: description.to_string(),
            status: status.to_string(),
        };
        assert!(ticket1 != ticket2);
    }

    #[test]
    fn test_status_not_matching() {
        let title = "title";
        let description = "description";
        let ticket1 = Ticket {
            title: title.to_string(),
            description: description.to_string(),
            status: "status".to_string(),
        };
        let ticket2 = Ticket {
            title: title.to_string(),
            description: description.to_string(),
            status: "status2".to_string(),
        };
        assert!(ticket1 != ticket2);
    }
}

PartialEqTicket に実装してほしいという問題です!

解説

PartialEq を型に実装することで、その型において == 演算子による比較が可能になります!(実装されていない場合勝手に参照同士が比較される、なんてヘンテコなことは起きません!) すなわちいわゆる「演算子オーバーロード」は、Rustではトレイトを実装するという形で実現できるわけです。

String 型にはあらかじめ PartialEq が実装されているので、文字列同士を == で比較して、その結果(真偽値)を && で結ぶことで目的は達成されます。

lib.rs
impl PartialEq for Ticket {
    fn eq(&self, other: &Self) -> bool {
        self.title == other.title
            && self.description == other.description
            && self.status == other.status
    }
}

ところで、上記の回答だと、 「全フィールド同士で比較したかどうかわからない」という問題があります。言い換えると、「Ticket 構造体に変更が加わった際、 PartialEq にも改変を加えなければならないのに、そのことに気付けない」という問題があります。 Ticket を改変した時に PartialEq の実装コードの方でも「比較が足りないよ」ということでエラーになってくれると便利です。

こういう時に便利な書き方が、次のエクササイズの冒頭で紹介されている「分解構文」(Destructing syntax)です。

lib.rs
impl PartialEq for Ticket {
    fn eq(&self, other: &Self) -> bool {
        let Ticket {
            title: self_title,
            description: self_description,
            status: self_status,
        } = self;

        let Ticket {
            title: other_title,
            description: other_description,
            status: other_status,
        } = other;

        self_title == other_title
            && self_description == other_description
            && self_status == other_status
    }
}

これは「一意なパターンしか取れない(論駁不可能な)パターンマッチング」とも言える機能で、後ほどのエクササイズ(第13回第14回)にて登場する match 式や if let 式に近い機能なのですが、それはさておき、もし書き出しているフィールドに不足があればコンパイルエラーになってくれたりします。筆者はこの分解構文が結構好きで多用しがちです。(最近乱用気味かも...)

ただ今回は(今回はというよりは、この目的に対しては)冗長な書き方に見えます。そのため本エクササイズの回答ではここまではしていませんでしたし、もっと適切な書き方こそ次のエクササイズで取り組む内容です。

[04_traits/04_derive] deriveマクロ

問題はこちらです。

lib.rs
// TODO: A (derivable) trait implementation is missing for this exercise to compile successfully.
//   Fix it!
//
// # `Debug` primer
//
// `Debug` returns a representation of a Rust type that's suitable for debugging (hence the name).
// `assert_eq!` requires `Ticket` to implement `Debug` because, when the assertion fails, it tries to
// print both sides of the comparison to the terminal.
// If the compared type doesn't implement `Debug`, it doesn't know how to represent them!

#[derive(PartialEq)]
struct Ticket {
    title: String,
    description: String,
    status: String,
}

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

    #[test]
    fn test_partial_eq() {
        // 省略
        // 例えば以下の行でコンパイルエラーとなる
        assert_eq!(ticket1, ticket2);
    }

    // 省略
}
テストを含めた全体
lib.rs
// TODO: A (derivable) trait implementation is missing for this exercise to compile successfully.
//   Fix it!
//
// # `Debug` primer
//
// `Debug` returns a representation of a Rust type that's suitable for debugging (hence the name).
// `assert_eq!` requires `Ticket` to implement `Debug` because, when the assertion fails, it tries to
// print both sides of the comparison to the terminal.
// If the compared type doesn't implement `Debug`, it doesn't know how to represent them!

#[derive(PartialEq)]
struct Ticket {
    title: String,
    description: String,
    status: String,
}

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

    #[test]
    fn test_partial_eq() {
        let title = "title";
        let description = "description";
        let status = "To-Do";
        let ticket1 = Ticket {
            title: title.to_string(),
            description: description.to_string(),
            status: status.to_string(),
        };
        let ticket2 = Ticket {
            title: title.to_string(),
            description: description.to_string(),
            status: status.to_string(),
        };
        assert_eq!(ticket1, ticket2);
    }

    #[test]
    fn test_description_not_matching() {
        let title = "title";
        let status = "To-Do";
        let ticket1 = Ticket {
            title: title.to_string(),
            description: "description".to_string(),
            status: status.to_string(),
        };
        let ticket2 = Ticket {
            title: title.to_string(),
            description: "description2".to_string(),
            status: status.to_string(),
        };
        assert_ne!(ticket1, ticket2);
    }

    #[test]
    fn test_title_not_matching() {
        let status = "To-Do";
        let description = "description";
        let ticket1 = Ticket {
            title: "title".to_string(),
            description: description.to_string(),
            status: status.to_string(),
        };
        let ticket2 = Ticket {
            title: "title2".to_string(),
            description: description.to_string(),
            status: status.to_string(),
        };
        assert_ne!(ticket1, ticket2);
    }

    #[test]
    fn test_status_not_matching() {
        let title = "title";
        let description = "description";
        let ticket1 = Ticket {
            title: title.to_string(),
            description: description.to_string(),
            status: "status".to_string(),
        };
        let ticket2 = Ticket {
            title: title.to_string(),
            description: description.to_string(),
            status: "status2".to_string(),
        };
        assert_ne!(ticket1, ticket2);
    }
}

Bookの序盤の方で「 PartialEq 」をDeriveマクロで実装する話が出ています。これこそが「もっと適した全フィールド比較の書き方」というわけでした。

演習の方では PartialEq は実装済みで、 「 Debug マクロが足りてないから足してくれ」という問題になっています。

解説

今回から本格的にRustの「マクロ」が紹介されましたね。TRPLでは専用のページを割いて紹介されている要素ですが、100 Exercisesではメインで取り上げているのは今回ぐらいで、マクロ自体は簡単な紹介がなされているだけのようです。

まぁそうしているのもわかるというか、実は未だに筆者も「マクロの全部」は良くわかってません。宣言的マクロ( macro_rules! を使って宣言するやつ)は別に難しいものではないので結構な頻度で使っていますが、手続き的マクロは実は定義したことがありません。

それでも、どっちかと言えば「使う」ことが多いから問題ないという感じでしょう。出てくるたびに覚えれば良くて、特に使っているだけのうちは「習うより慣れろ」色が強いのがRustのマクロだと思います。

今回の問題については、 Debug トレイトにはDeriveマクロ(トレイトを実装するのが主な役割となる手続きマクロ)が用意されていることより、単に #[derive(...)]Debug を加えてあげればよいです。

lib.rs
- #[derive(PartialEq)]
+ #[derive(PartialEq, Debug)]
struct Ticket {
    title: String,
    description: String,
    status: String,
}

マクロがどのようにコードとして展開されるかは、 cargo expand というツールを使うことで確認することが可能です。

Debug マクロの該当箇所は次のような感じになっていました。

bash
$ cargo expand
    Checking derives v0.1.0 (/path/to/04_traits/04_derives)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.14s
Rust
// ...省略...

#[automatically_derived]
impl ::core::fmt::Debug for Ticket {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field3_finish(
            f,
            "Ticket",
            "title",
            &self.title,
            "description",
            &self.description,
            "status",
            &&self.status,
        )
    }
}

前回も話しましたが、演算子オーバーロードの例のように、Rustのヘンテコ機能はトレイトで提供されることが多いのです。そしてそのトレイトはDeriveマクロで手軽に構造体に与えられます。

TRPLの冒頭で、「RustはEmpowermentな言語」と紹介されていますが、特に今回のトレイトやDeriveマクロはそれを感じられる要素だったと思います。

では次の問題に行きましょう!

次の記事: 【10】 トレイト境界・文字列・Derefトレイト ~トレイトのアレコレ~

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0