Rust Generic (Not Generics)
Have you ever wanted to convert Hlist
s into Structs or to reuse logic across different types that are structurally identical or very similar (e.g. same data across different domains)? Generic
can help you do that with minimal boilerplate.
Generic
is a way of representing a type in … a generic way. By coding around Generic
, you can write functions that abstract over types and arity, but still have the ability to recover your original type afterwards. This can be a fairly powerful thing.
Thanks to the new Macros 1.1 infrastructure added in Rust 1.15, Frunk comes out of the box with a custom Generic
derivation so that boilerplate is kept to a minimum. Without further ado, let’s dive in to see what Generic can do for us.
Add Frunk to your project
Frunk is published to Crates.io, so to begin, add the crate to your list of dependencies:
1 2 |
|
Examples
HList ⇄ Struct
Have an HList
lying around and want to turn it into a Struct with the same shape (maybe you’re using Validated)?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
This also works the other way too; just pass a struct to into_generic
and get its generic representation.
One usecase for something like this is if you have a bunch of fields that you want to validate “simultaneously”, and you want to transform the end result into a single Struct; this is often the case when you want to turn external input (e.g. coming into your API, a web form, or fields read from a database), and in a previous post I introduced Validated as a way of doing that.
With the introduction of Generic
, that last step of transforming an HList
into your struct gets much simpler:
1 2 3 4 |
|
Converting between Structs
Sometimes you might have 2 or more types that are structurally the same (e.g. different domains but the same data) and you’d like to convert between them. An example of this might be when you have a model for deserialising from an external API and another one for internal application business logic, and yet another for persistence.
Generic comes with a handy convert_from
method that helps here:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
Another example of where this might be useful is if you want to use different types to represent the same data at different stages (see this post on StackOverflow).
How it works (what is going on ? is it safe ?)
At a glance, Generic
might look magical and dangerous, but really it is no more mysterious than the From
trait in the standard lib; the only difference (for now) is that every Generic
instance is bidirectional (can turn an A
into a Repr
and a Repr
into an A
). If you don’t believe me, just look at the type signatures.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
Most of the magic resides in how the custom derive of Generic, made possible by the 1.15 release of Rust, is implemented. If you want to find out more, take a look at the derives
directory of Frunk on Github. In regards to the end-result though, the following:
1 2 3 4 5 6 |
|
Gets expanded at compile-time to something resembling:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
To be clear, the actual expanded coded is much gnarlier because we use fully qualified names for the sake of hygiene and I’ve sugared some things up with their macro-powered equivalents to cut down on noise (namely the HList type signature, pattern matching, and construction).
Someone on Twitter raised the point that if you had mixed up the ordering of the fields in your struct declaration (e.g. last name and first name are swapped between structs), then Generic
would cause silent errors at runtime because the Structs’ shape would be the same, and that implementing From
was more typesafe. With all due respect to that individual, the same could happen even if you hand-wrote your From
implementation and got your field assignments crossed. In the worst case; you’ve now got fields that are not ordered correctly, your From
is wrong, and you’ve got more boilerplate to maintain.
Really, the only way to truly prevent this kind of fat-fingering error is to have wrapper types (like struct FirstName(String)
, etc) for all your fields, in which case Generic
conversion would be foolproof (if you got your field declaration orders wrong, you’d get a compile-time error). Ultimately, how typesafe you want to be is a choice you will need to make while weighing the risk of fat-fingering against the burden of maintaining more code.
I hope you’re now convinced that there is no dirty casting / unsafe stuff going on, so you can rest easy knowing your code is still as type-safe as it would have been if you had gone with something like From
instead.
Conclusion
There are probably many other ways that Generic
can be used to make code nicer (more reusable, DRYer, less noisy), so go ahead and see what you can cook up. As always, please don’t hesitate to get in touch via comments, on Github or on Gitter with suggestions, issues, questions, or PRs.