Rust Lifetime Parameters

When a programming language makes you whip out a dictionary

Dictionary entry in book

A weird word is about to help me learn Rust lifetime parameters and I’ll start to piece together when they are needed. This is a complicated part of Rust tracking ownership and is somewhat unique, at least in the languages I’ve been exposed to up to this point.

Elision

e·li·sion /əˈliZHən/
1. The omission of a sound or syllable when speaking (as in I’m, let’s)
2. The process of joining together or merging things, especially abstract ideas.

So, I’m guessing definition 2 is why we have this in Rust, and definition 1 explains how the syntax developed. Anyway, I’m trying to piece it together. This is also referred to as a lifetime parameter in Rust. But what is it? Why do we does the compiler need it? What’s it for?

BTW, Rust 1.36.0 came out today! Congratulations Rust team!! Be sure to update!!

Help With Lifetimes

From The Rustonomicon, the advanced book of Rust for when you get bored, “Lifetimes are named regions of code that a reference must be valid for. … In most of our examples, the lifetimes will coincide with scopes. This is because our examples are simple.”[link] So, let’s look at some examples to figure this out:

pub struct Vehicle {
    make: String,
    model: String,
}

fn main() {
    let mut garage: Vec<Vehicle> = Vec::new();

    garage.push(Vehicle {
        make: "Subaru".to_string(),
        model: "Crosstrek".to_string(),
    });

    for vehicle in garage {
        println!("I have a {} {}", vehicle.make, vehicle.model);
    }
}

Here, we’ve created a simple struct to hold a Vehicle – with its make and model as two Strings. This compiles and runs just fine (and there are lifetimes involved, the compiler is figuring them all out for us.)





Hrm, but a vehicle’s make and model don’t change overnight (this isn’t a Pixar movie)… if we change them from String (the heap storage variable that can adapt in size as we change the value) into a str (fixed-length, generally immutable) type, maybe that’s better!

pub struct Vehicle {
    make: str,
    model: str,
}

but, oh, I have a bug

Bug with droplets of water crawling on plant
You’re still here? It’s over. Go home. Go.
  |
9 |     make: str,
  |     ^^^^^^^^^ doesn't have a size known at compile-time
  |

oh, right…. they have to be initialized at compile time – so that would only work as a constant. Ok, then, when we initialize with the strings, we’ll let the struct borrow our strings, that’s why you almost always see &str in code:

pub struct Vehicle {
    make: &str,
    model: &str,
}

and here we come to it:

error[E0106]: missing lifetime specifier
 --> src/main.rs:9:11
  |
9 |     make: &str,
  |           ^ expected lifetime parameter

error[E0106]: missing lifetime specifier
  --> src/main.rs:10:12
   |
10 |     model: &str,
   |            ^ expected lifetime parameter
Smaller version, same bug, same plant
Yup, another one…

rustc is telling us it cannot enforce the default lifetime for these fields inside the struct, because their lifetime is not affixed to the lifetime of the struct instance, but the struct instance depends on those fields!

The compiler is having a fit because when we initialize the field make with “Subaru”, the struct borrows a reference to that string. Which means if that string goes out of scope (and gets freed by the compiler) before the struct does, the struct would then be pointing out in no-man’s-land, which is bad! The compiler knows it can’t control this, so it bails.

Fixing things

pub struct Vehicle<'a> {  
    make: &'a str,  
    model: &'a str,  
}

The ‘a (pronounced “tic a”) is explaining to the compiler how it should handle the lifetimes.

We are basically adding a parameter to the definition – we used ‘a but we could have used ‘w or ‘lifetime (though, I’ve never seen it other than a single character, usually starting at ‘a for the first one you need). With this parameter, we are saying to the compiler:

when you construct an instance of this type, let’s agree to call the instance’s lifetime (that you manage) by the variable a and you let me know what that lifetime is … AND btw… when you borrow the strings for make and model as references, I’m telling you to make sure their lifetime is whatever a is. In other words, make sure that the memory for make and model hangs around for exactly as long as the struct instance itself does.”

This now looks just a little more complicated because we had to give hints to the compiler of how to maintain lifetimes, but runs just fine again:

pub struct Vehicle<'a> {
    make: &'a str,
    model: &'a str,
}

fn main() {
    let mut garage: Vec<Vehicle> = Vec::new();

    garage.push(Vehicle {
        make: "Subaru",
        model: "Crosstrek",
    });

    for vehicle in garage {
        println!("I have a {} {}", vehicle.make, vehicle.model);
    }
}

Return to String Basics

Where I try to give some life to poor “Bob”, but learn more about Clippy and Fmt

Pale robot face, is this the face of Bob?
I’m sorry “Bob”, I cannot do that.

Another sample programming problem and I am about to practice so string methods, but also learn about two Rust helpers: Cargo Clippy and Fmt. It is becoming apparent just how much help in Rust coding is embedded in the error messages and in cargo.

The next Exercism programming task was to have “Bob” respond in various ways to a passed in prompt. If you ask a question, Bob should respond with “Sure.” unless you are SCREAMING, in which case his response is “Calm down, I know what I’m doing!”. If you prompt “Bob” with nothing, he says “Fine. Be that way!”, but everything gets back a simple “Whatever.”

Coding Like a Rust Coder

Of course, there are many ways to check all these conditions. Still, I wanted to use the built-in helper methods and find the most Rust-idiomatic way. I was also cognizant of not repeating myself: not running the same method or checking the same test more than once and wasting clock cycles.

For instance, string.trim() removes leading and trailing whitespace, and combining that with a built-in test, we can see if the prompt is basically empty by using string.trim().is_empty()

I choose to see if “Bob” is being yelled at, but uppercasing the string and seeing if nothing changed – of course, that’s only true if the string includes at least 1 alphabetic character, so lets check that first with string.chars().any(char::is_alphabetic).

That will iterate using the chars() that we’ve seen before and tell me if any of them are a-z/A-Z. To uppercase the string (I’m only going to worry about the ASCII characters here; Rust also has to_uppercase which will follow the terms of the Unicode Derived Core Property Uppercase, see this for some issues with Unicode characters), so I’ll just use string.to_ascii_uppercase().




Searching for Possibilities

Lastly, it might be a “whispered question”, but I want to make sure the prompt isn’t entirely just a question mark – that doesn’t fit the premise of a question according to the Exercism-provided test I am still failing, so string.len() will give me the length of the string. I end up with this code:

 pub fn reply(message: &str) -> &str {
    let check_message = message.trim();

    if check_message.is_empty() {
        return "Fine. Be that way!";
    }

    let question = check_message.ends_with('?');

    if check_message.chars().any(char::is_alphabetic)
        && check_message.to_ascii_uppercase() == message
    {
        if question {
            return "Calm down, I know what I'm doing!";
        } else {
            return "Whoa, chill out!";
        }
    }

    if check_message.len() > 1 && question {
        return "Sure.";
    }
    "Whatever."
} 

So, “not running the same method or checking the same test more than once” meant I had to make a copy of the passed in string so I could trim() it just once at the beginning. I also didn’t want to test for a ‘?’ at the end of the prompt more than one time. Maybe my extra variable creation costs more than running trim() each time I would need it, I’m not sure!

Cargo Clippy and Fmt

Bad sci-fi movie robot, maybe his name can be Cargo Clippy
Cargo Clippy is here to help!

Another note: I used two Rust assistants: first, clippy to check my code before submitting it to Exercism. Clippy is basically a Rust lint program and will alert you to changes you can make to your code even though it compiles. For instance, I learned about is_empty() (instead of checking len() == 0) and I learned that ends_with() can apparently take a char OR a string (I originally had ends_with(“?”) since I was calling a method on a String type and I just assumed…)

You can get “clippy with it” (no, he didn’t!) by typing rustup component add clippy. I then ran it against my code by doing cargo clippy –all-targets.

Also, there is a prettifier called fmt; get it by doing rustup component add fmt and then run cargo fmt. This “fixed” my indentation and curly braces (and probably other things) to match what is commonly accepted correct-formatting for Rust scripts.

Crazy (En) code and decode

Where I fiddle with a crazy encoding scheme

laptop in dark room with green code on screen
The 0s and 1s have me…

Encoding with Rust code – sounds fun! Pulling from a different “programming exercise” site this time: CodeWars. One of the very first exercises there is a crazy encoding which looks like:

++++++++++++++++++++++++++++
++++++++++++++++++++++++++++
++++++++++++++++.++++++++++++
+++++++++++++++++.+++++++..+++.

Starting at ASCII value 0 when we begin decoding, we start processing the string:
+ means we add 1 to the ASCII value we are holding
. means we print the character based on the current ASCII value

If you add 1 to get 256, that would loop back to 0.

So, that mess above equates to the string “Hello” (you can probably find the two ‘l’s in the encoded string).

So, lets whip up a function to decode that:

Coding Time

fn decode(code: &str) -> String {
    let mut str = String::new();
    let mut cur: u8 = 0;

    for c in code.chars() {
        match c {
            '+' => cur = cur.wrapping_add(1),
            '.' => str.push(cur as char),
            _ => {},
        }
    }
    str
}

Simple enough, we get an encoded string as a parameter, we start our cur variable at 0 and loop thru the characters of the string. A simple match lets us choose what to do. BTW, wrapping_add means we don’t have to check for 256 ourselves, it will wrap back to 0 automatically. Anything other than a + or . we simply ignore.

So, for fun, lets write an encode function as well:

fn encode(orig: &str) -> String {
    let mut str = String::new();
    let mut cur: u8 = 0;

    for c in orig.chars() {
        while c != cur as char {
            cur = cur.wrapping_add(1);
            str.push('+');
        }
        str.push('.');
    }
    str
}

Just slightly different, but still quick and easy. Ok, we can setup a simple main() to test these:

fn main() {
     let original = "Hello, World!";

     let encoded = encode(&original);
     println!("'{}' encodes as '{}'", &original, &encoded);

     let decoded = decode(&encoded);
     println!("which then decodes to '{}'", &decoded);
     assert_eq!(original, decoded);
 }



Command Line Args

Well, that was fun. Hey, lets change it to take what we’d like encoded from the command line. There a built-in library to help with that and, of course, we only need to change main()

fn main() {
     for arg in std::env::args().skip(1) {
       let encoded = encode(&arg);
       let decoded = decode(&encoded);
       assert_eq!(arg, decoded);
       println!("'{}' encodes as '{}' and decode works",
           &arg, &encoded);
     }
 }

So, std::env::args will give us all the args a program was called with, including the program name itself which is always in [0] and why we want to skip(1) to avoid encoding it. So now:

cargo run “Please sir, may I have some more soup”

runs just fine – I’ll avoid showing you the encoded version! Also:

cargo run What in the world

runs, and encodes each word separately, since they are individual parameters to the program.

Ok, more fun, what if we changed the + to 1 and the . to 0 and made this encoding look like binary, lol! Now, “Hello” encodes as:

11111111111111111111111111111111111111111111111111111111111111111111111101111111111111111111111111111101111111001110

I have more ideas of ways to play with this, so more to come next post…