2

I'm new to rust and lately I read about Result type. loved the concept and started using it in my app.

My first try was with &str. So my return type looked like this:

pub fn calculate_risk_score(a: i32, b: i32) -> Result<i32, &str> {
    return Ok(a + b);
}

and thats the error I was getting:

compilation error

The following code works:

pub fn calculate_risk_score(a: i32, b: i32) -> Result<i32, &'static str> {
    return Ok(a + b);
}

Now I don't think I fully get why it's wrong to pass a reference as a generic type. Besides I don't understand why static solves the problem and I don't fully understand the explanation about named lifetime parameter. I read those articles but I think Im still missing something.

https://doc.rust-lang.org/std/result/

https://doc.rust-lang.org/rust-by-example/scope/lifetime/static_lifetime.html

so my questions:

  1. why does &'static str can be passed as generic type while &str can't?
  2. what it has to do with lifetime? and why does the compiler can't figure out how long the reference is valid when I set it to be &str?
Francis Gagné
  • 60,274
  • 7
  • 180
  • 155
yonBav
  • 1,695
  • 2
  • 17
  • 31
  • 1
    Does this answer your question? [Why do I need to add a lifetime to a function that returns a reference?](https://stackoverflow.com/questions/49509358/why-do-i-need-to-add-a-lifetime-to-a-function-that-returns-a-reference) – kmdreko May 22 '21 at 15:55
  • The compiler can often [deduce](https://doc.rust-lang.org/nomicon/lifetime-elision.html) the lifetime of returned references but only when there's lifetimes associated with the parameters. `i32`s don't have any lifetimes and the compiler won't deduce it to be `'static` for you, you must be explicit. And if there's multiple lifetimes involved, you'll probably want to annotate the returned reference anyway to ensure its correct. – kmdreko May 22 '21 at 16:03
  • And `Result` is a red herring, it doesn't affect how references or lifetimes in any way here. – kmdreko May 22 '21 at 16:05
  • @kmdreko you are right, but I I think the answer I got here from Aykut Yilmas is much better than the one in the post. Is there a way to move that answer over there? – yonBav May 22 '21 at 17:59
  • @kmdreko now there are 2 great answer in that thread maybe it would be best if I'd just edit the question and drop the Result from its title. WDYT? – yonBav May 22 '21 at 19:16
  • Such an edit would invalidate existing answers, please leave the question as-is. – user4815162342 May 23 '21 at 09:16

2 Answers2

3

To understand the problem with this function signature:

pub fn calculate_risk_score(a: i32, b: i32) -> Result<i32, &str> {
    todo!()
}

...consider what &str means. It means: a borrowed slice of somebody's string. If such a function were allowed, one could call it like this:

if let Err(e) = calculate_risk_score(foo, bar) {
    // `e` is &str - who is it borrowed from, and how long is it valid?
    // Can I put it in a container? Can I return it from this function?
    // Can I put it in a global variable?
    ...
}

Lifetimes provide answers to these questions by connecting the lifetime of the returned borrow to the lifetime of a function parameter, which is provided by the caller, so its scope must be known. This is often as simple as the scope of self:

struct Parser {
    last_error: String,
    // ...
}

impl Parser {
    // Equivalent to fn calculate_risk_scope<'a>(&'a self, a: i32, b: i32) -> Result<i32, &'a str>
    fn calculate_risk_scope(&self, a: i32, b: i32) -> Result<i32, &str> {
        if error {
            return Err(self.last_error.as_str());
        }
        ...
    }
}

Now the lifetime of the &str borrow is tied to the lifetime of the borrow of &self, i.e. the returned error may be used as long as the object on which the method was invoked is alive and unmodified.

In case of the original function, &str is the only reference in the whole signature, so there is no other borrow to connect it to, and the compiler requires you to supply an explicit lifetime. It suggests 'static as a reasonable choice given the constraints, and it indeed works if you're using literal strings which are baked into the executable and never deallocated.

Note that the compiler will never use the body of the function to infer the lifetime of arguments or return values. This is intentional, and allows the signature of the function to constitute an API contract that doesn't depend on the implementation. When the function is simple enough that it receives one reference and returns one reference, the compiler will take it to mean that their lifetimes are connected, as if they were spelled out as fn calculate_risk_scope<'a>(&'a self, a: i32, b: i32) -> Result<i32, &'a str>. This is called lifetime elision and works just by examining the function signature, completely ignoring its body. (The body must of course conform to the lifetimes in the signature.)

user4815162342
  • 141,790
  • 18
  • 296
  • 355
1

1. Why does 'static lifetime work?

The 'static lifetime works because it means that the referenced value lives the entire lifetime of the program. Every string in Rust that is written directly in the source code has a lifetime of 'static. e.g. let s = "hello world";, because it is included in the compiled binary and therefore is never dropped.

2. Why explicit lifetime and how to fix?

The compiler can't figure it out how long the returned value lives, as it expects an explicit lifetime. A simple fix would be to return an owned string (String type) instead of a borrowed string or just stick with &'static str.

I would suggest you to read References and Borrowing and Validating References with Lifetimes to get a better understanding of how lifetimes in Rust work.

Aykut Yilmaz
  • 183
  • 2
  • 6