Generics or Metaprogramming? Declarative macros with Rust
Smartly read inputs from standard input
Reading inputs from command line is probably the first task competitive coders do as they start to solve complex problems. While numerous languages are used by thousands of coders everyday, some emerge out as the popular ones; hello C++! One of the most powerful tools to write high performance programs. But… with great power, comes responsibility of managing memory *manually (Let’s not get into modern C++). While competitive coders do not care about (much) about de-allocating memory on an online judge, it is a pretty critical task for production systems to clean up and reclaim memory. Let’s bring our language into picture now; a language that offers several high level APIs, while being close to metal. Welcome to Rust!
Eh.. why mention C++ then?
One of the most powerful features that C++ offers, is metaprogramming via macros. These macros strip out large code blocks into few simple statements, which are expanded at compile time. Rust’s powerful macro system offers similar simplicity and re-usability! We are gonna get briefly into Rust’s macro system, while sticking to our original problem of reading inputs from command line as we compare 3 different approaches.
Let’s read some inputs, shall we?
This post will take 3 approaches to read input from standard input and will try to parse to required data type. For the sake of simplicity, let’s assume we wish to read a 32bit integer and a 32bit floating point number.
The concrete:
Can we not have 2 concrete implementations?
We sure can! Let’s quickly look at it:
While this gets the job done, but what if we want to extend our reader to other types? This approach will require rewriting another function altogether.
How about generics?
The generic:
Sure! Since this story does not focus on generics, I’ll briefly touch upon some things here and there. Let’s give it a shot!
It works!
But… there are additional things here. What is this FromStr
? Why do we need it?! In simplest terms, trait FromStr
on a type T
is the ability to convert a &str
to T
. For parse()
to work, our generic type should be a FromStr
, hence the additional trait bound on type argument.
There is definitely some underlying generic stuff going on, which could be slower at runtime. Thankfully, for most types we deal with in competitive coding, this generic function will be expanded using monomorphization and hence, a static dispatch will happen, reducing the runtime slowdown to merely a function call. Well, this gets work done.
However, it’s not just additional traits, this implementation also expects a mutable reference to the variable. The caller of this function will look like this:
let mut t: i32 = 10;
read(&mut t);
Which is reasonable, but can we really use a more powerful weapon here?
The macro:
We sure can, let’s bring in the macros into picture.
So here we formally define a macro from the Rust Book.
A macro is a way of writing code that writes other code
In Rust, invoking macros might look like function calls, but they are fundamentally different. A function is a block of reusable code that gets specified task done, whereas a macro is a piece of code that can generate more code (including functions), that are expanded as the code compiles. Macros in Rust detect patterns and then generate code based on how these patterns are handled by the programmer.
Enough talk, let’s look into the code now!
To define a macro in Rust, we use a macro from the standard library called macro_rules
.
A typical macro in Rust looks like:
Pattern matching for macros can be done on various designators like identifiers, types, expressions, etc.
However, for sticking to our initial use case, we will not dive in-depth into macros here, but do consider leafing through the macros section of The Rust Programming Language book.
Coming back to our problem of reading values from command line, here’s a macro based implementation:
This piece of code defines a macro called read. The macro accepts an identifier (in $v
) and a type (in $t
). These will be matched against the arguments provided while invoking the macro and will be replaced with values at compile time. Let’s look how the macro are invoked in Rust:
read!(n: i32); // Notice the `!`
And voila! Whatever is supplied via command line, will be stored in n
as a 32bit integer. Notice that this can be used for multiple types. Notice we didn’t need to explicitly bring FromStr
trait into scope!
The entire source code looks like:
But there’s a way to look into the magic! Rust’s nightly compiler can show the code after expanding macros.
Running rustc +nightly -Z unstable-options — pretty expanded src/main.rs
, we get:
One single line read!(n: i32);
has been expanded into a statements that declare, read, and parse the value for us.
It’s done, we are successfully able to read values with minimal effort using macros!
We have reduced our work to a single statement, is that all? No! not really. Macros can match on multiple patterns. Let’s put our read macro on steroids and enable it to read a vector as well!
To close things off, please note that Rust macros are capable of doing much more than what has been listed in this story. It surely will take many to cover it all.
Conclusion:
It is clear that we cut down significantly on the amount of code we’d need to write for reading inputs on from CLI. We also saw how macros can match against multiple patterns and expand in more than one way!