Best Practices When Defining a Default Implementation for a Trait's Method


#1

Within a small toy project that I’m working on, I’ve defined several structs, each defining a translate method. E.g.:

struct Structure {
    translation: HashMap<char, char>,
    // Other, not relevant data
}

impl Structure {
    fn translate(&self, input: char) -> char {
        self.translation.get(input).unwrap()
    }
}

Each struct, while holding different data, at least shares what’s above: a translation member defined as HashMap<char, char>, and a translate method.

Now, I can obviously make that code more reusable by defining a Trait – such as Translate – with a default method implementation similar to what’s above. However, my question is: is that good style? Is it still within best practice to define a Trait with methods that assume a particular member is available, with the above example being the translation HashMap? Or is there a better way of doing this that I’m not realizing?

And while I realize that all of these problems are fairly isolated to my own projects, and (probably) won’t impact the wider world, since I’m still learning the intricacies of the language, I’d like to learn how to do things The Right Way.


#2

Now, I can obviously make that code more reusable by defining a Trait – such as Translate – with a default method implementation similar to what’s above.

Can you? That default implementation can’t assume the existence of the translation field.


#3

The core lib does it as well. Yes, you can define default methods of a trait, so that you would just let a method that returns its HashMap, so that that other defined method performs the translation by using this getter method.

Say you have this Structure:

struct Structure {
   translation: HashMap<char, char>,
}

and then you have this trait Translation:

trait Translation {
   fn translate(&self, input: char) -> char {
       self.get_trans().get(input).unwrap()
   }

   // This is the method you'll need to implement
   fn get_trans<'a>(&'a self) -> &'a HashMap<char, char>;
}

So, whenever you implement the trait for any data structure, you’ll just need to define the get_trans method. Just like this:

impl Translation for Structure {
   fn get_trans<'a>(&'a self) -> &'a HashMap<char, char> {
      &self.translation
   }
}

And doing this:

let x = Structure { translation: HashMap::new() };
x.translate('X')

Is just fine.
Hope it’d be useful for you :smile:


#4

That’s the root of the problem. Were I to create a Translate trait that uses a translation field, it would put the responsibility on the programer (me) to make sure the struct which is having this trait being implemented for has the necessary translation field. Considering it’s just me that’s working on this project, that’s fine. However, it feels better (to me) to push that responsibility to the compiler.

I just don’t know what the best way of doing that is. Hence my question!


#5

Seems so obvious! Thank you very much for your answer, this is perfect.


#6

If you’re doing something like this, and you don’t want to give access to an internal structure, using macros to generate implementations is also something generally done.

Adding a trait and a method to gain access to internal data does work wonderfully if giving access to internal data is acceptable, but something like the following works well if keeping private data private is more needed:

macro_rules! impl_trans {
    ($name:ident) => (
        impl $name {
           fn translate(&self, input: char) -> char {
               *self.translation.get(&input).unwrap()
           }
        }
    )
}

struct Foo {
    translation: HashMap<char, char>,
}

impl_trans!(Foo);

playpen: https://play.rust-lang.org/?gist=53fa6a04d526a23d4144&version=stable


#7

Yeah, this is good as well!

But would be nice to tell the macro where’s the path of the field. http://is.gd/7Yp5I0

All in all, I still prefer the trait version, because the way we can treat structures in generic code. Let’s think you’ve got some function that treats with data that needs to implement Translation:

fn do_stuff<T>(data: T)
where T: Translation {
   println!("{}", data.translate('x'))
}

How could you know whether the T can be translated if you just implement a simple method like you did using macros?

So, the best way to solve this (IMO) is making the trait and a macro that implements the trait.