Rust: How to use generics

Pudding Entertainment
6 min readFeb 3, 2023
Photo by Markus Spiske on Unsplash

Designing a good architecture for your application is a challenging task. Moreover, as the system grows and requirements change, the original blueprints might no longer be applicable to the current needs. Fortunately for us developers, there is a concept of generic programming that can significantly improve maintainability of the code potentially requiring fewer adjustments in future.

Prerequisites

I’m using Rust version 1.67.0. This tutorial assumes prior knowledge of Rust and concentrates more on practical usage of the language. I’ll continue working on the codebase from my previous tutorial, and I do recommend you read that one first to better understand the background.

As always, project sources are available at GitHub, find the link at the end of the page.

There are 3 parts in this tutorial: Problem statement, Generics for envy, Generics for API calls

Part 1. Problem statement

To illustrate the true power of generics we will solve two problems in the scope of this tutorial.

First one is about the way env variables are fetched. Consider the following piece of code:

Here, in order to get the port value we first need to get the variable from the .env file, check that it is present, parse it or panic in case it is not parsable. With more variables added instead of copy-pasting the code from one place to another we will introduce a dedicated module env_utils that will encapsulate this logic. That would be a good and easy example to start working with generics, especially if you are coming from a language that doesn’t have this concept.

Second one is about requesting and processing the ImmutableX API to retrieve mint and asset data. As you remember, in the previous tutorial we introduced mint_reader:

Now as the application evolves we also want to fetch all the assets data. Luckily, both APIs are designed in a similar way so that we can minimize duplication with the help of generics. This example will use structs, traits and the async-trait crate.

Part 2. Generics for envy

Just to recap, the environment variables are fetched from .env file using the envy crate. As outlined in the previous part, this operation is quite verbose, thus copying it all over the code-base will introduce an unnecessary maintenance burden. Instead, let’s create a utility module that will hide the logic:

Now we can use it directly in db_handler like so:

But this solution has a very obvious drawback. With more env variables of different types added we would need to go back to env_utils and create a method for each of those. Here generics come to rescue! Instead of having a function that returns a concrete type let’s abstract it away with T:

With the help of the new method we can now parse env variables of the supported types: port(env_utils::as_parsed::<u16>(“DB_PORT”)) or some_bool(env_utils::as_parsed::<bool>(“MY_BOOL”))

Note! I’ve left the as_string method in the utils since most of the env variables in this application are strings. It is not actually required and can be replaced with env_utils::as_parsed::<string>(“DB_DATABASE”)

As you can see in the code snippet above, we used generics by not specifying a concrete type (such as u16 or bool) but by abstracting it away with T. The letter T doesn’t have any significant meaning as such, but it is a convention across programming languages to use it as a type parameter. On top of that there is also a where clause that is used to specify bounds on type parameter T. In this particular case the parse function (that is also using generics under the hood) requires the parameter to be bound by (aka implement) FromStr and Debug traits.

Part 3. Generics for API calls

Moving on to a more complex example. Now we will rewrite the previously created mints_reader module using generics, in order to add support for the assets API.
Just to recap, we have the following mint model already:

Following the ImmutableX specs, we will add the following asset model:

You might have already spotted a few similarities between mint and asset, namely they both have a cursor field and a vec of results.

With the model now in place let’s take a step back and identify all the changes required for the mints_reader module created previously:

I’ll follow the line numbers from the gist:

  • (0) Since this module will now be responsible for more than just mints it should be renamed to something more generic, say reader
  • (4) The read function should either be generic or its code extracted into a separate generic function. We will go with the latter option
  • (19) The url var should not use a static MINTS_URL field anymore
  • (20) The fetch_api_response function (which was using generics already) should not reference Mint as it’s type
  • (24) (28) Those two calls shouldn’t be referencing mint
  • (25) We should find a way to persist both mint and asset

Sounds like a lot of changes! At this point you might be tempted to simply copy-paste the existing solution and change Mint to Asset where needed. But believe me, in the long run such complex refactoring will pay off.

In my opinion it is beneficial to illustrate an iterative approach to refactoring of this kind. Thus, it will be split into 3 major parts.

We will begin with the first 4 points, those are low hanging fruits. After all the outlined changes are in place the interim version of the reader module will look like this:

Note! As this is an iterative process it is perfectly acceptable to have commented-out code in between the iterations. You just need to make sure that this code 1) compiles and 2) is not persisted in git history

At this moment we have already abstracted away all types in read_with_cursor_as and subsequent functions. Now, in order to implement required changes for (24) and (28) we need to adjust the models accordingly. As it was noticed earlier, both mint and asset have a cursor field and a vec of results. Which suggests we should be able to abstract those fields away. In Rust traits are used to define shared behavior. Let’s define it for our models:

Now we can impl it for mint and asset:

Both models are now implementing PaginatedApi, so we can go ahead and apply further changes to reader:

Note that now T is bound by both DeserializeOwned and PaginatedApi traits. That makes it possible to call has_results and get_cursor methods on result returned from fetch_api_response.

Last remaining step is to persist the fetched data into the database. Currently, we only have one module responsible for saving mint:

In order to make it generic, we will combine traits with structs here.
Let’s define the trait first. From looking at the existing implementation we can see that to save mint we need a vec of results and Postgres pool. Thus we can define a trait like this:

As you can see here I’m also changing the vec of results to T to be more future-proof on the persistence layer.

Note! As of time of writing, Rust only supports async traits in nightly builds. Eventually, the support should also be added in the production build, but for now async_trait crate is used. You can read more about it in the official blog post.

Next, we will create two separate modules responsible for saving the models. Each of those modules will define a struct that will implement the Persistable trait.

Previously implemented save_mint method is now moved to mints_handler:

And a new module to save assets will be created:

With those changes in place we can now finish reader implementation:

Note! Using dyn traits has its own trade-offs, please do read the official documentation about it!

You can see that now the methods accept a persistable trait of type T, thus abstracting away a concrete implementation. This approach makes it straightforward to add support for more APIs, making the code easily extendable.

Afterwards

Generic programming is a highly valuable skill for developers to have in their toolset. It empowers them to write more resilient and scalable code while reducing redundancy. This tutorial showcases the power of generic programming in Rust, but even if you don’t yet use Rust, I hope it inspires you to reconsider your programming practices and embrace generic programming in your primary language.

All of the code can be found at this GitHub repository.

Support

If you like the content you read and want to support the author — thank you very much!

Here is my Ethereum wallet for tips:

0xB34C2BcE674104a7ca1ECEbF76d21fE1099132F0

--

--

Pudding Entertainment

Serious software engineer with everlasting passion for GameDev. Dreaming of next big project. https://pudding.pro