In this example, set up a new cargo project and add a crate:
# Adding the crate $ cargo add num-traits
We are going to add pythagorean theorem function. We will come to learn that numbers are a little bit more awkward.
Rust doesn't automatically convert number types for you.
The following won't compile:
fn solve(a: f64, b: f64) -> f64 { (a.powi(2) + b.powi(2)).sqrt() } fn main() { let a: f32 = 3.0; let b: f64 = 4.0; println!("Result {}", solve(a, b)) }
You can't even do arithmetic (like a + b
).
We could convert it directly to a float 64 prior to passing the value.
fn solve(a: f64, b: f64) -> f64 { (a.powi(2) + b.powi(2)).sqrt() } fn main() { let a: f32 = 3.0; let b = 4.0; let a_f64 = a as f64; println!("Result {}", solve(a, b)) }
We could also use the crate num-traits
that we just installed.
use num::traits::ToPrimitive; fn solve(a: f64, b: f64) -> f64 { (a.powi(2) + b.powi(2)).sqrt() } fn main() { let a: f32 = 3.0; let b = 4.0; let a_f64 = a.to_64().unwrap(); println!("Result {}", solve(a, b)) }
It adds some helpful helpers to do the conversions.
For what it's worth, solve
is also very annoyingly strict. Let's solve this.
To support f32, we can write code like so:
use num::traits::{Float, ToPrimitive}; fn solve<T: Float>(a: T, b: T) -> f64 { let a_f64 = a.to_f64().unwrap(); let b_f64 = b.to_f64().unwrap(); (a_f64.powi(2) + b_f64.powi(2)).sqrt() } fn main() { let a: f32 = 3.0; let b: f32 = 4.0; println!("Result {}", solve::<f32>(a, b)) }
The generic type is like arguments for types.
We also don't strictly need the ::<TYPE>
annotation on solve
at the call site either. It just helps for completeness, but you can rely on inference.
In the context of the code before, "Float" is a trait. Here is it being used as a trait bound.
A trait is a set of methods. It can contain abstract methods which don't have an implementation, and it can contain default methods which have an implementation:
trait Vehicle { // abstract method fn start(&self); // default method fn stop(&self) { println!("Stopped"); } } struct Car {}; impl Vehicle for Car { fn start(&self) { println!("Start!!!"); } }
A struct/enum/primitive can implement a trait.
The implementor has to provide an implementation for all of the abstract methods.
The implementor can optionally override the default methods.
When we use it with generics:
fn start_and_stop<T: Vehicle>(vehicle: T) { vehicle.start(); vehicle.stop(); } fn main() { let car = Car {}; start_and_stop(car); }
Then T
is using Vehicle
as a trait bound.
What happens if we want to two different float types? We can use multiple generics:
use num::traits::{Float, ToPrimitive}; fn solve<T: Float, U: FLoat>(a: T, b: U) -> f64 { let a_f64 = a.to_f64().unwrap(); let b_f64 = b.to_f64().unwrap(); (a_f64.powi(2) + b_f64.powi(2)).sqrt() } fn main() { let a: f32 = 3.0; let b: f32 = 4.0; println!("Result {}", solve(a, b)) }
How can we pass in any time of number?
We could use ToPrimitive
trait instead of the Float
trait:
use num::traits::ToPrimitive; fn solve<T: ToPrimitive, U: ToPrimitive>(a: T, b: U) -> f64 { let a_f64 = a.to_f64().unwrap(); let b_f64 = b.to_f64().unwrap(); (a_f64.powi(2) + b_f64.powi(2)).sqrt() } fn main() { let a: f32 = 3.0; let b: f32 = 4.0; println!("Result {}", solve(a, b)) }
We want to make a Basket
struct that can hold any kind of data.
We also want a get
, put
and is_empty
method.
put
will also have a corner case where if a number is stored, it sums up the store.
We will also make a Stack
struct. It will have identical methods, but different implementations.
We can use a trait for both to make things more flexible.
// basket.rs pub struct Basket { item: Option<String>, } impl Basket { pub fn new(item: String) -> Self { Basket { item: Some(item) } } pub fn get(&mut self) -> Option<String> { self.item.take() } pub fn put(&mut self, item: String) { self.item = Some(item); } pub fn is_empty(&self) -> bool { self.item.is_none() } }
In main.rs
for now:
mod basket; use basket::Basket; fn main() { let b1 = Basket::new("apple".to_string()); }
Let's update our basket to be more generic.
// basket.rs pub struct Basket<T> { item: Option<T>, } impl<T> Basket<T> { pub fn new(item: T) -> Self { Basket { item: Some(item) } } pub fn get(&mut self) -> Option<T> { self.item.take() } pub fn put(&mut self, item: T) { self.item = Some(item); } pub fn is_empty(&self) -> bool { self.item.is_none() } }
Why the two Ts in impl<T> Basket<T>
?
First, let's take note of something in main.rs
:
mod basket; use basket::Basket; fn main() { let b1 = Basket::new("apple".to_string()); let b2 = Basket::new(3.14); let b3 = Basket::new(true); }
Right now, each basket binding will only even be able to work with the type it was initialized with.
In impl<T> Basket<T>
, the first T is a list of generics while the second T is the reference to those generics (simile to fn example<T>(arg: T) {}
).
Let's create our stack:
// stack.rs pub struct Stack<T> { items: Vec<T>, } impl<T> Stack<T> { pub fn new(items: Vec<T>) -> Self { Stack { items } } pub fn get(&mut self) -> Option<T> { self.items.pop() } pub fn put(&mut self, item: T) { self.items.push(item); } pub fn is_empty(&self) -> bool { self.items.is_empty() } }
As with before, we can use it in main.rs
:
mod basket; mod stack; use basket::Basket; use stack::Stack; fn main() { let b1 = Basket::new("apple".to_string()); let b2 = Basket::new(3.14); let b3 = Basket::new(true); let s1 = Stack::new(vec![1, 2, 3]); }
First, we create the trait:
// container.rs pub trait Container<T> { fn get(&mut self) -> Option<T>; fn put(&mut self, item: T); fn is_empty(&self) -> bool; }
Then we update our basket and stacks:
use super::container::Container; pub struct Basket<T> { item: Option<T>, } impl<T> Basket<T> { pub fn new(item: T) -> Self { Basket { item: Some(item) } } } impl<T> Container<T> for Basket<T> { fn get(&mut self) -> Option<T> { self.item.take() } fn put(&mut self, item: T) { self.item = Some(item); } fn is_empty(&self) -> bool { self.item.is_none() } }
Stack is effectively the same changes.
Things to note:
impl
block for methods that are not part of the trait.pub
keyword.The final code in main.rs
:
mod basket; mod container; mod stack; use basket::Basket; use container::Container; use stack::Stack; fn add_string<T: Container<String>>(container: &mut T, s: String) { container.put(s); } fn main() { let mut b1 = Basket::new("apple".to_string()); let b2 = Basket::new(3.14); let b3 = Basket::new(true); let s1 = Stack::new(vec![1, 2, 3]); let mut s2 = Stack::new(vec![ String::from("a"), String::from("b"), String::from("c"), ]); add_string(&mut b1, String::from("banana")); add_string(&mut s2, String::from("banana")); }