Learning Rust: Part 5— Traits, Generics and Closures
We continue our learning journey looking at these more advanced topics.

Overview
Rust Series
Part 1 — Basic Concepts
Part 2 — Memory
Part 3 — Flow Control and Functions
Part 4 — Option / Result and Collections
Part 5 — Traits, Generics and Closures (this article)
Introduction
This portion of our Learning Rust series is going to cover a lot of important information. We are starting to get into more advanced subjects, but we will do it in an organized and step-wise manner to make it digestible. Before we get into our core subject areas, let’s quickly go into an area we have not discussed yet, constant and static variables.
Constant and Static Variables
Static variables are closely related to constants which we have seen earlier. Constants are great for immutable values that will be used within your program and you want to give a descriptive name. These is good coding practice and makes your code easier to understand.
Here is an example of a const declaration.
const NUMBER_OF_LOOPS: i32 = 32;
fn main() {
let loop_count: i32;
for loop_count in 0..NUMBER_OF_LOOPS {
println!("We are on loop count {}", loop_count);
}
}Static variable are not like constants in two important areas. First they are global values and second can be made mutable if desired. Note that since Rust frowns on use of mutable globals, they must be enclosed in an unsafe block if done this way.
While immutable statics can be used everywhere, mutable statics can only be used (even if just for read access) inside unsafe blocks.
Let’s look at an example here of using a mutable and immutable static.
static mut TOTAL_USERS: u32 = 4;
static ANOTHER_VAL: u8 = 9;
fn main() {
unsafe {
println!("Total Users is {}", TOTAL_USERS); // mutable, in unsafe block
TOTAL_USERS = 42;
println!("Total Users is now {}", TOTAL_USERS);
}
println!("Some other static val is {}", ANOTHER_VAL); // not mutable, can be used anywhere
// e.g. a global constant
}Generally, if you don't need the memory location of your global values for anything, you should prefer using consts.
Traits
Traits are similar to interfaces in object-oriented languages. I am just going to get that out right away, in case you’re coming from Java or C# background.
Traits define a set methods that can be implemented by a type. They describe an abstract interface that types can implement and are a way to define shared behavior. If the function in a trait defines a body (has its own implementation), this is called a default. This can be overridden in types that implement the trait, but if not will provide the behavior as defined in the trait. Hence called default.
As you explore and learn more about traits you will see they provide some form of what are possibly object oriented concepts such as inheritance and polymorphism.
Traits are, I think, best thought of as contracts and promises at the same time.
Here is an example of a trait. This would be used to provide an implementation with the ability to track their name and age.
trait WithNameAndAge {
fn new(name: String, age: u8) -> Self;
fn get_name(&self) -> &str;
fn get_age(&self) -> u8;
fn print(&self) {
println!("My name is {} and age is {}", self.get_name(), self.get_age())
}
}Note get_name and get_age provide no implementation. However print does provide an implementation and therefore is a default function.
Since traits define methods that will be executed when attached to a type, they can be thought of as a group of methods defined for an unknown type, Self. All traits define this implicit type parameter Self. This refers to "the type that is implementing this interface".
The &self argument is syntactic sugar for self : &Self. We have seen this before in a previous article when defining methods.
To try to explain further, if we were to declare a struct Person and implement the WithAge trait, then the Self type that is returned by the new method would be Person.
Rust supports relationships between traits. A supertrait is a superset of another trait. Naturally, a trait that has a supertrait is a subtrait itself.
Now once you have a trait defined, you will implement it with a type. To implement a trait, declare an impl block for the type you want to implement the trait for.
The syntax is impl <trait> for <type> . You will need to implement all the methods defined in the trait that do not have default implementations.
Types can implement more than one trait.
A complete example of declaring and then implementing a trait using the example above.
// Define a trait
trait WithNameAndAge {
fn new(name: String, age: u8) -> Self;
fn get_name(&self) -> &str;
fn get_age(&self) -> u8;
fn print(&self) {
println!("My name is {} and age is {}", self.get_name(), self.get_age())
}
}
// define a type, Person
struct Person {
name: String,
age: u8,
}
// implement trait to my type - Person
impl WithNameAndAge for Person {
fn new(name_of: String, age_of: u8) -> Self {
Person {name: name_of, age: age_of}
}
// implement functions
fn get_name(&self) -> &str {
&self.name
}
fn get_age(&self) -> u8 {
self.age
}
}
fn main() {
let p = Person::new("Brian".to_owned(), 42);
p.print() // call default trait function
}This prints the following
My name is Brian and age is 42Marker Traits
Traits usually exist for the reason to share behavior, Rust also has something called marker traits. Marker traits do not have any behavior implemented, but are used to give the compiler certain guarantees by being assigned to an implementation. In Java you may know a similar idea from the interfaces Serializable or Cloneable.
There is more to a trait such as dynamic dispatch and trait bounds we will see more of as we proceed further. For now that is a decent introduction to the topic of traits.
Generics
With generics you can write code that can be used with multiple data types without having to rewrite the same code for each. In our previous article in our series we looked at Option and Result. If you remember they have a definition of T for Option and T and E for Result as the parameter types. These were example of Generics.
Typical, the variables T, U, and V are used as placeholder values and E is often used to denote an error type. For generics, you use angle brackets with the type inside, like this:
Below if an example of a generic function.
fn do_something_with_an_item<T>(item: T) -> T {
println!("In the function returning my item.");
item
}
fn main() {
let item = do_something_with_an_item(5);
println!("Here is your item returned from the function {:?}.", item);
let item = do_something_with_an_item("Brian".to_owned());
println!("Here is your item returned from the function {:?}.", item);
let item = do_something_with_an_item(9.8);
println!("Here is your item returned from the function {:?}.", item);
}My generic function do_something_with_an_item simply prints something and returns the same item, in this example. Notice the syntax, do_something_with_an_item<T>(item: T) -> T which takes an unknown type and returns that type.
In our main we then call this with 3 different types; integer, string and float. This prints the following.
In the function returning my item.
Here is your item returned from the function 5.
In the function returning my item.
Here is your item returned from the function "Brian".
In the function returning my item.
Here is your item returned from the function 9.8.Advantageous is the fact I did not need to write three different variants of do_something_with_an_item for the different types I needed to process.
Also worth noting in our example above. If do_something_with_an_item has tried to print item the compiler would have complained. This is not allowed. For instance defining our generic function as follows.
fn do_something_with_an_item<T>(item: T) -> T {
println!("In the function returning my item {:?}.", item);
item
}This would result in the following error.
|
3 | println!("In the function returning my item {:?}.", item);
| ^^^^ `T` cannot be formatted using `{:?}` because it doesn't implement `Debug`
|
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider restricting type parameter `T`
|
2 | fn do_something_with_an_item<T: std::fmt::Debug>(item: T) -> T {
| +++++++++++++++++Because there is no guarantee the type passed in (T) has implemented the Debug trait. To get around this you can tell your generic function that I guarantee all types passed in will implement Debug. This is done as follows and now our code compiles. Notice the T:Debug portion of the declaration.
use std::fmt::Debug;
fn do_something_with_an_item<T:Debug>(item: T) -> T {
println!("In the function returning my item {:?}.", item);
item
}
fn main() {
let item = do_something_with_an_item(5);
println!("Here is your item returned from the function {:?}.", item);
let item = do_something_with_an_item("Brian".to_owned());
println!("Here is your item returned from the function {:?}.", item);
let item = do_something_with_an_item(9.8);
println!("Here is your item returned from the function {:?}.", item);
}The key is the addition of
Trait Bounds
When you need to specify what types will use a generic function and what capability they will have you can add trait bounds (necessary traits) to the signature of the generic.
Look at the following example where we find the smallest item in a list.
fn smallest<T>(list: &[T]) -> T {
let mut smallest = list[0];
for &item in list.iter() {
if item < smallest {
smallest = item;
}
}
smallest
}
fn main() {
let chars = vec!['c', 'b', 'p', 'r', 't', 'f'];
let result = smallest(&chars);
println!("The smallest char is {}", result);
let numbers = vec![45, 33, 4, 999, 123];
let result = smallest(&numbers);
println!("The smallest number is {}", result);
}When you try to compile this you get the following error. The key part is where it says consider restricting type parameter `T`. Again the Rust compiler is being strict, but helpful.
|
4 | if item < smallest {
| ---- ^ -------- T
| |
| T
|
help: consider restricting type parameter `T`
|
1 | fn smallest<T: std::cmp::PartialOrd>(list: &[T]) -> T {
| ++++++++++++++++++++++So what do we do? Well in this case the less than operator < we want is defined as a default method on the standard library trait std::cmp::PartialOrd. So in order to be able to use the less-than operator, we need to specify PartialOrd in the trait bounds for T so that the smallest function will work on any type that can be compared.
How should our function look then, see below. Notice we also need to add the Copy trait and can specifiy multiple by using the + between traits.
use std::cmp::PartialOrd;
fn smallest<T: PartialOrd + Copy>(list: &[T]) -> T {
let mut smallest = list[0];
for &item in list.iter() {
if item < smallest {
smallest = item;
}
}
smallest
}
fn main() {
let chars = vec!['c', 'b', 'p', 'r', 't', 'f'];
let result = smallest(&chars);
println!("The smallest char is {}", result);
let numbers = vec![45, 33, 4, 999, 123];
let result = smallest(&numbers);
println!("The smallest number is {}", result);
}This now compiles and works as expected. We have a generic function for finding the smallest item in a list. If we try to pass in a type that doesn’t fit within the trait bounds we specified then the compiler will tell us there is a problem.
We have seen quite a bit of generics, like traits we will explore more in further articles in this series, but for now let’s move onto closures.
Closures
A closure is an anonymous function that can “close over” variables from an outer function. I find it most beneficial to think of a closure as a function you define in-place.
You may have read about the concept of closures before, but they were called lambda expressions.
The closed-over variables are called free variables because they are free from the scope of the closure. Like any function, closures execute code, have parameters, and return values.
Closures can also capture data from the scope from which they are called.”
Rust has three different Fn* traits, which closures implement.
- FnOnce — describes a closure that can only be called once. If some part of the environment is moved into the closure’s context, and the closure’s body subsequently moves it out of the closure’s context, then those moves can only happen once — there’s no other copy of the source item to move from — and so the closure can only be invoked once.
- FnMut — describes a closure that can be called repeatedly, and which can make changes to its environment because it mutably borrows from the environment.
- Fn — describes a closure that can be called repeatedly, and which only borrows values from the environment immutably.
When making the decision about using a closure or a standard function here are some considerations on when closures are beneficial.
- Closures are convenient when there is a single reference to the function.
- Closures are ideal as first-class citizens. You can treat closures as function parameters, return values, or even assign to a variable.
- Closures are often defined close to where the function is used, which makes the code more maintainable.
Let’s look at a simple example of a closure.
fn main() {
let greeting = || println!("Hello, Rust closures!");
greeting();
}Here a closure is declared and assiged to a variable greeting, which we can then call.
Closures start with the | | syntax. After that comes the implementation portion of the closure. Since it can be treated as a value, the closure is assigned to the greeting variable.
What about the anonymous part of closures we may have heard of. Well this is the same as above, but called directly.
fn main() {
fn main() {
(|| println!("Hello, Rust closures!"))();
}
}Here is a deeper description of the syntax around closures.
|parameter_1, parameter_2, …, parameter_n|->return_type {
// code block
}As mentioned closures start with the distinctive | | syntax. This is called the pipe symbol in case you are not aware. The function parameters are then placed between these pipes. If there are parameters, these are described in name:type pairs. The return type defines the type of the return value. Finally, the closure block contains the expression to be executed when the closure is called.
Here is another example.
fn main() {
let power = |number: i32, power : u32| -> i32 { i32::pow(number, power) };
let value = 2;
let result = power(value, 10);
assert_eq!(result, 1024);
println!("Resul: {}", result)
}This takes a number and an exponent to take that number to the power of. It defined two parameters with the pipes, and designates that it returns a i32 as return type. Finally within the code block it defined the actual logic.
Functions can also return closures as we will see below.
fn main() {
fn get_closure() -> impl Fn(i32, u32) -> i32 {
|number: i32, power: u32| i32::pow(number, power)
}
let power = get_closure();
let result = power(2, 10);
assert_eq!(result, 1024);
println!("{}", result);
}Here the function get_closure returns a closure. Notice how we do not need to define explicit a code block as the closure execution portion only has one statement. We define the Fn with the required parameters and that is all we need to be to call it.
That a closure can access a free variable (or closed over variable) is also beneficial. The captured variable is then available within the closure. Most often, captured variables are borrowed from the outer function.
fn main() {
let number = 2;
let power = 10;
let power = ||i32::pow(number, power);
let result = power();
assert_eq!(result, 1024);
println!("Resul: {}", result)
}Here we define two outside variables, number and power, that are within scope when the closure is executed. We can then execute the closure without parameters.
If you need to have something between the pipes but don’t want to use the parameter you can use |_|. It only means that the closure needs to take an argument but you don’t want to use it. See below for an example.
We will cover the iter() portion, which is an iterator in the next article. For now just now it iterates over each value in the vector.
fn main() {
let the_vec = vec![1, 2, 3];
the_vec
.iter()
.for_each(|_| println!("Print something here to check if called"));
}Just like above, we can use the same example to show a slightly modified version where we do use the value and the closure can be used as a parameter to the for_each.
fn main() {
let the_vec = vec![1, 2, 3];
the_vec
.iter()
.for_each(|n| println!("Print something here to check if called {}", n));
}Here as we “iterate” over the vector the variable n is filled with the current item from the vector.
Now that completes our introduction to closures.
Summary
Another information filled article for you. The article started with a side explanation of consts and static variables. It then moved into the world of traits and their syntax and use cases. Note we will see a lot more from traits as we progress further. Next were generics and an explanation how they are used. Like traits, generics are core to the language so we have seen them before with little explanation and will learn more. The overview ended with closures with several examples of how they are called, passed as parameters and can be return values from functions.
With that we have concluded our fifth article in the series. Any feedback or suggestions would be welcome how this could be improved or expanded on.
Enjoy the journey!
🔔 If you enjoyed this, subscribe to my future articles or view already published here. 🚀
📝 Have questions or suggestions? Leave a comment or message me through Medium.
Thank you for your support! 🌟





