この記事は Rust AdC 2021 その1の9日目の記事です。
こんにちは
1+2*(3+4)
「こんにちは。式です。」
1+2*(3+4)
「しかしこれは世を忍ぶ仮の姿!」
(1+(2*(3+4)))
「あるときには括弧フル装備」
+ 1 * 2 + 3 4
「またあるときにはポーランド記法」
1に2に3に4を足したものを掛けたものを足したもの
「さらには逆ポーランド記法の日本語表記にもなる」
???「まあどれも仮の姿なんですけどね」
問題
さて、このように「複数の姿を持つ」ケースで我々はどのように表示を実装すべきなのでしょうか。
単に1つか2つの形式しかないのであれば std::fmt::Display
と std::fmt::Debug
を使い分ける手もあるのですが、3通り以上あるとこれでは微妙です。
(なんなら Debug
をデバッグ表示以外の方法で使うのも、既に相当微妙なのですが……)
たとえば「仮の姿」が JSON や TOML などの汎用的な形式だったり読み書き両用であれば、 serde のようなライブラリに頼るのが有効でしょう。
しかし今回のケースではどうもそこまでするのは過剰戦力に思えます。要するに Display
を複数パターン実装したいだけですからね。
さてどうしましょう……というのが今回の問題です。
型定義
まず式から。
ついでに便利関数も用意しておきましょう。
/// Box された式。
type BoxExpr = Box<Expr>;
/// 式の抽象構文木 (Abstract Syntax Tree)。
#[derive(Debug, Clone, PartialEq)]
enum Expr {
/// リテラル。
Lit(Lit),
/// 加算。
Add(BoxExpr, BoxExpr),
/// 乗算。
Mul(BoxExpr, BoxExpr),
}
/// リテラル。
#[derive(Debug, Clone, Copy, PartialEq)]
enum Lit {
/// 整数。
Int(i64),
}
impl Expr {
/// Box して返す。
pub fn boxed(self) -> BoxExpr {
Box::new(self)
}
/// 与えられた式を加算する。
pub fn add(self, rhs: Expr) -> Self {
Self::Add(self.boxed(), rhs.boxed())
}
/// 与えられた式を乗ずる。
pub fn mul(self, rhs: Expr) -> Self {
Self::Mul(self.boxed(), rhs.boxed())
}
/// 整数リテラルを構築する。
pub fn int(v: i64) -> Self {
Self::Lit(Lit::Int(v))
}
}
「普通の」表示……?
さて、とりあえず「普通の」表記を実装しましょうか。
use std::fmt;
impl Expr {
/// 式 (`self`) を括弧で括る必要があれば true を返す。
fn needs_parens(&self, parent: &Self) -> bool {
match self {
Self::Add(..) => matches!(parent, Self::Mul(..)),
_ => false,
}
}
}
impl fmt::Display for Expr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Lit(e) => e.fmt(f),
Self::Add(lhs, rhs) => write!(f, "{}+{}", lhs, rhs),
Self::Mul(lhs, rhs) => {
// オペランドが優先順位の低い演算子の式だった場合、括弧を付ける。
if lhs.needs_parens(self) {
write!(f, "({})", lhs)?;
} else {
write!(f, "{}", lhs)?;
}
f.write_str("*")?;
if rhs.needs_parens(self) {
write!(f, "({})", rhs)
} else {
write!(f, "{}", rhs)
}
}
}
}
}
impl fmt::Display for Lit {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Int(v) => v.fmt(f),
}
}
}
いいですね。
fn main() {
let me = Expr::int(1).add(Expr::int(2).mul(Expr::int(3).add(Expr::int(4))));
assert_eq!(me.to_string(), "1+2*(3+4)");
}
できました。
できましたが、これは仮の姿のひとつに過ぎないのです……
他の姿はどのように実装すれば良いのでしょうか。
こたえ: 表示専用の型を作る
ひとつの標準的な答えは「表示に特化したラッパー型を用意する」です。
std::path::Path::display()
が返す std::path::Display<'_>
などがこの方式で実装されています。
Path
は Debug
を実装していますが、敢えて Display
を実装せず明示的に .display()
を呼ばせる仕組みになっているのです。
今日の話は言ってしまえばこれだけのことなのですが、とりあえず Expr
の例で実装してみましょう。
#[derive(Debug, Clone, Copy)]
struct DisplayWithParens<'a>(&'a Expr);
impl fmt::Display for DisplayWithParens<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.0 {
Expr::Lit(lit) => lit.fmt(f),
Expr::Add(lhs, rhs) => write!(
f,
"({}+{})",
lhs.display_with_parens(),
rhs.display_with_parens()
),
Expr::Mul(lhs, rhs) => {
write!(
f,
"({}*{})",
lhs.display_with_parens(),
rhs.display_with_parens()
)
}
}
}
}
impl Expr {
/// 式に括弧をつけまくって表示するようなオブジェクトを返す。
pub fn display_with_parens(&self) -> DisplayWithParens<'_> {
DisplayWithParens(self)
}
}
fn main() {
let me = Expr::int(1).add(Expr::int(2).mul(Expr::int(3).add(Expr::int(4))));
assert_eq!(me.to_string(), "1+2*(3+4)");
assert_eq!(me.display_with_parens().to_string(), "(1+(2*(3+4)))");
}
いいですね。
さて、このスタイルでやっていくといくらでも表示形式を増やすことができます。
しかし表示形式ごとに中間的な型 (上の例では DisplayWithParens
のような型) の数が増えていき、また型定義のみならず impl の数も一緒に増えていきます。
どうせこれら個別の型を表示以外に使いたいことなんてないのなら、すべての処理がひとつの箇所にまとまっていた方がうれしいですよね。
では関連するもの全部を一箇所にまとめてしまいましょう。
表示専用の型を隠す
今度はポーランド記法の例でいってみましょうか。
impl Expr {
/// 式をポーランド記法 (Polish notation) で表示するようなオブジェクトを返す。
pub fn display_pon(&self) -> impl fmt::Debug + '_ {
#[derive(Debug, Clone, Copy)]
struct DisplayPon<'a>(&'a Expr);
impl fmt::Display for DisplayPon<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.0 {
Expr::Lit(lit) => lit.fmt(f),
Expr::Add(lhs, rhs) => {
write!(f, "+ {} {}", lhs.display_pon(), rhs.display_pon())
}
Expr::Mul(lhs, rhs) => {
write!(f, "* {} {}", lhs.display_pon(), rhs.display_pon())
}
}
}
}
DisplayPon(self)
}
}
fn main() {
let me = Expr::int(1).add(Expr::int(2).mul(Expr::int(3).add(Expr::int(4))));
assert_eq!(me.to_string(), "1+2*(3+4)");
assert_eq!(me.display_with_paren().to_string(), "(1+(2*(3+4)))");
assert_eq!(me.display_pon().to_string(), "+ 1 * 2 + 3 4");
}
いいですね。
以下がポイントです:
- 関数内で型を定義できる。
- 関数内で型を定義すると、その関数内でのみ名前を呼べる。
- 関数内で型を定義しても、関数外でそのオブジェクトを扱うことはできる (ただし型名は呼べない)。
-
impl Trait
を関数の戻り値として使うと、「トレイトTrait
を実装している何らかの型 (ただし型名は教えない)」の値を返せる。
表示できさえすれば良くて型名をユーザに知ってもらう必要がない、むしろ知らせたくないという場合には、このように関数ローカルな型を用意して、型名を明かさぬまま値だけを返せば良いわけです。
めでたし。
……ところが、これはライブラリで使うには不向きな方式です。
ライブラリのユーザがライブラリから返された型を一時的に別の型に突っ込んで保管しておきたいなどの欲求を持ったとき、型名がわからないのではフィールドを定義することができません。
ユーザとしては全ての値の型名は明かされてほしいわけですね。
そういったわけで、もし多数の形式を実装したければ pub mod display {}
のような公開の子モジュールを用意して、そこに関連する表示用の型を突っ込むというのが良い落としどころでしょう。
全部まとめた例
とりあえず名前は明かす方向でいきます。
そちらの方がおすすめなので。
ついでに std::fmt::Display for Expr
を実装するのもやめました。それもまた仮の姿に過ぎないので。
use std::fmt;
/// Box された式。
type BoxExpr = Box<Expr>;
/// 式の抽象構文木 (Abstract Syntax Tree)。
#[derive(Debug, Clone, PartialEq)]
enum Expr {
/// リテラル。
Lit(Lit),
/// 加算。
Add(BoxExpr, BoxExpr),
/// 乗算。
Mul(BoxExpr, BoxExpr),
}
impl Expr {
/// Box して返す。
pub fn boxed(self) -> BoxExpr {
Box::new(self)
}
/// 与えられた式を加算する。
pub fn add(self, rhs: Expr) -> Self {
Self::Add(self.boxed(), rhs.boxed())
}
/// 与えられた式を乗ずる。
pub fn mul(self, rhs: Expr) -> Self {
Self::Mul(self.boxed(), rhs.boxed())
}
/// 整数リテラルを構築する。
pub fn int(v: i64) -> Self {
Self::Lit(Lit::Int(v))
}
/// 式 (`self`) を括弧で括る必要があれば true を返す。
fn needs_parens(&self, parent: &Self) -> bool {
match self {
Self::Add(..) => matches!(parent, Self::Mul(..)),
_ => false,
}
}
}
/// リテラル。
#[derive(Debug, Clone, Copy, PartialEq)]
enum Lit {
/// 整数。
Int(i64),
}
impl fmt::Display for Lit {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Int(v) => v.fmt(f),
}
}
}
pub mod display {
use super::Expr;
use std::fmt;
impl Expr {
/// 式を最も自然な形で表示するようなオブジェクトを返す。
pub fn display_natural(&self) -> DisplayNatural<'_> {
DisplayNatural(self)
}
/// 式に括弧をつけまくって表示するようなオブジェクトを返す。
pub fn display_with_parens(&self) -> DisplayWithParens<'_> {
DisplayWithParens(self)
}
/// 式をポーランド記法 (Polish notation) で表示するようなオブジェクトを返す。
pub fn display_pon(&self) -> DisplayPon<'_> {
DisplayPon(self)
}
/// 式を逆ポーランド記法 (Reverse Polish notation) 的な日本語で表示するようなオブジェクトを返す。
pub fn display_rpn_ja(&self) -> DisplayRpnJa<'_> {
DisplayRpnJa(self)
}
}
#[derive(Debug, Clone, Copy)]
pub struct DisplayNatural<'a>(&'a Expr);
impl fmt::Display for DisplayNatural<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.0 {
Expr::Lit(e) => e.fmt(f),
Expr::Add(lhs, rhs) => {
write!(f, "{}+{}", lhs.display_natural(), rhs.display_natural())
}
Expr::Mul(lhs, rhs) => {
// オペランドが優先順位の低い演算子の式だった場合、括弧を付ける。
if lhs.needs_parens(self.0) {
write!(f, "({})", lhs.display_natural())?;
} else {
write!(f, "{}", lhs.display_natural())?;
}
f.write_str("*")?;
if rhs.needs_parens(self.0) {
write!(f, "({})", rhs.display_natural())
} else {
write!(f, "{}", rhs.display_natural())
}
}
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct DisplayWithParens<'a>(&'a Expr);
impl fmt::Display for DisplayWithParens<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.0 {
Expr::Lit(lit) => lit.fmt(f),
Expr::Add(lhs, rhs) => write!(
f,
"({}+{})",
lhs.display_with_parens(),
rhs.display_with_parens()
),
Expr::Mul(lhs, rhs) => write!(
f,
"({}*{})",
lhs.display_with_parens(),
rhs.display_with_parens()
),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct DisplayPon<'a>(&'a Expr);
impl fmt::Display for DisplayPon<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.0 {
Expr::Lit(lit) => lit.fmt(f),
Expr::Add(lhs, rhs) => write!(f, "+ {} {}", lhs.display_pon(), rhs.display_pon()),
Expr::Mul(lhs, rhs) => write!(f, "* {} {}", lhs.display_pon(), rhs.display_pon()),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct DisplayRpnJa<'a>(&'a Expr);
impl fmt::Display for DisplayRpnJa<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.0 {
Expr::Lit(lit) => lit.fmt(f),
Expr::Add(lhs, rhs) => write!(
f,
"{}に{}を足したもの",
lhs.display_rpn_ja(),
rhs.display_rpn_ja()
),
Expr::Mul(lhs, rhs) => write!(
f,
"{}に{}を掛けたもの",
lhs.display_rpn_ja(),
rhs.display_rpn_ja()
),
}
}
}
}
fn main() {
let me = Expr::int(1).add(Expr::int(2).mul(Expr::int(3).add(Expr::int(4))));
assert_eq!(me.display_natural().to_string(), "1+2*(3+4)");
assert_eq!(me.display_with_parens().to_string(), "(1+(2*(3+4)))");
assert_eq!(me.display_pon().to_string(), "+ 1 * 2 + 3 4");
assert_eq!(
me.display_rpn_ja().to_string(),
"1に2に3に4を足したものを掛けたものを足したもの"
);
println!("{} 「こんにちは。式です。」", me.display_natural());
println!("{} 「しかしこれは世を忍ぶ仮の姿!」", me.display_natural());
println!("{} 「あるときには括弧フル装備」", me.display_with_parens());
println!("{} 「またあるときにはポーランド記法」", me.display_pon());
println!(
"{} 「さらには逆ポーランド記法の日本語表記にもなる」",
me.display_rpn_ja()
);
println!("{:?} 「まあどれも仮の姿なんですけどね」", me);
}
他にも、たとえば内部的なロジックが似通っている場合であれば、 struct DisplayFooStyle<'a>(&'a Object, Style);
のように値への参照に加えてスタイルも紐付けてもいいかもしれません。
v.display_foo(some_style)
のように使えるようにすると良いでしょう。
たとえばこの記事の例だと、括弧をフル装備するか否かは bool のフラグで制御する手はあるでしょう。型をひとつ減らせます。
おしまい
表示専用の型を用意して値への参照を持たせるという、たったそれだけのパターンを知っておくと、いざ「正式で唯一の表示形式」を持たない何かを表示したくなったとき大変便利です。
標準ライブラリ (libstd, liballoc, libcore) にはこうした「知っていると便利なやり方」がちょくちょくあるので、暇なときにドキュメントを全部読んでみるときっと学びがありますよ。