Books

Thursday, January 23, 2020

Learning Rust - Day 6 - Generic Types, Traits, and Lifetimes

This is the 6th journal entry of my learning Rust journey. You can read other posts in this series by following the label learning rust.

In this study session I went through chapter 10 of the Rust book, which covers Generic Types, Traits, and Lifetimes. Generic types and Traits were easy to digest, as they are concepts I am already familiar with from other languages. It was Lifetime that proved to be the difficult nut to crack. Just like when going over the section about Modules, I had to consult other sources outside the book in other to be able to wrap my head around the concepts. I would not say I have it 100% locked down, but I think I now know enough of the general gist to proceed with the rest of the book.


Generics and Traits

Going over Generic Types and Traits was more or less about learning how these concepts are encoded in rust. It did introduced a lot of syntax, which I outline below:

Generic related syntax

// Function that defines the generic type    
fn generic_function<T>(input: T) -> T {
    unimplemented!()
}

// Generic Struct and Enum    
struct Point<T> {
         x: T,
         y:T    
     }

enum Color<T> {
       Red(T),
       Green(T),
       Blue(T)
     }

// Generic methods    
impl<T> Color<T> {
    fn get_hue<U>(&self) -> T {
      unimplemented!()
   }
}

Trait related syntax

// defining a trait   
pub trait Summary {
   fn summarize(&self) -> String;
}

// defining a trait with default implementation    
pub trait DefaultSummary {
   fn summarize(&self) -> String {
      unimplemented!()
   }
}

pub struct Tweet {
     pub username: String,
     pub content: String,
     pub reply: bool,
     pub retweet: bool,
}

// defining an instance of a trait for a type
impl Summary /* <- trait name */ for Tweet /**/ {
    fn summarize(&self) -> String {
        unimplemented!()
    }
}

// specifying function parameters as accepting traits    
// uses the impl trait syntax    
fn notify(text: impl Summary) -> String {
    text.summarize()
}

// specifying function parameters as accepting traits    
// uses the trait bound syntax    
fn another_notify<T: Summary>(text: T) -> String {
    text.summarize()
}

// specifying multiple traits for a type    
fn multiple_notify(text: impl Summary + Display) ->String {
    unimplemented!()
}

fn another_multiple_notifify<T: Summary + Display>(text:T) -> String {
    unimplemented!()
}

// specifying multiple traits with where clause    
// instead of    
fn some_function<T: Display + Clone, U: Clone + Debug>(t: T, u: U) -> i32 {
    unimplemented!()
}
    
// we have    
fn some_other_fn<T, U>(t:T, u: U) -> i32 where T: Display + Clone, U: Clone + Debug {
    unimplemented!()
}
    
// which looks better with new line    
fn some_other_f<T, U>(t:T, u:U) -> i32        
        where T: Display + Clone,
              U: Clone + Debug {
        unimplemented!()
}

// Returning Types that Implement Traits    
fn return_trait() -> impl Summary {
   Tweet {
     username: "".to_string(),
     content: "".to_string(),
     reply: false,
     retweet: false        
   }
}

// implementing methods on a generic struct, 
// if the type parameter implements some traits    
struct Pair<T> {
        x: T,
        y: T,
    }

// new method would be available, regardlass of T    
impl<T> Pair<T> {
     fn new(x: T, y: T) -> Self {
         Self {
             x,
             y,
      }
   }
}

// cmp_display would be available only if T has instance for Display 
// and PartialOrd    
impl<T: Display + PartialOrd> Pair<T> {
  fn cmp_display(&self) {
      if self.x >= self.y {
         println!("The largest member is x = {}", self.x);
      } else {
         println!("The largest member is y = {}", self.y);
       }
    }
 }

// blanket implementations: Can implement ToString for T    
// only if T already implements Display    
impl<T: Display> Summary for T {
   fn summarize(&self) -> String {
      unimplemented!()
   }
}

fn re(x: &str) -> &str {
   unimplemented!()
}

Some noteworthy learning around generics include:

Given a generic type T, T can be a type that can be on the heap or stack. It can't be determined from just the generic type signature. I think this have ramification when it comes to borrowing.

Some noteworthy learning around traits include:

It is only possible to implement a trait on a type only if either the trait or the type is local to my crate.

Basically either of the following scenario:
  • I have my local type, I have an external Trait. I can import the external trait and implement it for my local type.
  • I have a local Trait, I have an external type. I can import the external type and implement my trait for it.
The Trait bound syntax enforces that multiple function parameter implements same traits and are of the same type. This is not the case with impl trait syntax.

// first and second has to be the same type
// they also must have an implementation for trait Summary
fn multiple_function_same_type<T: Summary>(first: T, second: T) -> i32 {
    unimplemented!()
}

// fist and second can be a different type
// but they must have an implementation for trait Summary
fn multiple_function(first: impl Summary, second: impl Summary) -> i32 {
    unimplemented!()
}

If i have a function whose return type is represented with a trait, It is not possible from that function to have an implementation that could return any of the available implementations; i.e. via if else. The implementation should only be returning one type that implements that trait. Even if the types implements same traits. This restriction can be circumvented though, using Traits Objects. But I won't be learning about that, not until Chapter 17.

Lifetime

The bulk of the time in this study session was spent grokking (or trying to?) lifetimes. Even though I do not have the concept 100% locked down, some points I think are worthy of note. I list these below.

I think the big idea about lifetime is that they ensure all borrows are valid. That means ensuring that every use of & is valid. The borrow checker can be seen as the enforcer that is saddled with this task, and in other to perform it, it uses the concept of lifetime.

For example, given the following non compiling function definition:

fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
 

The use of & can either be a valid or invalid borrow.

The use of in the argument can be assumed to always be valid, since the value needs to exist in other for it to be borrowed into the function to be used. The same can't be said of the use of & in the return value.

The value the borrow in the return refers, can only be from two places. From within the function or from the input to the function.  If the borrow is to a value created within the function, then you have an automatically invalid situation, because once the function call is over the value would be cleared, leading to dangling pointer situation. 

In the case where the borrow in return is based off the input, then it becomes impossible to immediately tell if the value that was borrowed into the function would be valid for as long as the variable that ends up holding the return value. Hence why the above code snippet won't compile.

In other to make the above to compile, extra information needs to be provided that helps the borrow checker ensure that the returned borrow would indeed continue to be valid. This is done using lifetime annotations.

The updated function with lifetime annotation provided would look like this:

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
} 

So what does this buy us? 

My interpretation is that we are telling Rust, that whenever this function is used, the inputs and the return value must be covered by the same lifetime. 

This means that for the return borrow, the scope should be shorter, or at-least as long as  those of the inputs. In case where the lifetime/scope of the return value is longer than any of the input, then the constraint of the lifetime annotation is being violated and Rust won't compile the code.

To make the above concrete, we take a look at two scenarios where the function above is used. One where the lifetime constraints are respected and another where they are violated

fn main() {
  let string1 = String::from("long string is long");

  {
    let string2 = String::from("xyz");
    let result = longest(string1.as_str(), string2.as_str());
    println!("The longest string is {}", result);
    // the lifetime of result ends here    
    // the lifetime of string2 ends here    
    // the lifetime of string1 continues    
    /* if result refers to string2, then it is fine, 
       because they have same lifetime */    
    /* if result refers to string1, it is fine, 
       because result has a shorter lifetime */  
   }
}

The above respects the lifetime annotations.

fn main() {
  let string1 = String::from("long string is long");
  let result;
  {
    let string2 = String::from("xyz");
    result = longest(string1.as_str(), string2.as_str());
    // the lifetime of result continues after here    
    // the lifetime of string2 ends here    
    // the lifetime of string1 continues after here    
    /* if result refers to string1 then it is fine,       
       because lifetime of result and string1 have same lifetime */
    /* but if result refers to string2, then it is not fine,      
       because lifetime of result is longer than string2,       
       hence violating the lifetime annotation */    
     }
    println!("The longest string is {}", result);
}

So the function signature:

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str

...could be interpreted as the return value should never have a longer lifetime than any of the inputs. And I think this is inline with the general rule of borrow: The subject of the reference should live as long or longer as the variable reference it..

In the first entry in this series, I noted that The plan is that, after I become proficient in Rust, I can return to these series of posts and cringe at my ignorance! I think this post, out of the others in this series is that one where that statement is truest for the most! 😂

Anyways, I think these were the main points from reading through chapter 10. This chapter took longer that expected so I am really itching to continue with the rest of the book, hopefully no more stumps! 

No comments:

Post a Comment