Named Arguments In Rust, If You Want Them
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.