The last several posts have introduced a number of abstractions, namely HList, Generic, LabelledGeneric, as well as pluck() and sculpt(). Although each of those have impressive party tricks of their own, I’d like to share how you can use them to write a reuseable, generic function that handles converting between structs with mis-matched fields and thus have different LabelledGeneric representations.

Unlike the last post, this one will be relatively light on recursion and mind-bending type-level stuff; it’s time to sit back and enjoy the fruits of our labour.

Adding Frunk to your project

Much of this post will make use of Frunk’s types (e.g. HCons, HNil), methods, macros (esp. for describing macro types via the Hlist! type macro), and terminology.

It might be easier to follow along if you add Frunk to your project and play around with it. Frunk is published to Crates.io, so to add it your list of dependencies, simply put this in your Cargo.toml:

Crates.io

1
2
[dependencies]
frunk = "${latest_version}"

Alternatively, take a look at the published Rustdocs.

Boilerplate-free conversions between Structs

Suppose we have a bunch of structs that are similar-ish in terms of their data but ultimately, not necessarily exactly the same. This means we can’t just use the normal LabelledGeneric convert_from method to convert between them.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#[derive(LabelledGeneric)]
struct UserFromDb<'a> {
    id: u64,
    first_name: &'a str,
    last_name: &'a str,
    email: &'a str,
    age: u32,
    pw_hash: &'a str,
    is_admin: bool,
    created_at: i64
}

// Holds User data for rendering in a front-end view
// or for sending over an API. Striped of any sensitive
// information
#[derive(LabelledGeneric)]
struct PresentableUser<'a> {
    last_name: &'a str,
    first_name: &'a str,
    age: u32,
    created_at: i64
}

// Holds data for sending a User over our internal API
#[derive(LabelledGeneric)]
struct InternalApiUser<'a> {
    id: u64,
    first_name: &'a str,
    last_name: &'a str,
    age: u32,
    email: &'a str,
    is_admin: bool,
    created_at: i64
}

In our example, PresentableUser and InternalApiUser structs have fields that are subsets of the fields in UserFromDb, and not in the same order either. The scenario is that UserFromDb is a struct that we get from reading our persistence layer, and the other 2 are types that we use in our application for business logic.

Assuming a flow where we want to be able to go from UserFromDb to either PresentableUser or InternalApiUser, the idea is that we don’t want be holding on to sensitive data like pw_hash when we don’t need to, thus lowering the risk of accidentally leaking said data (e.g. serialising it by accident, or by rendering it in debug messages, etc).

While we could go about writing Froms by hand for each of these, and for every other time a similar situation arises, that’s quite a lot of boilerplate to write and maintain. Thankfully, we can make use of Frunk’s LabelledGeneric and Sculptor to write a single, reuseable generic function.

Note, for a review of:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/// Converts from another type A into B assuming that A and B have labelled generic representations
/// that can be sculpted into each other.
///
/// Note that this method tosses away the "remainder" of the sculpted representation.
fn transform_from<A, B, Indices>(a: A) -> B
    where A: LabelledGeneric,
          B: LabelledGeneric,
// The labelled representation of A must be sculpt-able into the labelled representation of Self
          <A as LabelledGeneric>::Repr: Sculptor<<B as LabelledGeneric>::Repr, Indices> {
    // Turn A into its labelled generic representation
    let a_gen = <A as LabelledGeneric>::into(a);
    // Sculpt the generic labelled representation of A into the labelled generic representation
    // of B. We ignore the remainder.
    let (b_gen, _): (<B as LabelledGeneric>::Repr, _) = a_gen.sculpt();
    // Turn the lablled generic representation of B into B
    <B as LabelledGeneric>::from(b_gen)
}

Not bad. The body of the function is literally 3 lines long :) Now we can do this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let u_db = UserFromDb {
    id: 3,
    first_name: "Joe",
    last_name: "Blow",
    email: "[email protected]",
    age: 30,
    pw_hash: "asd35235adsf",
    is_admin: true,
    created_at: 12345,
};

let p_user: PresentableUser = transform_from(udb);
// or
let i_user: InternalApiUser = transform_from(udb);

In actuality, Frunk already ships with this function so you can use it out of the box.

Conclusion

Often times, you’ll hear that heterogeneous lists enable developers to write reuseable generic functions because they abstract over arity and types, and it might not be obvious exactly what that means on a practical level. The example shown in this post just scratches the surface of what is made possible through HList and LabelledGeneric, and there are definitely more creative usages out there, such as building of boilerplate-free (e.g. JSON) codecs (hint: look to Haskell and Scala libs for more).

As usual, please give it a spin and chime in with any questions, corrections, and suggestions !

Comments