Advanced Traits
Associated Types
-
This is how you can create an associate type inside a trait
#![allow(unused)] fn main() { pub trait Iterator { type Item; // You see, this is the associated type fn next(&mut self) -> Option<Self::Item>; // Now, this trait can use this type in it's signatures } } -
Here's an implementation of this trait on one of our types Counter:
#![allow(unused)] fn main() { impl Iterator for Counter { type Item = u32; fn next(&mut self) -> Option<Self::Item> { // --snip-- } -
Now, you might be wondering, can't we do something like this with using traits with generics?
#![allow(unused)] fn main() { pub trait Iterator<T> { fn next(&mut self) -> Option<T>; } } -
Here are the differences between Associated Types and Traits with generics?
Traits with Associated Types Traits with Generics There will be only one implementation for a type. There can be multiple implementations for a type, using individual concrete type. For Example, impl Iterator for CounterFor Example, impl Iterator<String> for Counter, and many more.Using functions of these traits will not require you to provide type annotations. You'll need to provide type annotations to provide which iteration to use.
Operator Overloading
-
This is how you can add two types by using the
+operator// This library `std::ops` contains all the overloadable operators use std::ops::Add; #[derive(Debug, Copy, Clone, PartialEq)] struct Point { x: i32, y: i32, } impl Add for Point { type Output = Point; // associated type will restrict the number of implementations fn add(self, other: Point) -> Point { // This fn will decide how the `+` will behave for the type Point Point { x: self.x + other.x, y: self.y + other.y, } } } fn main() { assert_eq!( Point { x: 1, y: 0 } + Point { x: 2, y: 3 }, Point { x: 3, y: 3 } ); } -
This is how the trait
Addis defined in the Rust's librarystd::ops#![allow(unused)] fn main() { // This syntax `<Rhs=Self>` is called "default parameters" // In case, someone implementing this trait doesn't define the type // then the type defined in default parameter will be used everywhere in this trait trait Add<Rhs=Self> { type Output; fn add(self, rhs: Rhs) -> Self::Output; // The "default parameter" is used here inside argument `rhs` } } -
You can also customize
Rhs, that'll mean you'll be adding a value of different type to the main type#![allow(unused)] fn main() { use std::ops::Add; struct Millimeters(u32); struct Meters(u32); impl Add<Meters> for Millimeters { // Modified Rhs to Meters, otherwise the default will be Self (or Millimeters) type Output = Millimeters; fn add(self, other: Meters) -> Millimeters { Millimeters(self.0 + (other.0 * 1000)) } } } -
You’ll use default type parameters in two main ways:
- To extend a type without breaking existing code
- To allow customization in specific cases most users won’t need
Function with same name in multiple traits
-
Following things are allowed:
- Multiple traits to have functions with same name
- A type implementing all these traits
-
Here's an implementation:
trait Pilot { fn fly(&self); } trait Wizard { fn fly(&self); } struct Human; impl Pilot for Human { fn fly(&self) { println!("This is your captain speaking."); } } impl Wizard for Human { fn fly(&self) { println!("Up!"); } } impl Human { fn fly(&self) { println!("*waving arms furiously*"); } } fn main() { let person = Human; Pilot::fly(&person); // You'll have to pass the reference to the person, because it takes self as an argument Wizard::fly(&person); // if we had two types that both implement one trait, this self would recognize the correct type person.fly(); // The direct implementation will get called first, instead you can also use Human::fly(&person) } -
Let's see what happens if the associated function of a struct and a trait has same name
trait Animal { // Notice, there is no self passed inside, // multiple types can implement this trait, // Calling this function won't be direct, // as the trait won't be able to infer // which type's implementation to call fn baby_name() -> String; } struct Dog; impl Dog { // This is an associated function, which can be called // using `Dog::baby_name()`, just like any other // associated fn without self fn baby_name() -> String { String::from("Spot") } } impl Animal for Dog { fn baby_name() -> String { String::from("puppy") } } fn main() { println!("A baby dog is called a {}", Dog::baby_name()); // The direct implementation will get called println!("A baby dog is called a {}", Animal::baby_name()); // Won't work, this associated fn don't has self, remember? It can't infer the type println!("A baby dog is called a {}", <Dog as Animal>::baby_name()); // We need to tell Rust that we want to use the implementation of Animal for Dog } -
This is the fully qualified syntax, used for all associated functions (including methods)
#![allow(unused)] fn main() { <Type as Trait>::function(receiver_if_method, next_arg, ...); // You don't need to provide all the information that Rust can infer }
Supertraits
-
Sometimes, you might need one trait to use another trait’s functionality.
-
The traits you would be using to build your own trait is called Supertrait.
-
One important thing is that, now your trait can only be implemented on the types that already implements the Supertrait.
-
Here's an implementation. This
OutlinePrintcan only be implemented on any trait that implementsDisplay.#![allow(unused)] fn main() { use std::fmt; trait OutlinePrint: fmt::Display { // It is also like bounding a trait with another trait fn outline_print(&self) { let output = self.to_string(); // It works only because all the types already implements `Display` let len = output.len(); println!("{}", "*".repeat(len + 4)); println!("*{}*", " ".repeat(len + 2)); println!("* {} *", output); println!("*{}*", " ".repeat(len + 2)); println!("{}", "*".repeat(len + 4)); } } } -
For reference, you can also implement
fmt::Displayon your type, like this:#![allow(unused)] fn main() { struct Point { x: i32, y: i32, } use std::fmt; impl fmt::Display for Point { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "({}, {})", self.x, self.y) } } } -
Now, we know that
PointimplementsDisplay, now we can implementOutlinePrintlike this:#![allow(unused)] fn main() { impl OutlinePrint for Point {} } -
The output will look like this for the fn
outline_print********** * * * (1, 3) * * * **********
Newtype Pattern
The Orphan Rule states that we’re allowed to implement a trait on a type as long as either the trait or the type are local to our crate.
-
The newtype pattern , helps us to get around this restriction.
-
All we need to do is to create a new type in a tuple struct.
-
Let's say, we want to implement
DisplayonVec<T>. Both of them are not local to our code, thereby orphan rule will prevent us from implementing it. -
Now, we can use this newtype pattern for getting a workaround. What we'll be building is a wrapper that will make the
Vec<T>local to us.use std::fmt; struct Wrapper(Vec<String>); // A new struct, which is just a wrapper for Vec<String> impl fmt::Display for Wrapper { // Now, we can implement Display on Wrapper, but we can't do directly on Vec<T> fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "[{}]", self.0.join(", ")) // self.0 is used to take the Vector out } } fn main() { // The only disadvantage is that, we'll need to create // a vector inside a wrapper to use the Display trait let w = Wrapper(vec![String::from("hello"), String::from("world")]); println!("w = {}", w); } -
If we wanted the new type to have every method the inner type has, implementing the Deref trait on the Wrapper to return the inner type would be a solution.
-
If we don’t want the Wrapper type to have all the methods of the inner type — for example, to restrict the Wrapper type’s behavior — we would have to implement just the methods we do want manually.