Default lifetime parameters

I recently wanted to use anyhow in a project, but I could not because my errors were not 'static. This made me realize that we do not have the ability to default lifetime parameters. It would be reasonable to change anyhows API to be:

pub struct Error<'a = 'static> {
    // anyhow doesnt actually use a box, but thats not important
    inner: Box<dyn std::error::Error + Send + Sync + 'a>,
}

This would make anyhow::Error an error with a 'static lifetime, but anyhow::Error<'_> with an elided lifetime according to the standard lifetime elision rules.

I discovered that someone brought this up before, and received no reply: Lifetime parameter default value

If someone wanted to spearhead this, I think it would be a relatively straightforward addition to the language that could be moved along at a fast pace. I'd like to see this feature implemented!

12 Likes

Right now we still allow Foo even if it has lifetime parameters, as meaning Foo<'_, '_, ...>.

So presumably adding defaults to existing lifetime parameters may not be backwards-compatible, but adding new lifetime parameters with defaults, should always be? (assuming the new parameter is used in places where its default used to be used, which I believe is the hypothetical anyhow::Error situation)


Implementation-wise I think we can do this, there's just some concerns around AST->HIR lowering, which still injects placeholders (i.e. one '_ per lifetime parameter) in path segments missing all lifetimes (for lifetime elision to have HirIds to attach elision results to).
Simplest thing to do would be to only inject as many '_ as there are lifetime parameters without defaults, hiding the defaulted parameters from elision (users can still write '_ themselves to get elision).

Long-term, AST->HIR lowering should probably leave lifetimes alone and lifetime elision can just use the path segment HirId and the index in that, instead.
However, that kind of refactoring should not be needed to implement this, we'd just need to be careful about the edge cases (e.g. struct Foo<'a, 'b = 'a> used as Foo, Foo<'_> or Foo<'_, '_>, in various elision contexts).

1 Like

I would personally find it pretty confusing if Error and Error<'_> were different types. I think we should be really wary of adding more complexity to lifetimes as they are already one of the hardest parts for beginners.

8 Likes

This seems consistent with the treatment of types though:

fn main() {
    type Foo<T = u8> = T;
    let ok: Foo;
    let error: Foo<_>;
}

This seems nice. In the best of worlds, having:

pub struct Error<trait Extra = Send + Sync + 'static> {
    // anyhow doesnt actually use a box, but thats not important
    inner: Box<dyn std::error::Error + Extra>,
}

seems like it could add additional flexibility atop of this, although the implementation is much less readily possible right now without Chalk.

3 Likes

That's true, but I don't think that behavior is very obvious for types either; it's just less confusing because people understand types better.

Also, one can't elide type parameters, whereas one can elide lifetime parameters. To me, leaving off a lifetime parameter is like saying "hey compiler figure out what goes here" and using _ for something does the same. But that mental model breaks if elide != fresh inference variable.

2 Likes

Maybe, but in terms of learnability, it seems better to be consistently "wrong" than to be inconsistently right and wrong about things. Aligning lifetimes more towards a more known quantity (types, as you say) seems like a good idea.

Well lifetime elision is a fairly syntactic thing; it's not unification based in the sense of "fresh inference variable, please figure these constraints out compiler" (although I think that would have made the language more ergonomic). I imagine that had we allowed _ in function signatures, it would have meant an inference variable and not "please universally quantify a parameter here" (which is sorta what it means for lifetimes).

1 Like

This also came up a while ago in relation to futures, it'd be nice to be able to specify default lifetimes for type aliases as well. You can currently write

fn foo() -> Pin<Box<dyn Future<Output = ()> + Send + Sync>>;

and have that infer the lifetime of the dyn to 'static, but if you want to use the futures::future::BoxFuture alias you have to specify that explicitly

fn foo() -> BoxFuture<'static, ()>;

Unfortunately this appears to run into the same back-compat issue that BoxFuture<T> is currently treated as BoxFuture<'_, T>, so it wouldn't be possible to use this feature until futures-core 0.4.

2 Likes

That's correct AFAIK.

This is already how lifetime ellision in trait objects work. dyn Error and dyn Error + '_ are different: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=5ab5c344201bafc4d771b91cff6c4ffa

We're trying to move away from eliding lifetimes in structs without a '_ marker because its very confusing for something with no lifetime markers to have anything but a 'static lifetime. Continuing to push those idioms should alleviate your concern.

5 Likes

Even Chalk doesn't let you quantify over traits, although it would indeed be easier to prototype there (if at all possible with its current design).

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.