Rust is one of the most loved languages1 by developers out there. Its in-built memory safeness and the great ecosystem around it make it a great option for building blazingly fast applications.
In this article, I highlight some examples of how I use some of the Rust features to make my code for this website more extensible and ergonomic.
The trait system
In Rust, traits are used to define functionality that particular types would implement. It's similar to the concept of interfaces in some languages. For example, in C++ one could define an interface using an abstract class, setting up the pure virtual functions that other classes would implement:
.cpp;
;
In Rust we would use traits:
.rs;
Traits and interfaces allow for polymorphism in our code, making our code easily extensible.
My database handler
I'm using MongoDB for this blog (at the time of writing this), but I wanted to create an interface to handle the database so that I could change it in the future if needed without worrying about the code that has nothing to do with the database interaction. If I decide it's better to use PostgreSQL sometime down the line, I don't want to go through my code modifying each function that needs to get the information from a user.
So I created a trait that a database handler should implement
.rs
Here is one of the nice things about traits in Rust. I just created a trait that is "the sum" of other traits, that I have separated for modularity. A DBHandler
must implement UserDb
, UnconfirmedUserDb
, and PostDb
. Where, for instance, the PostDb
trait is defined like this:
.rsuse crate;
Then my functions that need to use the database are implemented like this:
.rspub async
where the function takes any type as long as it implements the DBHandler
trait. This allows me to create custom implementations of the database handler for different databases, for instance:
.rs// Imports
// struct specific implementations
// Implementation of DBHandler, it's empty because it just
// requires the implementation of the other traits.
// Implementation of the UserDb trait
// The rest of the implementations
This is compile-time polymorphism, which takes advantage of the Rust's borrow checker.
If I decided to move to a different database I would only need to implement the traits and modify the main.rs
so I use the new handler.
Metaprogramming with macros
Another great feature of Rust is its powerful metaprogramming capabilities with macros. Macros allow us to generate code programmatically, which can help us evade boilerplate, make our code DRY (Don't Repeat Yourself), and sometimes make it more concise and readable.
Fail-fast Config struct
In my code, I needed some configuration coming from the environment variables. If the variable is not defined it is an irrecoverable error and the program cannot continue. Therefore, instead of waiting for the variable to be needed by the code, I want the program to fail fast if any variable is not loaded when the program starts.
I created a struct for the configuration and loaded each variable, making sure to give an error message if it's not defined.
.rs
But each new variable added means repeating the line to load it, so, in order not to repeat myself, I created this macro to help me with the implementation:
.rs;
}
}
Then I can declare the Config struct like
.rscreate_env_struct!
async
Of course, it seems like more code than I would need to implement it by hand,
but it comes with two main advantages: 1) Extensibility: just need to add the new variable's name when needed; and 2) Prevent copy/paste related bugs.