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
toself
means that now the compiler needs to know that the object passed isSized
: 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 aCommonStruct
(or returns anItemCreationError
) 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
fromsrc
, now we are not passing a reference anymore I
is now aT
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 implementsTryInto
, 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.
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.
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!