How to create a formatter that doesn't allocate while being composable?

Hello,

I am creating a formatting function, and I'm not yet satisfied with the design.

pub fn surround_with_star<T>(out: &mut T, int: some_value)
where
    T: ?Sized + std::fmt::Write,
{
    write!("*{}*", some_value);
}

It works well, and doesn't do unnecessary allocations, but I'm not satisfied with the interface. This function doesn't compose well at all. For example, I can't call it two time to add two stars on each side:

let mut out = Vec<u8>::new();
// This obviously doesn't work
surround_with_star(
    surround_with_star(out, 3)
);

And I kind of dislike that I need to choose between std::fmt::Write or std::io::Write for the bound of T.

Is there a way to rewrite this function as something like:

fn surround_with_star(in: Some_iterator) -> Some_iterator {
    chain("*".as_iterator(), in, "*".as_iterator())
}
2 Likes

How about returning a wrapper struct that implements Display. This will work with both fmt::Write and io::Write:

pub fn surround_with_star<T: Display>(some_value: T) -> impl Display {
    struct Starred<T>(T);
    
    impl<T: Display> Display for Starred<T> {
        fn fmt(&self, f: &mut Formatter) -> fmt::Result {
            write!(f, "*{}*", self.0)
        }
    }
    
    Starred(some_value)
}


fn main() {
    let x = surround_with_star(5);
    let y = surround_with_star(x);
    println!("{}", y);
}

Playground

4 Likes

Using this you can write to both fmt::Write and io::Write using the write macro. This works because write calls write_fmt, (which is on both fmt::Write::write_fmt andio::Write::write_fmt) using the format_args macro.

write!(writer, "{}", surround_with_star(something))?;
writer.write_fmt!(format_args!("{}", surround_with_star(something)))?;

That's perfect! Thanks to both of you.

In reality, my surround_with_star() fuction is part of a trait. So I can't use impl Display as the return type. What is my best option?

EDIT: It look like I can use #![feature(type_alias_impl_trait)] on nightly, I will test that tomorrow.

You can also add an associated type to the trait. Or you can just return Starred

struct Starred<T>(T);

impl<T: Display> Display for Starred<T> {
    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
        write!(f, "*{}*", self.0)
    }
}

pub trait Foo {
    pub fn surround_with_star<T: Display>(&self) -> Starred<&T> {
        Starred(self)
    }
}

2 Likes

I wanted to create a trait TextSerializer, then implement it for MarkdownSerializer and AnsiSerializer. That trait would have a few functions like bold(), and strike(), and would – as you suggested – take something that implement Display in input, and return something else that also implement Display as output (but not the same type). However, the concrete return type of MarkdowSerializer::bold() would not be the same concrete type than the one of AnsiSerializer::strong() since they don't implement Format the way (**{}** for MarkdownSerializer and ^[[1m{}^[[0m for AnsiSerializer).

I think I need either GAT (generic associated types) or impl Trait in the returns time of another trait for what I was trying to do.


I somehow made it work, with what I consider extreme boilerplate, and not an easy to use API. The goal was to be able to pass a struct MarkdownFormatter that implements TextFormatter and that's all, but all I managed to do was to be able to have a MarkdownFormatter<T>

mod formatter {
    use std::fmt;
    use std::fmt::{Display, Formatter};
    use std::marker::PhantomData;

    pub trait TextFormatter<T: Display> {
        type Bold: Display;
        fn bold(value: T) -> Self::Bold;

        type Strike: Display;
        fn strike(value: T) -> Self::Strike;
    }   

    pub struct MarkdownBold<T: Display>(T);
    impl<T: Display> Display for MarkdownBold<T> {
        fn fmt(&self, f: &mut Formatter) -> fmt::Result {
            write!(f, "**{}**", self.0)
        }   
    }   

    pub struct MarkdownStrike<T: Display>(T);
    impl<T: Display> Display for MarkdownStrike<T> {
        fn fmt(&self, f: &mut Formatter) -> fmt::Result {
            write!(f, "~~{}~~", self.0)
        }
    }

    pub struct MarkdownFormatter<T: Display> (PhantomData<T>);
    impl<T: Display> TextFormatter<T> for MarkdownFormatter<T> {
        type Bold = MarkdownBold<T>;
        fn bold(value: T) -> Self::Bold {
            MarkdownBold::<T>(value)
        }   

        type Strike = MarkdownStrike<T>;
        fn strike(value: T) -> Self::Strike {
            MarkdownStrike::<T>(value)
        }   
    }   
}

Usage

// I would have prefer to be able to just say
// T: TextFormatter
// and not have to specialize it for
// T: TextFormatter<i64>>
pub fn format_dices<'a, T: TextFormatter<i64>>(dices: &'a [i64]) -> impl Display + 'a {
    use itertools::Itertools;

    // Note: here I can only format i64 objects using my formatter.
    // Otherwise I would need a second formatter (like a
    // `TextFormatter<i64> and a `TextFormatter<str>`)
 
    dices
        .iter()
        .format_with(", ", |&dice, f|
            if dice == 1  || dice == 10 {
                f(&format_args!("{}", T::bold(dice)))
            } else if dice <= 5 {
                f(&format_args!("{}", T::strike(dice)))
            } else {
                f(&format_args!("{}", dice))
            }
        )
} // note: there is more code below (it wasn't obvious in the preview)

// I would have prefer to be able to just say
// format_dices::<MarkdownFormatter>
// and not have to specialize it for
// format_dices::<MarkdownFormatter<i64>>
pub fn main() {
    // prints "Some numbers: **1**, ~~2~~, ~~3~~"
    println!("Some numbers: {}", format_dices::<MarkdownFormatter<i64>>(&[1,2,3]));

    // prints "Some other numbers: 8, 9, **10**"
    println!("Some other numbers: {}", format_dices::<MarkdownFormatter<i64>>(&[8, 9, 10));
}

To be able to simplify all of that, I think that I need to either be able to return an impl Display for the function of the trait TextFormatter, or to have GAT to be able to do:

pub trait TextFormatter {
    type Bold<T: Display>: Display;
    fn bold<T: Display>(value: T) -> Self::Bold<T>;

    type Strike<T: Display>: Display;
    fn strike<T: Display>(value: T) -> Self::Strike<T>;
}

pub struct MarkdownFormatter; // No PhantomData nonsense
impl TextFormatter for MarkdownFormatter {...}

pub fn format_dices<'a, T: TextFormatter>(dices: &'a [i64]) -> impl Display + 'a;
                           ^^^^^^^^^^^^^
                     no specialization needed

println!("Some numbers: {}", format_dices::<MarkdownFormatter>(&[1,2,3]));
                                            ^^^^^^^^^^^^^^^^^
                                               nor here

Is there yet another technique that I don't know or do I basically need either to use Rust nightly, or give up the genericity of the formatter, or have not so nice API with a lot of boilerplate?

1 Like

Here's a technique I've used which is heavy on trait declaration boilerplate, but should reduce boilerplate for trait implementation if you're doing multiple trait impls. It uses one struct per trait method, and one default method per implemented method to turn the "fmt-style" method into a method returning something which implements Display

mod formatter {
    use std::fmt;
    use std::fmt::{Display, Formatter};
    use std::marker::PhantomData;

    pub struct TextFmtDisplayBold<TF: ?Sized, T> {
        value: T,
        phantom: PhantomData<TF>,
    }
    pub struct TextFmtDisplayStrike<TF: ?Sized, T> {
        value: T,
        phantom: PhantomData<TF>,
    }

    pub trait TextFormatter<T: Display> {
        fn write_bold(f: &mut Formatter<'_>, value: &T) -> fmt::Result;
        fn bold(value: T) -> TextFmtDisplayBold<Self, T> {
            TextFmtDisplayBold {
                value,
                phantom: PhantomData,
            }
        }

        fn write_strike(f: &mut Formatter<'_>, value: &T) -> fmt::Result;
        fn strike(value: T) -> TextFmtDisplayBold<Self, T> {
            TextFmtDisplayBold {
                value,
                phantom: PhantomData,
            }
        }
    }

    impl<TF, T> Display for TextFmtDisplayBold<TF, T>
    where
        T: Display,
        TF: TextFormatter<T>,
    {
        fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
            TF::write_bold(f, &self.value)
        }
    }
    impl<TF, T> Display for TextFmtDisplayStrike<TF, T>
    where
        T: Display,
        TF: TextFormatter<T>,
    {
        fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
            TF::write_strike(f, &self.value)
        }
    }

    // trait implementation is just:

    pub struct MarkdownFormatter<T: Display>(PhantomData<T>);
    impl<T: Display> TextFormatter<T> for MarkdownFormatter<T> {
        fn write_bold(f: &mut Formatter<'_>, value: &T) -> fmt::Result {
            write!(f, "**{}**", value)
        }

        fn write_strike(f: &mut Formatter<'_>, value: &T) -> fmt::Result {
            write!(f, "~~{}~~", value)
        }
    }
}

The usage is identical to the usage of the code in your last post.

Since there are no associated types now, we can also remove the trait's type parameter:

mod formatter {
    use std::fmt;
    use std::fmt::{Display, Formatter};
    use std::marker::PhantomData;

    pub struct TextFmtDisplayBold<TF: ?Sized, T> {
        value: T,
        phantom: PhantomData<TF>,
    }
    pub struct TextFmtDisplayStrike<TF: ?Sized, T> {
        value: T,
        phantom: PhantomData<TF>,
    }

    pub trait TextFormatter {
        fn write_bold<T: Display>(f: &mut Formatter<'_>, value: &T) -> fmt::Result;
        fn bold<T: Display>(value: T) -> TextFmtDisplayBold<Self, T> {
            TextFmtDisplayBold {
                value,
                phantom: PhantomData,
            }
        }

        fn write_strike<T: Display>(f: &mut Formatter<'_>, value: &T) -> fmt::Result;
        fn strike<T: Display>(value: T) -> TextFmtDisplayBold<Self, T> {
            TextFmtDisplayBold {
                value,
                phantom: PhantomData,
            }
        }
    }

    impl<TF, T> Display for TextFmtDisplayBold<TF, T>
    where
        T: Display,
        TF: TextFormatter,
    {
        fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
            TF::write_bold(f, &self.value)
        }
    }
    impl<TF, T> Display for TextFmtDisplayStrike<TF, T>
    where
        T: Display,
        TF: TextFormatter,
    {
        fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
            TF::write_strike(f, &self.value)
        }
    }

    // trait implementation is just:

    pub struct MarkdownFormatter;
    impl TextFormatter for MarkdownFormatter {
        fn write_bold<T: Display>(f: &mut Formatter<'_>, value: &T) -> fmt::Result {
            write!(f, "**{}**", value)
        }

        fn write_strike<T: Display>(f: &mut Formatter<'_>, value: &T) -> fmt::Result {
            write!(f, "~~{}~~", value)
        }
    }
}

Let me know if this works! I've only used it a few times, but it should function. I don't really know how to describe the hack - I think it mostly just bypasses the problem. But it should be just as general?

6 Likes

That's really clever!

1 Like

I really like the idea. This looks a lot like NVI (Non Virtual Interface) from C++. It's a shame that every method in a trait is public, and can't be made private, since write_bold() and write_strike() shouldn't be part of the public API.

1 Like

It look like, it's too early for me! It took me 5 minutes to spot that there is a typo in your code:

       fn strike<T: Display>(value: T) -> TextFmtDisplayBold<Self, T> {
            TextFmtDisplayBold { // <-- should be TextFmtDisplayStrike
                value,
                phantom: PhantomData,
            }
        }
1 Like

It's absolutely working for me. My issue is fully solved!

And I learned a lot in the process

Note: it is possible to make private part of a trait, but the syntax isn't fantastic (compared to a simple pub before each function but the private one in a trait, just like for anything else):

mod formatter {
    use std::fmt;
    use std::fmt::{Display, Formatter};
    use std::marker::PhantomData;

    pub struct TextFmtDisplayBold<TF: ?Sized, T> {
        value: T,
        phantom: PhantomData<TF>,
    }
    pub struct TextFmtDisplayStrike<TF: ?Sized, T> {
        value: T,
        phantom: PhantomData<TF>,
    }

    // Create a private trait
    mod private {
        use std::fmt;
        use std::fmt::{Display, Formatter};
        pub trait TextFormatter {
            fn write_bold<T: Display>(f: &mut Formatter<'_>, value: &T) -> fmt::Result;
            fn write_strike<T: Display>(f: &mut Formatter<'_>, value: &T) -> fmt::Result;
        }
    }

    // make the public trait inherit from the private trait
    pub trait TextFormatter: private::TextFormatter {
        fn bold<T: Display>(value: T) -> TextFmtDisplayBold<Self, T> {
            TextFmtDisplayBold {
                value,
                phantom: PhantomData,
            }
        }

        fn strike<T: Display>(value: T) -> TextFmtDisplayStrike<Self, T> {
            TextFmtDisplayStrike {
                value,
                phantom: PhantomData,
            }
        }
    }

    impl<TF, T> Display for TextFmtDisplayBold<TF, T>
    where
        T: Display,
        TF: TextFormatter,
    {
        fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
            TF::write_bold(f, &self.value)
        }
    }
    impl<TF, T> Display for TextFmtDisplayStrike<TF, T>
    where
        T: Display,
        TF: TextFormatter,
    {
        fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
            TF::write_strike(f, &self.value)
        }
    }

    pub struct MarkdownFormatter;
    // Default implementation of the public trait
    impl TextFormatter for MarkdownFormatter {}
    // And the real work is done in the implementation of the private trait
    impl private::TextFormatter for MarkdownFormatter {
        fn write_bold<T: Display>(f: &mut Formatter<'_>, value: &T) -> fmt::Result {
            write!(f, "**{}**", value)
        }

        fn write_strike<T: Display>(f: &mut Formatter<'_>, value: &T) -> fmt::Result {
            write!(f, "~~{}~~", value)
        }
    }
}

The code doesn't change a lot, I added a comment before each part that changed to make it more obvious.

1 Like

It case it may interest you, I wrote an article about all of this on my blog.

3 Likes

For the people that like this thread, I highly advice you to look at the other question I opened, especially this post and my final version.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.