macro_rules! lazy_format {
    ($pattern:literal $(, $($args:tt)*)?) => { ... };
    (match ($condition:expr) {
        $($(
            $match_pattern:pat
            $(if $guard:expr)?
            => $output:tt
        ),+ $(,)?)?
    }) => { ... };
    (
        if $(let $match:pat = )? $condition:expr => $output:tt
        $(else if $(let $elseif_match:pat = )? $elseif_condition:expr => $elseif_output:tt)*
        $(else => $else_output:tt)?
    ) => { ... };
    ($output:tt for $item:pat in $collection:expr) => { ... };
}
Expand description

Lazily format something. Essentially the same as format!, except that instead of formatting its arguments to a string, it captures them in an opaque struct, which can be formatted later. This allows you to build up formatting operations without any intermediary allocations or extra formatting calls. Also supports lazy conditional and looping constructs.

The return value of this macro is left deliberately unspecified and undocumented. The most important this about it is its Display implementation, which executes the deferred formatting operation. It also provides a Debug implementation, which simply prints the lazy_format!(...) call without evaluating any of its arguments, as well as Clone and Copy if those traits are available in the captured context.

Note that this macro is completely lazy; it captures the expressions to be formatted in the struct and doesn’t evaluate them until the struct is actually written to a String or File or or other writable destination. This means that the argument expression will be evaluated every time the instance is written, which may not be what you want; be sure to eagerly perform any 1-time calculations you want to before calling lazy_format!.

Basic example:

use std::fmt::Display;
use lazy_format::lazy_format;

fn get_hello() -> String {
    String::from("Hello")
}

fn get_world() -> String {
    String::from("World")
}

fn hello_world() -> impl Display {
    lazy_format!("{}, {w}!", get_hello(), w = get_world())
}

let result = hello_world();

// get_hello and get_world aren't called until the object is
// formatted into a String.
let result_str = result.to_string();
assert_eq!(result_str, "Hello, World!");

Just like with regular formatting, lazy_format can automatically, implicitly capture named parameters:

use std::mem::{size_of_val, size_of};
use lazy_format::lazy_format;

let a = 10;
let b = 20;

let result = lazy_format!("{a} {b}");
assert_eq!(size_of_val(&result), size_of::<i32>() * 2);
assert_eq!(result.to_string(), "10 20");

Demonstation of lazy capturing:

use std::fmt::Display;
use std::mem::{size_of_val, size_of};
use lazy_format::lazy_format;


fn get_formatted() -> impl Display {
    let a: isize = 10;
    let b: isize = 15;

    lazy_format!("10 + 15: {v}, again: {v}", v = (a + b))
}

let result = get_formatted();

// The result captures 2 isize values (a and b) from get_formatted.
assert_eq!(size_of_val(&result), size_of::<isize>() * 2);

let result_str = result.to_string();
assert_eq!(result_str, "10 + 15: 25, again: 25");

Conditional formatting

lazy_format! supports conditional formatting with match- or if- style syntax. When doing a conditional format, add the formatting pattern and arguments directly into the match arms or if blocks, rather than code; this allows conditional formatting to still be captured in a single static type.

match conditional example:

use std::fmt::Display;
use lazy_format::lazy_format;

fn get_number(num: usize) -> impl Display {
    // Note that the parenthesis in the match conditional are required,
    // due to limitations in Rust's macro parsing (can't follow an
    // expression with `{}`)
    lazy_format!(match (num) {
        0 => "Zero",
        1 => "One",
        2 => "Two",
        3 => "Three",
        4 | 5 => "Four or five",
        value if value % 2 == 0 => ("A large even number: {}", value),
        value => "An unrecognized number: {value}",
    })
}

assert_eq!(get_number(0).to_string(), "Zero");
assert_eq!(get_number(1).to_string(), "One");
assert_eq!(get_number(2).to_string(), "Two");
assert_eq!(get_number(3).to_string(), "Three");
assert_eq!(get_number(4).to_string(), "Four or five");
assert_eq!(get_number(5).to_string(), "Four or five");
assert_eq!(get_number(6).to_string(), "A large even number: 6");
assert_eq!(get_number(7).to_string(), "An unrecognized number: 7");

if conditional example:

use std::fmt::Display;
use lazy_format::lazy_format;

fn describe_number(value: isize) -> impl Display {
    lazy_format!(
        if value < 0 => ("A negative number: {}", value)
        else if value % 3 == 0 => ("A number divisible by 3: {}", value)
        else if value % 2 == 1 => ("An odd number: {}", value)
        else => "Some other kind of number"
    )
}

assert_eq!(describe_number(-2).to_string(), "A negative number: -2");
assert_eq!(describe_number(-1).to_string(), "A negative number: -1");
assert_eq!(describe_number(0).to_string(), "A number divisible by 3: 0");
assert_eq!(describe_number(1).to_string(), "An odd number: 1");
assert_eq!(describe_number(2).to_string(), "Some other kind of number");
assert_eq!(describe_number(3).to_string(), "A number divisible by 3: 3");

if formatters are allowed to exclude the final else branch, in which case lazy_format will simply write an empty string:

use std::fmt::Display;
use lazy_format::lazy_format;

fn only_evens(value: i32) -> impl Display {
    lazy_format!(if value % 2 == 0 => ("An even number: {}", value))
}

assert_eq!(only_evens(10).to_string(), "An even number: 10");
assert_eq!(only_evens(5).to_string(), "");

if let conditional example:

use std::fmt::Display;
use lazy_format::lazy_format;

fn describe_optional_number(value: Option<isize>) -> impl Display {
    lazy_format!(
        if let Some(10) = value => "It's ten!"
        else if let Some(3) | Some(4) = value => "It's three or four!"
        else if let | Some(0) = value => "It's zero!"
        else if let Some(x) = value => ("It's some other value: {}", x)
        else => "It's not a number!"
    )
}

assert_eq!(describe_optional_number(Some(10)).to_string(), "It's ten!");
assert_eq!(describe_optional_number(Some(3)).to_string(), "It's three or four!");
assert_eq!(describe_optional_number(Some(4)).to_string(), "It's three or four!");
assert_eq!(describe_optional_number(Some(0)).to_string(), "It's zero!");
assert_eq!(describe_optional_number(Some(5)).to_string(), "It's some other value: 5");
assert_eq!(describe_optional_number(None).to_string(), "It's not a number!");

Looping formatting

lazy_format! supports formatting elements in a collection with a loop:

use std::fmt::Display;
use lazy_format::lazy_format;

let list = vec![1i32, 2, 3, 4];
let list_ref = &list;

// Format each element in the iterable without additional arguments to `format_args`
let simple_semicolons = lazy_format!("{v}; " for v in list_ref.iter().map(|x| x - 1));
assert_eq!(simple_semicolons.to_string(), "0; 1; 2; 3; ");

// Perform a full format with additional arguments on each element in the iterable.
let header = "Value";
let full_format = lazy_format!(("{}: {}; ", header, v) for v in list_ref);
assert_eq!(full_format.to_string(), "Value: 1; Value: 2; Value: 3; Value: 4; ");

Note that these looping formatters are not suitable for doing something like a comma separated list, since they’ll apply the formatting to all elements. For a lazy string joining library, which only inserts separators between elements in a list, check out joinery.