Welcome to the first letter of The Monthly Oxide that bundles some Rust knowledge, articles, and projects for you to peruse and use. This month we’ll dive into Traits, probably one of my favorite things in Rust, and often the source of pain and confusion for those new to Rust as well as a barrier for intermediate Rustaceans. I’ve also included some articles that I found interesting this month that you might find worth your time as well as some projects I saw that were pretty nifty. With that let’s dig Into<Traits>!
Understanding and Using Traits
Traits are a way to do one of two things with a type:
Describe some behavior that the type can do (e.g. Iterator, From, Into, and Deref for instance)
Act as a marker for some kind of behavior (e.g. Send and Sync)
I think it’s important to make a distinction between the two because even though they’re both traits, they’re used differently and so how we approach learning about them will be slightly different. We won’t get into anything super crazy in this issue, but I want you to have a solid understanding of it that you can experiment and try new things and this foundation can be expanded upon at a later date.
Why traits though?
I think the best way to understand why and how is to see traits in action. Let’s make a small function to record dog noises:
pub fn record_dog_noise(dog: &Dog) -> Result<Sound, Mp4EncodeError> {
let bark = dog.bark();
mp4_encode(bark)
}
This is nice we can record a dog’s bark, but what if we wanted to record a Cat’s meow? Okay we add a new function then:
pub fn record_cat_noise(cat: &Cat) -> Result<Sound, Mp4EncodeError> {
let meow = cat.meow();
mp4_encode(meow)
}
This is a bit repetitive and if we had to add a function for every pet things get real tedious and annoying. Plus updating all the functions to add extra logic makes it even more annoying to maintain and if you forget to update one of many functions? That’ll be a fun bug to find. Could we do something to make it easier? Generics! Let’s rewrite the function with them:
pub fn record_pet_noise<P>(pet: &P) -> Result<Sound, Mp4EncodeError> {
let noise = pet.make_noise(); // This won't work
mp4_encode(noise)
}
So we have this generic type P
which could be anything, but we can’t do anything with it. We know it’s a type of some sort, but Rust doesn’t know what else it can do! This is where we need traits. This will let us say “We want a function to record pet noises, but we only accept types that are an animal that can make a noise.” Let’s define a trait for that then:
pub struct Dog;
pub struct Cat;
// Assume `fn bark` is in impl Dog and `fn meow` is in impl Cat
// which we are omitting here for brevity
pub trait Animal {
fn make_noise(&self) -> Mp4EncodableSound;
}
impl Animal for Dog {
fn make_noise(&self) -> Mp4EncodableSound {
self.bark()
}
}
impl Animal for Cat {
fn make_noise(&self) -> Mp4EncodableSound {
self.meow()
}
}
We’ve created a trait Animal
that says that the type implementing this needs to implement the function make_noise
which returns a type that can be encoded into an mp4 format. You can see that we’ve done this with both types. Lets go back to our original implementation then and spruce it up:
pub fn record_pet_noise<P: Animal>(pet: &P) -> Result<Sound, Mp4EncodeError> {
let noise = pet.make_noise(); // This works now
mp4_encode(noise)
}
// This is also a valid way to write the function header
// and does the same thing:
// pub fn record_pet_noise<P>(pet: &P)
// -> Result<Sound, Mp4EncodeError> where P: Animal {
// Generally speaking where clauses are better with lots of
// generics for your function readability wise
Now that Rust knows the type has to implement Animal
it knows that it has a make_noise
function that it can call for that type. It then creates two versions in the final binary of the same function, one for Cat
and one for Dog.
If we want to add a new pet that can work with this function all we have to do is implement Animal
for it. It’s important to note that the compiler will only make a copy of the function for a type if you call it on that type. Making extra code for no reason is just inefficient! That’s pretty cool, but remember we said that we only want to do this for pets not all animals. How can we make it so that we only accept pets? Let’s use marker traits:
pub trait Pet {}
impl Pet for Dog {}
impl Pet for Cat {}
Okay so far so good so now we know that these are pets not just animals. We call these marker traits because they have no functions for you to implement, but they allow you to “mark” the type with the trait. How do we tell our function to utilize this functionality then? Let’s take a look:
fn record_pet_noise<P: Animal + Pet>(pet: &P) -> Result<Sound, Mp4EncodeError> {
let noise = pet.make_noise();
mp4_encode(noise)
}
We added another trait boundary to P
which says “We accept a type only if it implements the traits Animal
and Pet.
” Pet
is a marker trait. It doesn’t do anything, but it restricts what types are acceptable!
This is a more contrived example, but I hope it illustrates why you might want them as well how to use them. There’s way more to dig into about traits, such as impl Trait/dyn Trait
or commonly used traits in Rust worth knowing, but I’ll hold off for now or else this letter will get too long. In summary:
Traits describe behaviors a type can do or mark some property of it
Traits are often used for generics (there are places you might use them in a non generic context like calling
into
to convert a type)Traits give generic types a set of behavior they can do inside a generic function
Traits can restrict which types are used in a generic function
Traits make generics possible and help with code maintenance and reusability for little cost to the maintainer
In future letters we can delve a bit more into more complicated uses of traits!
Article Picks of the Month
Here are the articles I read this month that I think are worth your time whether to learn or just for news:
Already familiar with traits and what they can do? Are you a more advanced Rust user? This cool usage of them, Inlineable Dyn Extension Traits, is worth understanding for niche use cases. If you can’t understand it now that’s okay we’ll get there eventually!
Pants last year switched over to Rust and the gains were great for a variety of reasons. A lot to learn from other’s experience of using it in production.
Google announced they would further collaborate with the ISRG to help mitigate memory safety issues in open source projects
If you haven’t seen the news Rust has a foundation now! It’s exciting to see almost 6 years after 1.0. When I first wrote Rust then there was a feeling of “Will this even go big?” and now we have a foundation being supported by some pretty big heavy hitters in the industry and it’s balanced out by having a lot of people from the project on the board. This way it’s not completely pay to play. Take the time to read it over if you haven’t it’s great news.
Projects of the Month
Camino - Paths that assume UTF-8 which for a lot of the people is the case. Useful when you know that your paths are valid UTF-8 and you don’t want to have to keep calling things like
display()
all the time just to print them out to the console!MORPH - An incredibly cool art project with all the synthesis and firmware written in Rust. It looks so organic and real it’s awesome
subway - A skip list implementation in Rust! I’m a sucker for data structures that have niche use cases but make everything better.
If you liked what you’ve read and aren’t subscribed consider doing so now!