🧵

Rustのto_string実装パターン

2022/12/19に公開

Rustで値の文字列表現を返すにあたって、 String を直接返すのではなく Display を実装するのが一般的です。この派生パターンとして以下の4つのパターンを紹介します。

  • 基本: 文字列化を実装したいとき
  • 文字列化をインターフェースに含めたいとき
  • カスタム文字列化
  • カスタム文字列化をインターフェースに含めたいとき

基本: 文字列化を実装したいとき

Displayを実装すると、文字列化できるようになります。 println!, format! などのフォーマット処理から呼べるようになるほか、 .to_string() というヘルパ関数が使えるようになります。

以下はプログラミング言語処理系において、「変数」をあらわす構造体に文字列化を実装する例です。

Playground

pub struct Var(String);

impl std::fmt::Display for Var {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "{}", self.0)?;
        Ok(())
    }
}

#[test]
fn test_var() {
    assert_eq!(Var("x".to_owned()).to_string(), "x");
}

文字列化をインターフェースに含めたいとき

Displayを継承します。std::error::Errorなどで使われています。

以下はプログラミング言語処理系において、「式」をあらわすトレイトに文字列化インターフェースを定義し、「変数」をあらわす構造体で実装する例です。

Playground

pub struct Var(String);

pub trait Expr: std::fmt::Display {
    // ...
}

impl std::fmt::Display for Var {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "{}", self.0)?;
        Ok(())
    }
}
impl Expr for Var {}

#[test]
fn test_var() {
    let e: &dyn Expr = &Var("x".to_owned());
    assert_eq!(e.to_string(), "x");
}

カスタム文字列化

Displayを実装する方法は以下のようなケースでは使えません。

  • 標準的な文字列化とは異なる意味論であることを明示したい場合
  • 引数をとる必要がある場合

このような場合はDisplayを実装した専用の構造体を返す方法があります。標準ライブラリの Path::displayがそのような例になっています。

以下はプログラミング言語処理系において、「変数」をあらわす構造体に文字列化を実装する例です。ただし、変数名をinternしているため、引数としてContextを取るようになっています。

Playground

use std::collections::HashMap;

#[derive(Default)]
pub struct Context {
    var_names: Vec<String>,
    var_indices: HashMap<String, usize>,
}

pub struct Var(usize);

impl Var {
    pub fn new(ctx: &mut Context, name: &str) -> Self {
        let id = *ctx.var_indices.entry(name.to_owned()).or_insert_with(|| {
            let id = ctx.var_names.len();
            ctx.var_names.push(name.to_owned());
            id
        });
        Self(id)
    }

    pub fn display<'a>(&'a self, ctx: &'a Context) -> VarDisplay<'a> {
        VarDisplay { var: self, ctx }
    }
}

pub struct VarDisplay<'a> {
    var: &'a Var,
    ctx: &'a Context,
}

impl std::fmt::Display for VarDisplay<'_> {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        let name = &self.ctx.var_names[self.var.0];
        write!(f, "{}", name)?;
        Ok(())
    }
}

#[test]
fn test_var() {
    let mut ctx = Context::default();
    let v = Var::new(&mut ctx, "x");
    assert_eq!(v.display(&ctx).to_string(), "x");
}

カスタム文字列化をインターフェースに含めたいとき

上記のようなカスタム文字列化は存在型を要求することになるため、共通インターフェースとしては使い勝手が悪いです。そこで、インターフェースには impl Display を返す関数ではなく、 Display::fmt と同等のシグネチャを持つメソッドを定義します。 impl Display を組み立てる部分はヘルパとして分離します。

以下はプログラミング言語処理系において、「式」をあらわすトレイトに文字列化インターフェースを定義し、「変数」をあらわす構造体で実装する例です。ただし、変数名をinternしているため、引数としてContextを取るようになっています。

Playground

use std::collections::HashMap;

#[derive(Default)]
pub struct Context {
    var_names: Vec<String>,
    var_indices: HashMap<String, usize>,
}

pub trait Expr {
    fn fmt_with(&self, f: &mut std::fmt::Formatter, ctx: &Context) -> std::fmt::Result;
}

impl dyn Expr {
    pub fn display<'a>(&'a self, ctx: &'a Context) -> ExprDisplay<'a> {
        ExprDisplay { expr: self, ctx }
    }
}

pub struct ExprDisplay<'a> {
    expr: &'a dyn Expr,
    ctx: &'a Context,
}

impl std::fmt::Display for ExprDisplay<'_> {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        self.expr.fmt_with(f, self.ctx)
    }
}

pub struct Var(usize);

impl Var {
    pub fn new(ctx: &mut Context, name: &str) -> Self {
        let id = *ctx.var_indices.entry(name.to_owned()).or_insert_with(|| {
            let id = ctx.var_names.len();
            ctx.var_names.push(name.to_owned());
            id
        });
        Self(id)
    }
}
impl Expr for Var {
    fn fmt_with(&self, f: &mut std::fmt::Formatter, ctx: &Context) -> std::fmt::Result {
        let name = &ctx.var_names[self.0];
        write!(f, "{}", name)?;
        Ok(())
    }
}

#[test]
fn test_var() {
    let mut ctx = Context::default();
    let v = Var::new(&mut ctx, "x");
    let e: &dyn Expr = &v;
    assert_eq!(e.display(&ctx).to_string(), "x");
}

Discussion