Rust Borrow Checker Explained Part 3: Function Result Lifetimes
Tags: rust, tutorial
Published: 2023-11-11 21:13:38

## Intro

In Part 2, we went over the basics of borrowing and how it looks out for you. In this article we will go over the rules of lifetimes which are used by the borrow checker to ensure the *owned* lifetime always out lasts the *borrowed* lifetime.

## Lifetimes

Lifetimes are just a fancy name for ["scope"](https://en.wikipedia.org/wiki/Scope_(computer_science)). If you recall, in [Part 1](https://dev.to/brimworks/rust-borrow-checker-explained-2ohj), we went over how Rust uses the stack to manage memory. Any argument functions and local variables are automatically "popped" off the stack when the function returns. A Rust lifetime simply describes how long a variable is in scope.

## Functions With Explicit Lifetime

Let's try to define a function to get the longest string:

```rust
fn main() {
    let str1 = "tiger";
    let str2 = "three blind mice";
    let longest = longest_str(str1, str2);
    print!("longest={longest}");
}

fn longest_str(str1: &str, str2: &str) -> &str {
    if str1.len() < str2.len() {
        return str2;
    } else {
        return str1;
    }
}
```
[Try it!](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=c89bbf0603657651b14eee3d2ab8a3a5)

Which produces this error:

```
error[E0106]: missing lifetime specifier
 --> src/main.rs:8:43
```

Rust requires lifetimes to be in the function signatures so it can avoid having to infer these lifetimes, which would be computationally expensive and small changes in implementation would alter the implied function signature in ways that are incompatible.

If we manually inspect this code, we see that the lifetime of the resulting `&str` must always be the same as (or longer) than the `str1` and `str2` parameters, since we can't know until runtime if `str1` will be returned or if `str2` will be returned.

So, the explicit lifetime we need is:

```rust
fn longest_str<'a>(str1: &'a str, str2: &'a str) -> &'a str
```

Lifetime names always begin with a single tick, and they need to be declared in the same way as function template parameters. To use them, add the single tick and name after each `&`, so if one of these was a mutable borrow, you would use the syntax `&'a mut`.

You might be asking, why this is needed when this example uses the exact same lifetime for every parameter. Let's look at this example:

```rust
use std::collections::HashMap;

fn main() {
    let mut map: HashMap<String, String> = HashMap::new();
    map.insert("one".into(), "1st".into());
    let value;
    {   // "{" opens a new "scope":
        let one = String::from("one");
        value = lookup(&map, &one);
    }   // `one` is now out of "scope"
    print!("value={value}");
}

fn lookup<'a>(map: &'a HashMap<String, String>, key: &'a String) -> &'a str {
    return map.get(key).map(|v| v.as_str()).unwrap_or("not found");
}
```
[Try it!](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=cfe8bb31f6d4d04137d98f68aa52380e)

...which when ran produces this error:

```
error[E0597]: `one` does not live long enough
  --> src/main.rs:9:30
```

This `lookup` function is declaring that the `map` **and** `key` lifetimes must be the same or greater than the result lifetime... however, this actually is not necessary, since the lifetime of the result is **only** dependent on the lifetime of the `map`. So, we just need the `map` and result lifetime to be the same, the lifetime of the `key` can be different:

```rust
fn lookup<'a, 'b>(map: &'a HashMap<String, String>, key: &'b String) -> &'a str
```

...and now this code compiles.

## Implied Function Lifetime Rules

Rust will try to imply lifetimes using [lifetime elision rules](https://doc.rust-lang.org/beta/reference/lifetime-elision.html) to avoid tediously declaring lifetimes which can often be inferred.

Rust already knows the lifetime of all function parameters at the call point, the borrow checker just needs help in matching up which function parameter lifetime(s) match with any borrowed result(s)... which is why some of these lifetimes can be implied.

The rules:
* If a function has no borrowed result, then you don't need to worry about annotating parameters with lifetimes.
* If a function takes in a single borrowed parameter, **all** borrowed results will have the same lifetime.
* If a function is actually an object method (it has a borrowed `self` parameter), **all** borrowed results will have the same lifetime as the object's `self` parameter.
* Any borrowed parameter without a lifetime declaration which does not follow a rule above will get a distinct lifetime.

When compiling functions, Rust will enforce the lifetime constraints, so there is no harm in trying to always declare your functions without explicit lifetimes.

## Function Result Lifetimes Recap

To recap, we can declare lifetime "template" parameters and use them to describe the relationship between the parameters passed into your function and the result of the function. The borrow checker tries to apply some common patterns for these rules, but it is not perfect, so knowing how to explicitly declare them is necessary at times.

The [next article](https://dev.to/brimworks/rust-borrow-checker-explained-part-4-structure-lifetimes-2od0) in this series will talk about how to use borrowing inside of your data structures to avoid copying.