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

Rust 100 Ex 🏃【14/37】 フィールド付き列挙型とOption型 ~チョクワガタ~

Last updated at Posted at 2024-07-17

前の記事

全記事一覧

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

今回の関連ページ

[05_ticket_v2/03_variants_with_data] フィールド持ち列挙子

問題はこちらです。

lib.rs
// TODO: Implement `Ticket::assigned_to`.
//  Return the name of the person assigned to the ticket, if the ticket is in progress.
//  Panic otherwise.

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

#[derive(Debug, PartialEq)]
enum Status {
    ToDo,
    InProgress { assigned_to: String },
    Done,
}

impl Ticket {
    pub fn new(title: String, description: String, status: Status) -> Ticket {
        if title.is_empty() {
            panic!("Title cannot be empty");
        }
        if title.len() > 50 {
            panic!("Title cannot be longer than 50 bytes");
        }
        if description.is_empty() {
            panic!("Description cannot be empty");
        }
        if description.len() > 500 {
            panic!("Description cannot be longer than 500 bytes");
        }

        Ticket {
            title,
            description,
            status,
        }
    }
    pub fn assigned_to(&self) -> &str {
        todo!()
    }
}

Ticket 構造体の status: Status フィールドについて、 InProgress の時には担当者の名前を返し、それ以外の時はパニックするメソッド assigned_to を定義してほしいという問題です!

テストを含めた全体
lib.rs
// TODO: Implement `Ticket::assigned_to`.
//  Return the name of the person assigned to the ticket, if the ticket is in progress.
//  Panic otherwise.

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

#[derive(Debug, PartialEq)]
enum Status {
    ToDo,
    InProgress { assigned_to: String },
    Done,
}

impl Ticket {
    pub fn new(title: String, description: String, status: Status) -> Ticket {
        if title.is_empty() {
            panic!("Title cannot be empty");
        }
        if title.len() > 50 {
            panic!("Title cannot be longer than 50 bytes");
        }
        if description.is_empty() {
            panic!("Description cannot be empty");
        }
        if description.len() > 500 {
            panic!("Description cannot be longer than 500 bytes");
        }

        Ticket {
            title,
            description,
            status,
        }
    }
    pub fn assigned_to(&self) -> &str {
        todo!()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use common::{valid_description, valid_title};

    #[test]
    #[should_panic(expected = "Only `In-Progress` tickets can be assigned to someone")]
    fn test_todo() {
        let ticket = Ticket::new(valid_title(), valid_description(), Status::ToDo);
        ticket.assigned_to();
    }

    #[test]
    #[should_panic(expected = "Only `In-Progress` tickets can be assigned to someone")]
    fn test_done() {
        let ticket = Ticket::new(valid_title(), valid_description(), Status::Done);
        ticket.assigned_to();
    }

    #[test]
    fn test_in_progress() {
        let ticket = Ticket::new(
            valid_title(),
            valid_description(),
            Status::InProgress {
                assigned_to: "Alice".to_string(),
            },
        );
        assert_eq!(ticket.assigned_to(), "Alice");
    }
}

解説

match 式を使って、 Status::InProgress { assigned_to } にマッチするときだけ、 assigned_to に人の名前を束縛して不変参照として返します。

lib.rs
pub fn assigned_to(&self) -> &str {
    match &self.status {
        Status::InProgress { assigned_to } => assigned_to,
        _ => panic!("Only `In-Progress` tickets can be assigned to someone"),
    }
}

今回、新しい要素が2つ出てきました!

  • フィールドを持つ列挙体(!): Rustでは、他のプログラム言語ではあまり例を見ない、「別な値を内包している」列挙子を持つ列挙体を定義できます!
  • パターンマッチで列挙子を分解し、値を変数に束縛する: 第9回等で地味に登場している機能ですが、列挙体がパターンにマッチする時、そのパターンを利用して分解しながらの束縛ができます!
    • 例えばJavaScript等にある分割代入に当たる機能です、モダン!

このようなフィールド持ち列挙子を match 式のパターンマッチで分解する書き方は、今後頻出になります! match 式が本領を発揮してきたというところでしょうか。

継承は不要...?

フィールドを持つ列挙体はかなり強力で、Rustにオブジェクト指向の「継承を用意しなくて良い」理由の一端を担っているのではないかと筆者は考えています。

というのも、まずトレイトがあるのと、トレイトの存在のみでカバーできないシナリオ「似たような挙動をするけど、バリエーションを持たせて、それらの値を例えば同じ動的配列で管理したい()」、という場合でも、2, 3通り思いつく方法のうち1つはフィールド付きEnumで実装可能なためです!

  • 方法1: トレイトオブジェクトを利用する
    • 動的ディスパッチと呼ばれる方法で、詳細は省略します。多分継承がある言語も内部的にはコレ
  • 方法2: Deref トレイトを活用し、参照の方を保持する
    • ニュータイプパターンの応用で擬似的に継承を実装可能ですが、かなり複雑になるので非現実的です。
  • 方法3: まとめたい構造体をフィールドに持つ列挙型を定義し、その列挙型にメソッドを定義する

継承で表現したいものって、トレイトか、実は結構方法3で済むものがあったりするのです!例えばIPアドレスなら以下のような実装例が思いつきます(まだ紹介していない機能を用いていて申し訳ないです)。

Rust
use anyhow::Result;

trait Validate {
    fn validation(&self) -> Result<()>;
}

struct IPv4(String);

impl Validate for IPv4 {
    fn validation(&self) -> Result<()> {
        // ...
        Ok(())
    }
}

struct IPv6(String);

impl Validate for IPv6 {
    fn validation(&self) -> Result<()> {
        // ...
        Ok(())
    }
}

enum IPAddress {
    V4(IPv4),
    V6(IPv6),
}

impl Validate for IPAddress {
    fn validation(&self) -> Result<()> {
        use IPAddress::*;    

        match self {
            V4(adrs) => adrs.validation(),
            V6(adrs) => adrs.validation(),
        }
    }
}

ちなみに標準ライブラリでも列挙体を使っています!(上記を書いてから答え合わせで見ましたが、結構一致してた...): https://doc.rust-lang.org/std/net/enum.IpAddr.html

こんな感じで「バリエーションを表現したい」という時は継承は使わずEnumを使うのがRustっぽい書き方じゃないかなぁと思います。

[05_ticket_v2/04_if_let] if let 式 / let else

問題はこちらです。

lib.rs
enum Shape {
    Circle { radius: f64 },
    Square { border: f64 },
    Rectangle { width: f64, height: f64 },
}

impl Shape {
    // TODO: Implement the `radius` method using
    //  either an `if let` or a `let/else`.
    pub fn radius(&self) -> f64 {
        todo!()
    }
}
テストを含めた全体
lib.rs
enum Shape {
    Circle { radius: f64 },
    Square { border: f64 },
    Rectangle { width: f64, height: f64 },
}

impl Shape {
    // TODO: Implement the `radius` method using
    //  either an `if let` or a `let/else`.
    pub fn radius(&self) -> f64 {
        todo!()
    }
}

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

    #[test]
    fn test_circle() {
        let _ = Shape::Circle { radius: 1.0 }.radius();
    }

    #[test]
    #[should_panic]
    fn test_square() {
        let _ = Shape::Square { border: 1.0 }.radius();
    }

    #[test]
    #[should_panic]
    fn test_rectangle() {
        let _ = Shape::Rectangle {
            width: 1.0,
            height: 2.0,
        }
        .radius();
    }
}

if let 式か、 let else 文を使って、 Shape::Circle の時だけ半径 radius を返し、それ以外はパニックさせてほしいという問題です。

解説

lib.rs
pub fn radius(&self) -> f64 {
    let Shape::Circle { radius } = self else {
        panic!("Not Circle!");
    };

    *radius
}

実は let else 文は筆者が一番好きな構文だったりします。ここで解説するより拙著を読んでほしいです :bow: if let 式の方も解説を入れています。

Rustのlet-else文気持ち良すぎだろ #Rust - Qiita

まぁBookでも言っている通り、 match 式と比べると乱用注意です。ここぞというところで書けるとカッコいいですね!

[05_ticket_v2/05_nullability] Option型 (Rust用NULL)

問題はこちらです。

lib.rs
// TODO: Implement `Ticket::assigned_to` using `Option` as the return type.

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

#[derive(Debug, PartialEq)]
enum Status {
    ToDo,
    InProgress { assigned_to: String },
    Done,
}

impl Ticket {
    pub fn new(title: String, description: String, status: Status) -> Ticket {
        if title.is_empty() {
            panic!("Title cannot be empty");
        }
        if title.len() > 50 {
            panic!("Title cannot be longer than 50 bytes");
        }
        if description.is_empty() {
            panic!("Description cannot be empty");
        }
        if description.len() > 500 {
            panic!("Description cannot be longer than 500 bytes");
        }

        Ticket {
            title,
            description,
            status,
        }
    }
    pub fn assigned_to(&self) -> Option<&String> {
        todo!()
    }
}
テストを含めた全体
lib.rs
// TODO: Implement `Ticket::assigned_to` using `Option` as the return type.

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

#[derive(Debug, PartialEq)]
enum Status {
    ToDo,
    InProgress { assigned_to: String },
    Done,
}

impl Ticket {
    pub fn new(title: String, description: String, status: Status) -> Ticket {
        if title.is_empty() {
            panic!("Title cannot be empty");
        }
        if title.len() > 50 {
            panic!("Title cannot be longer than 50 bytes");
        }
        if description.is_empty() {
            panic!("Description cannot be empty");
        }
        if description.len() > 500 {
            panic!("Description cannot be longer than 500 bytes");
        }

        Ticket {
            title,
            description,
            status,
        }
    }
    pub fn assigned_to(&self) -> Option<&String> {
        todo!()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use common::{valid_description, valid_title};

    #[test]
    fn test_todo() {
        let ticket = Ticket::new(valid_title(), valid_description(), Status::ToDo);
        assert!(ticket.assigned_to().is_none());
    }

    #[test]
    fn test_done() {
        let ticket = Ticket::new(valid_title(), valid_description(), Status::Done);
        assert!(ticket.assigned_to().is_none());
    }

    #[test]
    fn test_in_progress() {
        let ticket = Ticket::new(
            valid_title(),
            valid_description(),
            Status::InProgress {
                assigned_to: "Alice".to_string(),
            },
        );
        assert_eq!(ticket.assigned_to(), Some(&"Alice".to_string()));
    }
}

先程の問題では InProgress ではない際にパニックさせてしまっていましたが、ここでRust用のNULLに相当する None を返すようにしてほしい、という問題です!

解説

パニック部分は None にし、有効な値になる時は Some(...) にすれば良さそうです!

lib.rs
pub fn assigned_to(&self) -> Option<&String> {
    match &self.status {
        Status::InProgress { assigned_to } => Some(assigned_to),
        _ => None,
    }
}

オプショナル型 Option は、Bookにも書いてあるように次のような列挙体です。

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

それだけです。それ以上でもそれ以下でもありません。 Option 型という型であり、内包している型のメソッドを直接呼べなくなるので最初戸惑いますが、 Option 型に用意されているメソッドに慣れてくればめっちゃシンプルであることに気づくでしょう。

フィールド付き列挙体を使うことで値が「あるか」「ないか」を表現しているだけで、NULL なんていう特殊な値でも特殊な型でも無いのです。わかりやすい!

直和型

構造体やタプルを数学用語で直積型、そして今回のようなフィールド付き列挙体を直和型と呼んだりするらしいです。

参考: https://zenn.dev/exyrias/articles/d8b56fc900900b4238a9

数学クラスタの人からマサカリが飛んでくるかもしれないですが...この直和型を扱えるという性質はモダンな言語(TSのユニオンとか?)に見られる傾向なんじゃないかなと思います。

似たような機能があったら直和型かも...?言語間で比較してみると面白そうです。

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

次の記事: 【15】 Result型 ~Rust流エラーハンドリング術~

1
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
1
0