localhost

sorting the wheat from the chaff

Rust Generics

Today I've learned a little about refactoring code using Generics.

§ (Don't) fix what ain't broken

I had some code that worked well. Let's recap step by step the main points.

I have a Trait with a extract_data() method. This method does nothing :-) I also have two structs for different kind of data:

trait CommonBehaviourTrait {
    fn extract_data(&self) {
        debug!("Default empty implementation");
    }
}

struct AgencyStruct {
    location: String
}

struct MemberStruct {
    name: String
}

I'm implementing the extract_data() method for both structs. The actual work done depends on the struct. We see that the mapper() function comes from different modules. While the name of the function is the same, the implementation might be different.

impl CommonBehaviourTrait for Vec<AgencyStruct> {
    fn extract_data(&self) {
        agency_utils::mapper(self);
    }
}

impl CommonBehaviourTrait for Vec<MemberStruct> {
    fn extract_data(&self) {
        member_utils::mapper(self);
    }
}

Let's see what mapper() does for AgencyStruct:

pub fn mapper<'a, I>(src: I) -> impl Iterator<Item = CommonStruct> + 'a
where
    I: IntoIterator<Item = &'a AgencyStruct>,
    <I as std::iter::IntoIterator>::IntoIter: 'a,
{
    src.into_iter().map(|list_item| {
        let res = CommonStruct::try_from(list_item)
}

Which reads as: this function take an object I, an iterator (IntoIter, i.e. a trait that implements the Iterator itself) of AgencyStruct with an attached lifetime 'a. The lifetime is valid for all items inside the iteration plus the iterator itself. The body of the function iterates src and remap the content to CommonStruct (a data structure that remaps different things to the same content). The function returns another iterator, this time of our CommonStruct type. The same lifetime 'a is attached to the returning value.

The try_from invoked there takes the reference to AgencyStruct and returns a new instance of CommonStruct (or an ItemCreationError):

impl TryFrom<&AgencyStruct> for CommonStruct {
    type Error = ItemCreationError;

    fn try_from(src: &Agencystruct) -> Result<Self, Self::Error> {
        CommonStruct {
            data: src.location
        }
}

I was so proud of this code (written with heavy guidance on how to use iterators and the where clause), but a friend of mine decided to break my application with an innocent remark: "why don't we make a generic implementation?". And he is right, there is a lot of boilerplate here!

Ok, let's try a refactor.

§ Generics at work

First thing, we want to replace the specific type AgencyStruct/MemberStruct stuff with a generic type T, to be able to pass anything to the function.

 pub trait CommonBehaviourTrait {
    fn extract_data(self)
    where
        Self: std::marker::Sized,
    {
         debug!("Default implementation of CommonBehaviourTrait");
     }
}

impl<T> CommonBehaviourTrait for Vec<T>
where
    T: TryInto<CommonStruct, Error = ItemCreationError> + Sized,
{
    fn extract_data(self) {
         let expimp_data = mapper(self);
     }
 }

With this I could remove all the specific implementations of the Trait. Notable changes:

  • Changing &self to self means that now the compiler needs to know that the object passed is Sized: a reference has a known compile-time size, but a copy of an object doens't.
  • The new TryInto says: "T must be anything that can be turned into a CommonStruct (or returns an ItemCreationError) and that you know the size at compile-time (Sized).

The mapper() function is then refactored as follows. Let's the diff for clarity:

+pub fn mapper<I, T>(src: I) -> impl Iterator<Item = CommonStruct>
 where
-    I: IntoIterator<Item = &'a MyStuff>,
-    <I as std::iter::IntoIterator>::IntoIter: 'a,
+    I: IntoIterator<Item = T>,
+    T: TryInto<CommonStruct, Error = ItemCreationError> + Sized,
 {
     src.into_iter().map(|course| {
-        let res = CommonStruct::try_from(course);
+        let res = course.try_into();
         match res {
             ...

Changes:

  • We've removed the lifetime 'a from src, now we are not passing a reference anymore
  • I is now a T generic parameter
  • The T generic parameter is defined as we described a moment ago (an object that implements a TryInto etc. etc.).
  • Since src iterator implements TryInto, now we can also call its items with .try_into() (not sure if this was needed).

§ Closing notes

This is how the TryFrom I was using before is defined.

Rust book and source:

pub trait TryFrom<T>: Sized {
    /// The type returned in the event of a conversion error.
    type Error;

    /// Performs the conversion.
    fn try_from(value: T) -> Result<Self, Self::Error>;
}

This is how the TryInto is defined.

Rust book and sources:

impl<SpecificType, CommonStruct> TryInto<CommonStruct> for Specifictype

impl<T, U> TryInto<U> for T where U: TryFrom<T>
{
    type Error = U::Error;

    fn try_into(self) -> Result<U, U::Error> {
        U::try_from(self)
    }
}

There's a tricky part in TryInto that got us lose some time; we were confused by the ordering of U and T.

Another comment that I received looking at the refactored code is: you could have done that more easily with a macro, instead of getting crazy with that where clauses.

Thanks to @tglman for causing all this trouble :-) and patiently teach me something really cool!


Comments closed for this article