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:
- FromStr
- From (and Into, which we get for free if implementing
From
) - TryFrom (and TryInto, which we similarly get for free from
TryFrom
) - Everything in ops like
Index
andIndexMut
- Hash
- AsRef
- Default
- Display
- Debug
- Error
- Iterator
- IntoIterator and FromIterator
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.