Rustのenum型をもっと便利に!strumクレートの紹介
さまざまな場面で活躍する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
を定義してくれるのがEnumDiscriminants
deriveマクロ。
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!
Discussion