Difficulty understanding utility of traits

I have a small little example that I am trying to understand. As far as I understand it a trait lets me to call certain functions on an object for which it is implemented for. Let say I have an object:

struct XX {
    x:String
}

impl XX {
    fn new(S: String)->Self{
        XX{
            x:S
        }
    }
}

and now i want to add:

pub trait Y {
    fn get(&self);
}

impl Y for XX{
    fn get(&self){
        println!("{}", self.x)
    }
}

fn main(){
   let a = XX::new("bla".to_string());
   a.get()
}

So now my question is why would I ever do that ? Why not just make it the core object function since i need the construct the object anyway... Where is the gain? (Point is, I know how to write them but have no clue why would i use them ) Any help?

PS
is is possible to abstract the object creation through them or something?

Traits allow code to be generic. E.g.:

fn foo<T: Y>(x: T) {
    x.get()
}

For instance, how do you suppose println! works?

When you called this, this expanded to a call to the trait method std::fmt::Display::fmt. Thanks to this, you are not just limited to printing Strings, but rather you can print any type that implements Display.

1 Like

If you have ever programmed in Java or C#, Traits would have a similar utility to what they call Interfaces (although traits are more composable/powerful).

If you only ever plan on having a single implementation of something, then you are right, traits would not really be too helpful.

However, imagine for example, something like a UI graphics library, where you would have different/unique components, but they would all share the same ability to be displayed on the UI.

Here is a semi convoluted example below that hopefully can illustrate the value:

fn main() {
    let checkbox =  Checkbox { is_checked: true };
    let dropdown = DropdownMenu { choices: vec!["Do".to_string(), "Rei".to_string()] };

    display_on_ui(checkbox);
    display_on_ui(dropdown);
}


fn display_on_ui<T: UiComponent>(component: T) {
    println!("About to display a component on the UI!");
    component.render();
}



trait UiComponent {
    fn render(&self);
}

struct Checkbox {
    is_checked: bool
}

struct DropdownMenu {
    choices: Vec<String>
}


impl UiComponent for Checkbox {
    fn render(&self) {
        if self.is_checked {
            println!("CHECKED");
        } else {
            println!("UNCHECKED");
        }
    }
}

impl UiComponent for DropdownMenu {
    fn render(&self) {
        for choice in &self.choices {
            println!("{}", choice);
        }
    }
}
4 Likes

For example, your code that takes s: String won't let you call new("foo"), because "foo" is &str, not String. But if you use a trait:

    fn new(s: impl Into<String>)-> Self{
        Self {
            x: s.into(),
        }
    }

then it will work with &str, String, Cow<str>, Box<str>, &String, because they all implement .into() function of the Into trait.

2 Likes

I like to think of it like this:

You have to consider the situation backwards...

Normally we think about the code we are writing, about some object we are creating and what methods it may require. What functionality we have to expose through those methods.

But if we look at it from the other end of the telescope somebody, somewhere, is creating some code X and wants to define what methods other objects should have so that X can make use of them. Even other objects that don't even exist yet. They want to specify that if you want your object to be usable by X then it must provide some methods that X likes.

That definition of what those unknown/unwritten objects should provide is a "trait."

In other worlds an "interface".

So, now that you know what X needs, because it has specified the trait. Then you can provide an implementation of that trait for your new object.

Someone correct me if I have the wrong end of the stick here.

7 Likes

Here's one of my favorites. Let's say you want an input of bytes, whether those bytes come from a &str, String, Vec, BytesMut, etc, doesn't matter:

fn get_bytes_from<B: AsRef<[u8]>>(input: B) -> &[u8] {
    input.as_ref()
}

... Or, maybe you want a &str instead:

fn get_bytes_from<B: AsRef<str>>(input: B) -> &str {
    input.as_ref()
}

If you want to mutate the data, then use AsMut<[u8]> instead:

fn get_bytes_from<B: AsMut<[u8]>>(input: B) -> &mut [u8] {
    let mut data_to_edit = input.as_mut();
    //do processing...
    data_to_edit
}
1 Like

Shouldn't the argument be impl Into<String>? AFAIK, what you've written is the deprecated syntax for trait objects.

2 Likes

yeah, that was a typo

1 Like

Thank you so much for the examples and elaborattions. In conclusion one would say that the traits can be used for :

a) different implementations of the same method for different objects (e.g.)
b) as a "parser" for different types passed or retrieved from a given object

Which is brilliant in case of large code base. Which brings another question: Why not implement all object methods as traits (from constructor all the way to a destructor) sincenecessity this opens the object for further re-implementations down the road without the need to re-write the whole code base (or mess with it), right?

1 Like

I'm just guessing here, but I think the answer comes down to two things:

  1. Compile times. If the compiler has to resolve all those imports and try to optimize all the dynamic dispatch calls where it can be statically dispatched, compile times will shoot through the roof. Compile times are already one of the reasons people give which is a pain about working with Rust, so this is a no-go.
  2. Alternatively, or as well, this will make code slower, because runtime dispatch will be necessary either to keep compile times in check or just inevitably with some designs.

This is my guess, anyway.

1 Like

You'll only suffer the cost of dynamic dispatch when you use trait objects (dyn Trait). Generic types (T: Trait) get resolved at compile time, which means you pay the cost in longer compilation times and larger binaries, but not in program execution speed.

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.