Using trait constraints

Back in the section on Generic data structures and functions in Chapter 5, Higher Order Functions and Error-Handling, we made a function sqroot to calculate the square root of a 32-bit floating point number:

// see code in Chapter 5/code/sqrt_match.rs 
Use std::f32; 
 
fn sqroot(r: f32) -> Result<f32, String> { 
if r < 0.0 {  
return Err("Number cannot be negative!".to_string());  
} 
   Ok(f32::sqrt(r)) 
} 

What if we wanted to calculate the square root of an f64 type number? It would be very unpractical to make a different version of the function for each type. A first attempt would be to just replace an f32 type with a generic type <T>:

// see code in Chapter 6/code/trait_constraints.rsfn sqroot<T>(r: T) -> Result<T, String> { 
   if r < 0.0 {  
      return Err("Number cannot be negative!".to_string());  
   } 
    Ok(T::sqrt(r)) 
} 

But Rust does not agree because it doesn't know anything about T, signaling multiple errors:

      binary operation `<` cannot be applied to type `T`
    
    and no function or associated item named `sqrt` found for type `T` in
    the current scope
    ...  

A Float trait exists that would be general enough, but not in the standard library, it lives in the num crate, more specifically as num::traits::Float. To be able to use that crate, we must create a project with cargo:

cargo new trait_constraints - -bin  

Then we have to edit the Cargo.toml file and add the following section:

[dependencies] 
num = "*" 

Now give the commands cargo update to add crate num to your project.

To be able to use our external crate num, we add the following code at the start of our file:

extern crate num; 
use num::traits::Float; 

We can assert that T must implement this trait as follows:

fn sqroot<T: num::traits::Float>(r: T) -> Result<T, String> {...} 

This is called putting a trait constraint or a trait bound on the type T. We assert that type T implements the trait num::traits::Float, and it ensures that the function can use all methods of the specified trait.

To be as general as possible, we also use the special indicator for 0 that exists in the num crate, named num::zero(), so our function now becomes:

fn sqroot<T: num::traits::Float>(r: T) -> Result<T, String> { 
      if r < num::zero() {  
            return Err("Number cannot be negative!".to_string());  
      } 
      Ok(num::traits::Float::sqrt(r)) 
} 

This works for both of the following calls:

println!("The square root of {} is {:?}", 42.0f32, sqroot(42.0f32) ); 
println!("The square root of {} is {:?}", 42.0f64, sqroot(42.0f64) ); 

This produces the following output:

    The square root of 42 is Ok(6.480741)
    The square root of 42 is Ok(6.48074069840786)  

But if we try to call the function sqroot on an integer, like this:

println!("The square root of {} is {:?}", 42, sqroot(42) );     

We get the following error:

    the trait `num::Float` is not implemented for {integer}  

Because an integer is not a Float type.

Another way to write the same trait constraint is with a where clause like this:

fn sqroot<T>(r: T) -> Result<T, String> where T: Float { ... } 

Why does this other form exist? Well, there can be more than one generic type T and U, and each type can be constrained to multiple traits (indicated by a + between the traits) Trait1, Trait2, and so on, like in this fictitious example:

fn multc<T: Trait1, U: Trait1 + Trait2>(x: T, y: U) {...} 

With the where syntax this can be made much more readable, like this:

fn multc<T, U>(x: T, y: U) where T: Trait1, U: Trait1 + Trait2 {...}
Exercise:

Define a trait Draw with a draw method.
Define struct types S1 with an integer field, and S2 with a float field. Implement the trait Draw for S1 and S2 (draw prints the values out surrounded by ***).
Make a generic draw_object function that takes any object that implements Draw.
Test it out! (see example code in Chapter 6/exercises/draw_trait.rs)