This is it, the final blog. It's long and arduous, so take some time to read it. Don't worry, I'll still be posting blogs, but probably not on the features of the Rust language - I think we all need a break from that for now.
Yesterday's questions answered
No questions to answer
Today's open questions
*No open questions
Associated types vs generics
We've already looked at how generics are a way to improve reusability of functions or traits in Rust. Associated types offer a different approach to reusability. There may be instances where a certain trait need only be implemented once for a given type. This is when an associate type definition on a trait may be more useful. Consider this example from the Rust book:
use std::ops::Add;
#[derive(Debug, Copy, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}
impl Add for Point {
type Output = Point;
fn add(self, other: Point) -> 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 }
);
}
Note how in the implementation of Add
we declare the type Output
. What's we're actually doing is defining the associate type of the Add
trait. Without this, our code won't compile. The associate type serves as a contract in this function. As per the trait definition, our associated type serves as the return type of our add
function. Here's the trait declaration:
trait Add<Rhs=Self> {
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
}
What's interesting about the Add
trait is that it also has default associate type. This is represented by the Rhs
alias input argument.
Default types in this context provide flexibility to library authors: often most users of a library will use a trait in one way. This typical usage pattern can be expressed through the default type of a trait. By making this a parameter, however, you can give power users the chance to use the library in a way you may not have foreseen.
Fully qualified syntax for duplicated method and associated functions
Rust, like Python, provides no limitations on defining multiple methods of the same name on an object. That's probably because no programming language intended on its users to do that (except when overloading methods, of course).
In Rust, however, there is actually a relatively common situation where a struct
may have multiple methods or associated functions of the same name. When implementing a trait
from an external library, we may be forced to use a method name that we have already defined.
Syntax for method calls
Remember back to when we talked about multithreading, we briefly talked about the Send
(and Sync
) trait. Imagine you want code that implements not only Send
from the standard library, but also from a third party library that instantiates threads in a different way. Suddenly you have two versions of the send
method.
When calling these methods, we have to use fully qualified syntax. This consists of the trait
and the method name
:
let thread_struct = MyCustomThread::new();
StdLibrarySendTrait::send(&thread_struct);
ExternalLibrarySendTrait::send(&thread_struct);
Syntax for associated functions
As a struct
method contains a reference to the struct
as its first argument, when we call the method using fully qualified syntax, Rust knows which implementation block is being called as it knows the type of self
:
impl ExternalLibrarySendTrait for MyCustomThread {
fn send(&self) {
// Do something
}
}
impl ExternalLibrarySendTrait for SomeOtherType {
fn send(&self) {
// Do something
}
}
ExternalLibrarySendTrait::send(&thread_struct);
This is different for associate functions which don't take self
:
trait Animal {
fn baby_name() -> String;
}
struct Dog;
struct Cat;
impl Dog {
fn baby_name() -> String {
String::from("Spot")
}
}
impl Animal for Dog {
fn baby_name() -> String {
String::from("puppy")
}
}
impl Animal for Cat {
fn baby_name() -> String {
String::from("kitten")
}
}
Dog::baby_name() // No problems
Animal::baby_name() // Cat or Dog?
You can see here how the fully qualified syntax for methods isn't enough here (also, the code won't compile). That's actually because we didn't use fully fully qualified syntax. Before we could skip the type aliasing because it's implicit in the &self
argument. Here is how we use fully qualified syntax which will get the above code to compile:
<Dog as Animal>::baby_name() // A puppy
<Cat as Animal>::baby_name() // A kitten
We could use this syntax for methods, too, but it's overly verbose.
Supertraits: declaring trait dependencies
When defining a trait
we can declare an optional supertrait
which must be implemented if that trait
is to work as expected. Using a struct
that does not implement the supertrait
will cause you code to not compile:
trait PrettyPrint: fmt::Display {
// Do something
}
This code declares a trait
that can only be implemented on structs
that implement Display
.
Supertraits can help reduce code duplication at the cost of increasing code coupling and complexity.
Type wizardry
Until now we've barely touched on the type
keyword. This keyword is used to declare type aliases:
type Kilometer = i32;
What's important to understand is that Kilometer
is still an i32
:
let a: Kilometer = 1;
let b = 1;
asserteq!(a + b, 2);
Often we see the newtype pattern in Rust. This is popular because of Rust's trait
implementation rule:
A trait can only be implemented for a type if the trait or the type is local to your code.
We can implement the above code using the new type pattern:
struct Kilometer(i32);
let a = Kilometer(1);
let b = 1;
asserteq!(a + b, 2);
This code won't compile, because the Kilometer
type doesn't implement the Add
trait. i32
does implement the trait, which is is proof that the type
alias really is an alias around i32
.
When to alias and newtype?
Aliasing makes sense when:
- You have a long type (nested Result or Options, for example) that can be shortened
- A composite type where one part is always the same but the other should be treated as a generic
- Giving a meaningful name to an existing type (like
i32
) can make your code easier to read.
Newtyping makes sense when:
- You need to implement external traits
- You want to use a public API to control access to internal code
The type you never knew about
Functions that return !
return the so-called never type. The never type is a Rust alias for the empty type. The point of a never type is that it expresses a function that never returns.
Functions that do this yield a result that can be coerced into any other type. This is useful when using match
statements, as these must always return a single type. Keywords and macros such as continue
, panic!
and loop
return the never type.
Sized types
A short note on sized types. Generic functions by default can only take sized types. A type is sized when its size is known at compile time. Sized types by default implement the Sized
trait.
If you want to use a dynamically sized type with generics you need to use the ?Trait
notation:
fn generic<T: ?Sized>(t: &T) {}
Note that we also expect to receive a reference to the dynamically sized type as these are also stored on the heap behind a pointer.
Passing and return functions
Functions can easily passed to functions using the fn
type. This is different from the trait
restraints used to indicate that a closure
should be passed as an argument. As something of type fn
implements all three Fn
traits, you can always pass functions where closures are accepted.
If you want to return a closure
from a function, you have to wrap it in a Box
as a closure
's size is determined at runtime.
Last but not least: Macros
The most advanced Rust feature is the macro
. These are functions that use Rust code to generate more Rust code. This pattern is generally known as metaprogramming.
Rust has different kinds of macros. This have different implementation detail, and each type is suited to a different use case.
macro_rules!
macros
The most common type of macro in Rust is the declarative macro. This is created by using the macro_rules!
macro. In a declarative macro, you can match Rust source code to patterns and generate different kind of code based on which kind of pattern is matched. This makes it possible for these kind of macros to take an undefined number of arguments.
Procedural macros
The remaining three types of macros are procedural. These macros must take Rust source code as an input and return it.
The first of these is the derive macro. To create a derive macro, you need to derive from the proc_macro_derive
macro of the standard library. Derive macros only work
on structs
and enums
.
Derive macros have a number of restrictions around packaging. When developing such macros, developers are encouraged to include them as a dependency of the main library being developed. Then the macro can simply be reexported as part of the main library for users to import.
Attribute macros are similar to derive macros, only that they are more flexible as they can be used for functions and other data types as well. They derive from the proc_macro_attribute
macro.
Finally, you have function-like macros. Though procedural in nature, these macros can be used inline like a macro_rules!
macro and can take an unspecified number of arguments. They derive from proc_macro
.
Top comments (0)