🐈‍⬛

Rustのenum型をもっと便利に!strumクレートの紹介

2023/12/21に公開

さまざまな場面で活躍するRustのenum型ですが、列挙子の配列を取得したり、str型から変換したいといったユースケースが発生することが多々あります。

その際そのenum型へのtrait実装やenumを参照する定数を用意する必要がありますが、ベタに書いてしまうとenumの変更による変更箇所が増加してしまい、変更漏れなどのバグが発生するリスクも生じてしまいます。

この記事ではその手間を減らしてくれる便利マクロ集であるstrumクレートを紹介したいと思います。

題材

僕はコーヒーが好でよく家で焙煎しているのですが、皆さんはいかがでしょうか。

コーヒーにはさまざまな品種が存在します。カフェでよく提供されるのがアラビカ種という品種で、アフリカや中南米を中心に栽培され、風味がよく、スペシャリティコーヒーといった名前でも呼ばれます。一方でインスタントコーヒーや缶コーヒーによく使われるのがロブスタ種という、東南アジアでよく栽培される品種で、風味はアラビカ種に劣りますが、安価でカフェインが強いといった特徴があります。

このアラビカ種とロブスタ種の二種類で流通量の99%を占めますが、このほかにリベリカやエクセルサといった希少種が存在します。

このコーヒーの品種をRustのenumで表してみましょう。

pub enum CoffeeVariety {
    Arabica,
    Robusta { amount: usize },
    Liberica(usize),
    Excelsa,
}

説明の都合上Robustaには構造体を、Libericaにはunitの値を定義しています。

今回はこのenumを題材にstrumが提供する機能を紹介していきたいと思います。

なお、strumのderive機能を利用するため、deriveフィーチャーを有効にする必要があります。

$ cargo add strum --features derive

EnumString

文字列型の値を元にenum型のインスタンスを取得したいケースがあります。例えば"Arabica" → CoffeeVariety::Arabicaといったように。

この場合std::str::FromStrトレイトを実装する必要がありますが、これをderiveしてくれるマクロがEnumStringです。

以下のようにderiveすることで、文字列からの変換が可能になります。

#[derive(Debug, PartialEq, strum::EnumString)]
pub enum CoffeeVariety {
    Arabica,
    Robusta { amount: usize },
    Liberica(usize),
    Excelsa,
}

#[cfg(test)]
mod test {
    use std::str::FromStr;

    use crate::CoffeeVariety;

    #[test]
    fn test_enum_string() {
        let input = "Arabica";
        let actual = CoffeeVariety::from_str(input).unwrap();
        assert_eq!(CoffeeVariety::Arabica, actual);
    }
}

デフォルトでは単純にenum列挙子の名前とのマッチとなりますが、アトリビュートにより別名をつけたり複数文字列を割り当てたりといったことが可能です。

#[derive(Debug, PartialEq, strum::EnumString)]
pub enum CoffeeVariety {
    #[strum(serialize = "arabica")] // 個別に対応する文字列を割り当てる
    Arabica,
    #[strum(serialize = "robusta", serialize = "r")] // 複数の文字列を対応させる
    Robusta {
        amount: usize,
    },
    #[strum(disabled)] // 文字列対応を無効化する
    Liberica(usize),
    Excelsa,
}

この際、値を持つ列挙子は各フィールドDefaultトレイトのメソッドを使って初期化されるため、各フィールドがDefaultトレイトを実装している必要があります。

#[test]
fn test_from_str() {
    let actual = CoffeeVariety::from_str("robusta");
    assert_eq!(Ok(CoffeeVariety::Robusta { amount: 0 }), actual);
}

また、enum自体に#[strum(serialize_all = "case_style")]のアトリビュートをつけることによりデシリアライズする際の文字列ケースを指定することができます。

#[derive(Debug, PartialEq, strum::EnumString)]
#[strum(serialize_all = "camelCase")] // camelCaseでの変換を指定
pub enum CoffeeVariety {
    Arabica,
    Robusta { amount: usize },
    Liberica(usize),
    Excelsa,
}

(使えるケースなどの詳細はこちら。)

Display

逆にenum列挙子を文字列に変換したい場合はDisplayを使います。

#[derive(Debug, PartialEq, strum::Display)]
pub enum CoffeeVariety {
    Arabica,
    Robusta { amount: usize },
    Liberica(usize),
    Excelsa,
}

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

    #[test]
    fn test_display() {
        let actual = CoffeeVariety::Arabica.to_string();
        assert_eq!("Arabica", actual);
        let actual = CoffeeVariety::Robusta { amount: 100 }.to_string();
        assert_eq!("Robusta", actual);
        let actual = CoffeeVariety::Liberica(100).to_string();
        assert_eq!("Liberica", actual);
    }
}

EnumStringと同様にserializeアトリビュートによる各フィールドの変換の指定、および#[strum(serialize_all = "case_style")]によるデフォルトの変換ケースの指定が可能です。

EnumIs

与えられたenum型が期待する列挙子と一致するかどうか調べたい際に使えるのがEnumIsのderive。各列挙子のis_*というメソッドを生やすだけのシンプルなものですが、出番は多そうです。

#[derive(Debug, PartialEq, strum::EnumIs)]
pub enum CoffeeVariety {
    Arabica,
    Robusta { amount: usize },
    Liberica(usize),
    Excelsa,
}

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

    #[test]
    fn test_enum_is() {
        assert!(CoffeeVariety::Arabica.is_arabica());
        assert!(!CoffeeVariety::Arabica.is_robusta());
    }
}

EnumCount

列挙子がいくつあるのかを調べる際に使えるのがEnumCount。こちらもCOUNT定数を定義するだけのシンプルなものですが意外な場面で使うことがあるかもしれません。

#[derive(Debug, PartialEq, strum::EnumCount)]
pub enum CoffeeVariety {
    Arabica,
    Robusta { amount: usize },
    Liberica(usize),
    Excelsa,
}

#[cfg(test)]
mod test {
    use strum::EnumCount;

    use super::CoffeeVariety;

    #[test]
    fn test_enum_count() {
        assert_eq!(CoffeeVariety::COUNT, 4);
    }
}

EnumVariantNames

列挙子の名前の配列が欲しい際に使えるのがEnumVariantNames。マクロを使わないと対応漏れが起こりがちな部分なので非常に便利です。

#[derive(Debug, PartialEq, strum::EnumVariantNames)]
pub enum CoffeeVariety {
    Arabica,
    Robusta { amount: usize },
    Liberica(usize),
    Excelsa,
}

#[cfg(test)]
mod test {
    use strum::VariantNames;

    use super::CoffeeVariety;

    #[test]
    fn test_enum_variant_names() {
        assert_eq!(
            CoffeeVariety::VARIANTS,
            ["Arabica", "Robusta", "Liberica", "Excelsa"]
        );
    }
}

こちらも#[strum(serialize_all = "case_style")]によるケースの指定が可能です。

EnumIter

列挙子をイテレートしたいケースに使えるのがEnumIter。こちらもテストケースであったり、抜け漏れがないかのチェックなど、使える場面は多そうです。

値を持つ列挙子の場合はEnumStringの時のようにDefaultトレイトを使って初期化されるため、各フィールドがDefaultトレイトを実装している必要があります。

#[derive(Debug, PartialEq, strum::EnumIter)]
pub enum CoffeeVariety {
    Arabica,
    Robusta { amount: usize },
    Liberica(usize),
    Excelsa,
}

#[cfg(test)]
mod test {
    use strum::IntoEnumIterator;

    use super::CoffeeVariety;

    #[test]
    fn test_enum_iter() {
        let mut iter = CoffeeVariety::iter();
        assert_eq!(Some(CoffeeVariety::Arabica), iter.next());
        assert_eq!(Some(CoffeeVariety::Robusta { amount: 0 }), iter.next());
        assert_eq!(Some(CoffeeVariety::Liberica(0)), iter.next());
        assert_eq!(Some(CoffeeVariety::Excelsa), iter.next());
        assert_eq!(None, iter.next());
    }
}

EnumDiscriminants

最後に個人的な推しderiveであるEnumDiscriminantsを紹介します。

クエリ時のフィルターのように、「内部のデータモデルとしては色々なフィールドを持つものの、インターフェイスとしてはどの列挙子かだけが分かれば良い」というユースケースはないでしょうか。

例えば以下のようなメソッドを提供したいようなケース。

pub enum CoffeeVarietyDiscriminants {
    Arabica,
    Robusta,
    Liberica,
    Excelsa,
}

fn list_coffee(by_variety: CoffeeVarietyDiscriminants) -> Vec<Coffee> {
    ...
}

この際上記のCoffeeVarietyDiscriminantsを定義してくれるのがEnumDiscriminantsderiveマクロ。

EnumDiscriminantsは元のenum型からのFromも実装しているので、以下のように元のenum型と相互に変換ができます。

#[derive(Debug, PartialEq, strum::EnumDiscriminants)]
pub enum CoffeeVariety {
    Arabica,
    Robusta { amount: usize },
    Liberica(usize),
    Excelsa,
}

#[cfg(test)]
mod test {
    use super::{CoffeeVariety, CoffeeVarietyDiscriminants};

    #[test]
    fn test_enum_discriminants() {
        assert_eq!(
            CoffeeVarietyDiscriminants::Arabica,
            CoffeeVarietyDiscriminants::from(CoffeeVariety::Arabica)
        );
    }
}

また、ここが推しポイントなのですが、#[strum_discriminants(derive(...))]により、生成されたEnumDiscriminantsにこれまで紹介したderiveマクロを適用することができます

例としてEnumStringをderiveさせてみると、

#[derive(Debug, PartialEq, strum::EnumDiscriminants)]
#[strum_discriminants(derive(strum::EnumString))]
pub enum CoffeeVariety {
    Arabica,
    Robusta { amount: usize },
    Liberica(usize),
    Excelsa,
}

#[cfg(test)]
mod test {
    use std::str::FromStr;

    use super::{CoffeeVariety, CoffeeVarietyDiscriminants};

    #[test]
    fn test_enum_discriminants_derive_enum_string() {
        let actual = CoffeeVarietyDiscriminants::from_str("Robusta");
        assert_eq!(Ok(CoffeeVarietyDiscriminants::Robusta), actual);
    }
}

この仕組みにより複雑なstructを持つenum列挙子であっても一つのenum定義でさまざまなユースケースをカバーすることができるようになります。


いかがでしたでしょうか。

enum型に関する「こういうのあったらいいな」という便利マクロが揃っているのではないでしょうか。

strumからは今回紹介しなかったマクロが他にもいくつか用意されています。詳細はドキュメントを参照してください。

Enjoy Rust coding and have happy holidays!

FRAIMテックブログ

Discussion