Consider the following snippet of Rust, of a Target struct, along with a builder to go along with it:

struct Target {
    foo: bool,
    bar: u64,
    baz: i64,
}

struct Builder {
    foo: Option<bool>,
    bar: Option<u64>,
    baz: Option<i64>,
}

impl Builder {
    fn new() -> Builder {
        return Builder {
            foo: None,
            bar: None,
            baz: None,
        };
    }

    fn foo(mut self, v: bool) -> Builder {
        self.foo = Some(v);
        return self;
    }

    fn bar(mut self, v: u64) -> Builder {
        self.bar = Some(v);
        return self;
    }

    fn baz(mut self, v: i64) -> Builder {
        self.baz = Some(v);
        return self;
    }

    fn build(self) -> Target {
        return Target {
            foo: self.foo.unwrap_or(false),
            bar: self.bar.unwrap_or(0),
            baz: self.baz.unwrap_or(0),
        };
    }
}

This lets you build up an object, and then get it.

let t = Builder::new().foo(true).bar(2).baz(3).build();

This is not super fun though. Beyond having to talk about builders, we're somehow not talking about our target result!

Let's instead have a construction pattern that talks about our target (T), and that looks a bit more like assignment via named arguments.

macro_rules! T {
    ($($n:ident = $e:expr),+) => {
        Builder::new()$(.$n($e))*.build()
    };
}

Now you can talk about your targets. And sometimes they're so important they deserve a single-letter macro:

let t = T!(foo=true, bar,2, baz=3);

That's a bit better. Named arguments, so you know what you're passing in. Underneath, a builder pattern.

This pattern still allows for passing in only what you need to, unlike direct struct construction.

let t = T!(bar=2);
assert_eq!(
    t,
    Target {
        foo: false,
        bar: 2,
        baz: 0
    }
)

One might consider that many real-world builders in Rust are falliable. They're usually working with Result types in order to get anything done.

Let's think about that a bit:

struct TryBuilder {}

impl TryBuilder {
    fn new() -> TryBuilder {
        return TryBuilder {};
    }
    fn bar(mut self, v: u64) -> Self {
        return self;
    }

    fn build(mut self) -> Result<Target, String> {
        return Err("Couldn't build".to_string());
    }
}

Instead of building T's, we'll "try" to build one:

macro_rules! tryT {
    ($($n:ident = $e:expr),+) => {
        TryBuilder::new()$(.$n($e))*.build()?
    };
}

The above macro gives me exception-like behavior for free. One less ? to type!

fn try_u64_from_t() -> Result<u64, String> {
    let t = tryT!(bar = 3);
    return Ok(t.bar);
}

tryT auto-?'s the expression, so t is already unwrapped. Of course, this is a bad idea, ? is already pretty excellent!

There are a lot of reasons to not use macros, from just general impenetrability to debugability issues. But the biggest reason that we are all embarassed to use is simply taste: macros have certain flavors to them, and the slightest misalignment there can just leave people with bad tastes in their mouths.

But if you're working on a codebase by yourself, for your own needs: go wild if you want! There is no intrinisic evil in a codebase that is macro heavy. We're not in C, these things are pretty principled.