Learning Rust: Inline Macros

Adrian Macal
3 min readJun 11, 2023

--

Ever find yourself typing the same old boilerplate code again and again? Spot a pattern, and boom! Rust can whip up the code for you!

As a software developer, I write a notable amount of code. It’s enjoyable to type and see a stream of characters manifesting my thoughts. However, my thought process tends to move faster than my fingers on the keyboard. To address this disparity, I sometimes seek ways to minimize typing while maintaining my thinking speed.

During these moments, I may recognize opportunities for optimization, such as extracting a repeated code into a dedicated function. This step not only reduces typing but also simplifies code maintenance. When this is done, it often brings a sense of satisfaction. However, there are times when this process can feel like a trade-off: the extracted and generalized code could perform slower than the original, leaving me wishing for an optimal balance of efficiency and performance.

Unlike Python, Java, or C#, Rust allows code generation with macros at compile time. What does this mean? It means you can write code that looks like standard Rust, which will then be expanded into different code during compilation. Consider the well-known example of using the println! macro:

let name = "Adrian";
println!("Hello, {}", name);

The code is intended to print a single concatenated line. In languages like Java, the runtime would parse the first argument, create a placeholder, and then substitute it during printing. This process consumes additional CPU cycles and can potentially fail. In contrast, Rust parses such formatting at compile time and produces the following equivalent code:

let name = "Adrian";
{
::std::io::_print(
::core::fmt::Arguments::new_v1(
&["Hello, ", "\n"],
&[::core::fmt::ArgumentV1::new_display(&name)],
),
);
};

In Rust, you can create such macros yourself. The syntax is challenging to understand initially, but the paste crate simplifies the learning curve. Let’s consider an example that involves considerable boilerplate code:

struct ParserRevision {
revision_id: Option<String>,
revision_id_parent: Option<String>,
sha1: Option<String>,
timestamp: Option<String>,
contributor_id: Option<String>,
contributor_ip: Option<String>,
contributor_name: Option<String>,
}

impl ParserRevision {
fn with_revision_id(mut self, value: String) -> Self {
self.revision_id = Some(value);
self
}

fn with_revision_id_parent(mut self, value: String) -> Self {
self.revision_id_parent = Some(value);
self
}

fn with_sha1(mut self, value: String) -> Self {
self.sha1 = Some(value);
self
}

fn with_timestamp(mut self, value: String) -> Self {
self.timestamp = Some(value);
self
}

fn with_contributor_id(mut self, value: String) -> Self {
self.contributor_id = Some(value);
self
}

fn with_contributor_ip(mut self, value: String) -> Self {
self.contributor_ip = Some(value);
self
}

fn with_contributor_name(mut self, value: String) -> Self {
self.contributor_name = Some(value);
self
}
}

The code demonstrates the usage of a mutable builder object populating its private fields. Each field requires a new method that looks identical to the others. We could improve this by implementing a runtime method like the following:

struct ParserRevision {
revision_id: Option<String>,
revision_id_parent: Option<String>,
sha1: Option<String>,
timestamp: Option<String>,
contributor_id: Option<String>,
contributor_ip: Option<String>,
contributor_name: Option<String>,
}

impl ParserRevision {
fn with(mut self, key: &str, value: String) -> Self {
match key {
"revision_id" => self.revision_id = Some(value),
"revision_id_parent" => self.revision_id_parent = Some(value),
"sha1" => self.sha1 = Some(value),
"timestamp" => self.timestamp = Some(value),
"contributor_id" => self.contributor_id = Some(value),
"contributor_ip" => self.contributor_ip = Some(value),
"contributor_name" => self.contributor_name = Some(value),
key => panic!("Key not found: {}", key),
}

self
}
}

This implementation has several drawbacks, such as the use of panic! and the absence of robust checks for valid identifiers. With Rust macros, however, you can bypass these issues. We can generate the previous verbose code, which validates the identifiers and doesn’t resort to panicking:

macro_rules! with_functions {
($($name:ident,)*) => {
paste::paste! {
$(
fn [<with_ $name>](mut self, value: String) -> Self {
self.$name = Some(value);
self
}
)*
}
};
}

impl ParserRevision {
with_functions! {
revision_id,
revision_id_parent,
sha1,
timestamp,
contributor_id,
contributor_ip,
contributor_name,
}
}

This macro takes a list of comma-separated identifiers and generates the corresponding method for each field responsible for setting that field. Isn’t it a beautiful approach?”

Writing code is fun, and generating code takes this fun to a whole new level. Can you see what you might automate for an extra dose of fun next time?

--

--

Adrian Macal
Adrian Macal

Written by Adrian Macal

Software Developer, Data Engineer with solid knowledge of Business Intelligence. Passionate about programming.

No responses yet