Search code examples
rusttraitsconceptual

Why use traits?


What typical scenario would traits be used for in a Rust program? I have tried to wrap my head around them, but I still don't have a clear concept as to why one would decide to use them. I understand the syntax from reading about them from here.

How would one explain them to someone as if they were 5?


Solution

  • One way to conceptualize traits is to think of them as 'behaviors you want available to, and to operate over, a particular chunk of state'. So, if you have some struct that contains state, and you want to do something with it, you would write a trait for it.

    There are two primary usages:

    • You are dealing with a struct in your code, and you would like that struct to know how perform some behavior. You can call the trait-defined behavior on the struct.
    • You would like to pass the struct to some other code (yours or a third party) that might not know anything about the struct itself, but want to perform some set of functions on it, trusting that the struct knows what to do in those cases.

    In the first case, it allows you to do things like this:

    struct Article {
      body: String
    }
    
    trait Saveable {
      fn save(&self) -> ();
    }
    
    impl Saveable for Article {
      fn save(&self) -> () {
        ...  // All the code you need to run to save the Article object
      }
    } 
    
    // A function called by your UX
    fn handle_article_update(article: Article) -> () {
      ...
      article.save()  // Call the save functionality
    }
    

    The second case is arguably more interesting, though. Let's say you - or more probably a third party - has a function defined like this:

    fn save_object(obj: Saveable) -> () {
      ...
      obj.save()
    }
    
    struct Person {
      name: String
    }
    
    impl Saveable for Person {
      fn save(&self) -> () {
        ... // Code needed to save a Person object, could be different from that needed for an Article object
      }
    }
    
    ...
    // Note that we are using the same function to save both of these, despite being different underlying Structs
    save_object(article)
    save_object(person)
    

    What this means is that the save_object function does not need to know anything about your custom Article struct in order to call save on it. It can simply refer to your implementation of that method in order to do so. In this way you can write custom objects that third party or generic library functions are able to act upon, because the Trait has defined a set of behaviors with a contract that the code can rely on safely.

    Then the question of, 'when do you want to use a Trait' can be answered by saying: whenever you want to use behavior defined on a struct without needing to know everything about the struct - just that it has that behavior. So, in the above, you might also have an 'edit' functionality attached to Article, but not to Person. You don't want to have to change save_object to account for this, or to even care. All save_object needs to know is that the functions defined in the Saveable trait are implemented - it doesn't need to know anything else about the object to function equally well.

    Another way to phrase this is to say, 'Use a trait when you want to pass an object based on what it can do, not what it is.'