Have you ever wanted to convert Hlists 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
[dependencies]
frunk = "0.1.9"

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
extern crate frunk;
#[macro_use] // for the hlist macro
extern crate frunk_core;
use frunk::*; // for the Generic trait and HList

#[derive(Generic, Debug, PartialEq)]
struct Person<'a> {
    first_name: &'a str,
    last_name: &'a str,
    age: usize,
}

let h = hlist!("Joe", "Blow", 30);
let p: Person = from_generic(h);
assert_eq!(p,
           Person {
               first_name: "Joe",
               last_name: "Blow",
               age: 30,
           });

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
let validated = get_first_name().into_validated() + get_last_name() + get_age();
let person: Result<Person, _> = validated
  .into_result()
  .map(|h| from_generic(h)); // <-- much simpler !

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
// Assume we have all the imports needed

#[derive(Generic)]
struct ApiPerson<'a> {
    FirstName: &'a str,
    LastName: &'a str,
    Age: usize,
}

#[derive(Generic)]
struct DomainPerson<'a> {
    first_name: &'a str,
    last_name: &'a str,
    age: usize,
}

let a_person = ApiPersion {
                   first_name: "Joe",
                   last_name: "Blow",
                   age: 30,
};
let d_person: DomainPersion = convert_from(a_person); // done

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
pub trait Generic<Repr> {

    /// Go from something to Repr
    fn into(self) -> Repr;

    /// Go from Repr to something
    fn from(r: Repr) -> Self;
}

/// Given a generic Representation of an A, returns A
pub fn from_generic<A, Repr>(gen: Repr) -> A where A: Generic<Repr>

/// Given an A, returns its generic Representation
pub fn into_generic<A, Repr>(a: A) -> Repr where A: Generic<Repr>

/// Converts one type into another assuming they have the same generic Representation
pub fn convert_from<A, B, Repr>(a: A) -> B
  where
    A: Generic<Repr>,
    B: Generic<Repr>

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
#[derive(Generic)]
struct Person<'a> {
    first_name: &'a str,
    last_name: &'a str,
    age: usize,
}

Gets expanded at compile-time to something resembling:

1
2
3
4
5
6
7
8
9
10
11
12
impl<'a> Generic<Hlist!(&'a str, &'a str, usize)> for Person<'a> {

    fn into(self) -> Hlist!(&'a str, &'a str, usize) {
        let Person { first_name, last_name, age } = self;
        hlist! [ first_name, last_name, age ]
    }

    fn from(r: Hlist!(&'a str, &'a str, usize)) -> Self {
        let hlist_pat! [ first_name, last_name, age ] = r;
        Person { first_name: first_name, last_name: last_name, age: age }
    }
}

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.

  1. Frunk on Github
  2. Frunk on Crates.io

Credit

  1. Shapeless

Comments