Rust traits are more powerful than I thought

I have been learning the Rust programming language in my spare time for a little over 2 years now. My first mental model of traits consisted of a contract for types that would implement the trait, similar to Go’s interfaces. It wasn’t until recently when I decided to explore traits during Advent of Code that I started to realize how integrated these contracts are into the language and standard library. Using traits really helped clean up the code I was writing.

Hammers and nails

[If all you have is a hammer, everything looks like a nail]({% post_url 2021-01-24-hammer %}), or, you can’t really use traits unless you know about them. For example, the FromStr trait is used for str::parse.

use std::str::FromStr;

#[derive(Debug, PartialEq)]
enum Foo {
    Bar,
    Baz,
}

impl FromStr for Foo {
    type Err = &'static str;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "bar" => Ok(Foo::Bar),
            "baz" => Ok(Foo::Baz),
            _ => Err("not bar or baz"),
        }
    }
}

fn main() {
    let f: Foo = "bar".parse().unwrap();
    assert_eq!(f, Foo::Bar);
}

As such, here’s a laundry list of traits that I’m currently finding useful:

Some of these are derivable, which makes it very easy to get functionality for free.

Traits and generics

Traits are tightly integrated with generic code via trait bounds. In the example above, str::parse is generic over any type that implements the FromStr trait. Similarly, types that implement IntoIterator work with Rust’s for loop syntax.

Something that clicked recently for me is using trait bounds to make my library API friendlier to more types. For example, I might define a function with an AsRef<str> trait boundary to allow any type that can return a &str to be used.

fn print_it<S: AsRef<str>>(s: S) {
    println!("{}", s.as_ref());
}

// usage can be..
print_it("foo");
print_it(String::from("foo"));

Perhaps the Display trait would have been more suitable here, but this simple example demonstrates the flexibility in function definitions. This aspect of trait boundaries is very similar to Go interfaces.

I think a very useful trait bound is Into<T>, which allows a function to take any type so long as it can be converted into T.

struct Foo {
    data: String,
}

impl Foo {
    fn new<S: Into<String>>(s: S) -> Self {
        Self { data: s.into() }
    }
}

fn main() {
    let x = Foo::new("bar");
    let y = Foo::new(String::from("baz"));
}

Conclusion

Learning more about the trait ecosystem has helped me write cleaner and more idiomatic Rust code. I have found it’s more than just a tool of the language, but its deep integration in the standard library helps tremendously with code composability and integration.