📚

Display と FromStr も derive したい!

2023/12/01に公開

この記事はRust Advent Calendar 2023 シリーズ 3 の 1 日目の記事です。

Rust には derive macro という仕組みがあり CopyClone などの基本的なトレイトを自動で実装することができます。
しかし、標準ライブラリには DisplayFromStr を実装する derive macro はなく、手動で実装しなければなりませんでした。

そこで、 DisplayFromStr を簡単に実装できるようにするクレート parse-display を(かなり前に)作ってみました。

どうやって使うの?

まずは例をご覧ください。

Cargo.toml
[dependencies]
parse-display = "0.8.2"
use parse_display::{Display, FromStr};

#[derive(Display, FromStr)]
#[display("{a}, {b}")]
struct Example {
    a: i32,
    b: i32,
}

これだけ。

型の定義に #[derive(Display, FromStr)] を追加し #[display(...)]std::fmt と同じ形式のフォーマット文字列を指定するだけです。

これで DisplayFromStr を実装できます。

仕組みは?

Rust の静的ソースコード生成機能である procedural macro を使い、属性と型の定義から実装を生成しています。

上の例の場合、次のような Display の実装を生成しています。

impl Display for Example {
    fn fmt(&self, f: &mut Formatter) -> Result {
        write!(f, "{}, {}", self.a, self.b)
    }
}

Display の実装は単純なコードでした。一方、 FromStr の実装は少し複雑です。

#[derive(FromStr)] が生成するコードでは、フォーマット文字列に対応する正規表現を使用して文字列を分割した後、各フィールドの FromStr を利用して値に変換しています。
(既定ではフォーマット文字列のプレースホルダ {} に対応する正規表現は .*? となります。)

use core::str::FromStr;
use parse_display::helpers::{once_cell::sync::Lazy, regex::Regex};
use parse_display::ParseError;

impl FromStr for Example {
    type Err = ParseError;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        static RE: Lazy<Regex> =
            Lazy::new(|| Regex::new("^(?<a>(?s:.*?)), (?<b>(?s:.*?))$").unwrap());
        if let Some(c) = RE.captures(s) {
            return Result::Ok(Self {
                a: c.name("a")
                    .map_or("", |m| m.as_str())
                    .parse()
                    .map_err(|_| ParseError::with_message("field `a` parse failed."))?,
                b: c.name("b")
                    .map_or("", |m| m.as_str())
                    .parse()
                    .map_err(|_| ParseError::with_message("field `b` parse failed."))?,
            });
        }
        Err(ParseError::new())
    }
}

列挙型にも対応

parse-display は列挙型にも対応しています。

#[derive(Display, FromStr)]
enum X {
    #[display("A = {0}")]
    A(i32),
    B,
}

assert_eq!(X::A(10).to_string(), "A = 10");
assert_eq!(X::B.to_string(), "B");

バリアント毎にフォーマット文字列を指定することができます。
また、ユニット型のバリアントではフォーマット文字列を省略でき、その場合はバリアント名がそのまま文字列になります。

属性によるカスタマイズ

parse-display では補助属性を使用して実装をカスタマイズすることができます。

DisplayFromStr の両方の実装を同時にカスタイマイズできるようにするため、下記のように属性によって影響範囲が異なっています。

  • #[display]DisplayFromStr の両方に影響
  • #[from_str]FromStr のみに影響

使用可能な属性の一覧はドキュメントにありますが、ここでは特に便利なものを紹介します。

バリアント名の変換

バリアント名をそのまま使用するのではなく snake_case や camelCase などに変換したい場合があります。

#[display(style = "...")] と指定することでバリアント名を変換することができます。

#[derive(Display, FromStr)]
#[display(style = "snake_case")]
enum X {
    VarA,
    VarB,
}

assert_eq!(X::VarA.to_string(), "var_a");
assert_eq!(X::VarB.to_string(), "var_b");

使用可能なスタイル一覧はドキュメントの style の項目を参照してください。

フィールド毎のフォーマット文字列の指定

フィールド毎にフォーマット文字列を指定することもできます。

#[derive(Display, FromStr)]
#[display("{a}, {b}")]
struct X {
    #[display("a = {}")]
    a: i32,
    #[display("b = {}")]
    b: i32,
}

assert_eq!(X { a: 10, b: 20 }.to_string(), "a = 10, b = 20");
assert_eq!("a = 10, b = 20".parse(), Ok(X { a: 10, b: 20 }));

フィールド毎にフォーマット文字列を書くことで、見やすくなりました。
フィールドの多い構造体でも安心です。

フィールドに対して指定されたフォーマット文字列では、そのフィールドの値を {} で参照することができます。

FromStr で使用する正規表現の指定

FromStr の実装ではフォーマット文字列のプレースホルダ {} に対応する正規表現は既定で .*? になると説明しました。
この動作はフォーマット文字列次第では意図しない結果が生じる可能性があります。

例えば、次の例では "abc12".parse() が成功することが期待されますが、実際には "abc12" 全体に対して i32 への変換が試みられるため失敗します。

#[derive(Display, FromStr)]
#[display("{a}{b}")]
struct X {
    a: String,
    b: i32,
}

i32 型のフィールドに対して #[regex(from_str = "[0-9]+")] と指定すると、プレースホルダに対応する正規表現が .*? から [0-9]+ に変更されます。
その結果、 i32 が数字のみとマッチし、期待通りに動作するようになります。

#[derive(Display, FromStr)]
#[display("{a}{b}")]
struct X {
    a: String,

    #[from_str(regex = "[0-9]+")]
    b: i32,
}

また、構造体自体に正規表現を指定することで FromStr を型付きの正規表現のように使用することもできます。

#[derive(FromStr)]
#[from_str("(?<y>[0-9]{4})-(?<m>[0-9]{2})-(?<d>[0-9]{2})")]
struct Date {
    y: i32,
    m: i32
    d: i32,
}

コンストラクタ関数の利用

Rust では構造体を直接作成するのではなく、new() のようなコンストラクタ関数を使用する場合があります。

例えば、不変条件を持つ型の場合、コンストラクタ関数によって不変条件が保証されることになるので FromStr の実装でもコンストラクタ関数を使用したいところです。

そのような場合に役立つのが #[from_str(new = ...)] です。この属性を指定することでコンストラクタ関数を使用した FromStr の実装を生成することができます。

#[derive(Display, FromStr)]
#[display("{0}")]
#[from_str(new = Self::new(_0))]
struct NonOneUsize(usize);

impl NonOneUsize {
    fn new(s: usize) -> Option<Self> {
        if s == 1 {
            None
        } else {
            Some(Self(s))
        }
    }
}
assert_eq!("0".parse(), Ok(NonOneUsize(0)));
assert!("1".parse().is_err());

#[from_str(new = ...)] にはコンストラクタ関数を呼び出す式を指定でき、式の中ではフィールド名と同名の変数(タプル構造体の場合はインデックスの前に _ を追加した変数)を使用できます。
式の戻り値の型は Self, Option<Self>, Result<Self, E> (Eは任意の型) のいずれかを使用でき、式が NoneErr を返した場合は FromStr::from_str の結果も Err になります。

型制約の指定

フォーマット文字列内でジェクリックパラメータを含む型のフィールドを参照すると、その型が自動的に型制約に追加されます。

例えば、次のように型 Y<T> のフィールドを参照すると

#[derive(Display, FromStr)]
#[display("{0}")]
pub struct X<T>(Y<T>);

struct Y<T>(T);

下記のように、 型制約 Y<T>: DisplayY<T>: FromStr が自動的に追加されます。

生成されたコード
impl<T> Display for X<T>
where
    Y<T>: Display,
{
    fn fmt(&self, f: &mut Formatter) -> Result {
        write!(f, "{}", self.0)
    }
}

impl<T> FromStr for X<T>
where
    Y<T>: FromStr,
{
    // ...
}

フォーマット文字列で Display 以外のトレイトを使用した場合は、そのトレイトも型制約に追加されます。

#[derive(Display)]
#[display("{0:?}")]
pub struct X<T>(Y<T>);
生成されたコード
impl<T> Display for X<T>
where
    Y<T>: Debug,
{
    // ...
}

しかし、この動作は内部実装が表に出てしまう為、適切でない場合もあるでしょう。

そのような場合に役立つのが #[display(bounds())] です。
この属性を指定することで型制約を手動で指定することができます。

#[derive(Display)]
#[display("{0}", bound(T: Display))]
#[form_str(bound(T : FromStr))]
pub struct X<T>(Y<T>);

また、#[display(bound(T: Display))]#[from_str(bound(T: FromStr))] をまとめて #[display(bound(T))] と書くこともできます。

#[derive(Display)]
#[display("{0}", bound(T))]
pub struct X<T>(Y<T>);

おわりに

直感的な仕様が評価されて多くのプロジェクトで使用された結果、ダウンロード数も 140 万回を超えました。(2023 年 12 月時点)

とても便利なクレートですので、ぜひ使ってみてください。

https://github.com/frozenlib/parse-display

Discussion