Problem with closures "holding" values unexpectedly

A higher-order lifetime ceases to be so

So, all this stems from a very basic question, or observation, about a probably hand-wave but critical detail here: what is the signature of f = first?

  • the first closure is defined as |vals: &[i32]| vals[0], which means its signature is that of:

    Fn(&[i32]) -> i32
    

    that is:

    for<'any> Fn(&'any [i32]) -> i32
    
  • the f argument must have a signature of Fn(A) -> B, meaning there must exist types A, and B so that impl Fn(&[i32]) -> i32 : Fn(A) -> B.

    We'd be tempted to say that B = i32, and A = &[i32] fit the situation, except &[i32] is not a type, not a complete type I mean: there is a missing lifetime parameter in there.

So we end up with: A = &'? [i32], and thus:

// find `'?` so that:
impl for<'any> Fn(&'any [i32]) -> i32
   : Fn(&'? [i32]) -> i32

It turns out that while any choice of '? is satisfiable here, e.g., A = &'static [i32], it will nonetheless be a fixed lifetime choice :warning:

So we end up with:

                                                           // A = &'inferred [i32]
let first_plus_one: impl Fn(&'inferred [i32]) -> i32 = compose(first, increment);

So, already, notice how your resulting composed closure has lost the for<'any> universal quantification of its lifetime / its "being lifetime-generic", and, instead, wants an input with some very specific 'inferred duration.

A lifetime-infected type

If we now look at your compose function for your specific case, its signatures ends up being:

fn compose<'inferred, 'f, 'g, 'ret>(
    f: impl 'f + Fn(&'inferred [i32]) -> i32,
    g: impl 'g + Fn(i32) -> i32,
) -> impl 'ret + Fn(&'inferred [i32]) -> i32
where
    'inferred : 'f, // `f` captures `'inferred`, so for `f` to be usable, `'inferred` must not have ended yet
    'f : 'ret, // ret value catpures `f`, so for it to be usable, `f` must be usable too.
    'g : 'ret, // ret value captures `g`, so for it to be usable, `g` must be usable too.
  • Intuitively, 'ret will be the intersection of the 'inferred, 'f, and 'g regions.

The closures in your example don't capture anything (but the fixed 'inferred lifetime), so we can further simplify all this down to:

fn compose<'inferred>(
    f: impl 'inferred + Fn(&'inferred [i32]) -> i32,
    g: impl 'static + Fn(i32) -> i32,
) -> impl 'inferred  + Fn(&'inferred [i32]) -> i32

And now, the interesting thing is, we end up with:

-> impl ... 'inferred ...

That is, an existential lifetime-infected type (note: for dyn this would be the case as well).

Since 'inferred is part of this type, for (instances of) the type to be usable, 'inferred must not have ended yet.

  • This is a classic rule of Rust. For instance, consider:

    let mut s;
    {
        let local = String::from("...");
        s = Some(&local);
        s = None;
    }
    drop(s); // Error, `local` already dropped.
    

    This will fail because the type of s, Option<&'inferred String>, requires that 'inferred not dangle when we drop(s);, since that constitutes a usage of an instance of the type (and the snippet fails because 'inferred must fit within the lifetime of the borrow of local for the assignment to be valid, but local dies too soon).

And yet, both these things, alone, would not cause your compile error, since you do not drop or otherwise explicitly use increment_then_double after vals is dropped.

The last issue, and which ultimately causes the compile error, is that:

An -> impl ... type has conservative drop glue / no NLL

That is, if we look at our -> impl ... 'inferred ... existential return type, by virtue of being so (type-erased / implementation-agnostic yadda yadda), there could be implementations of this type where instances of it could be using 'inferred within the implicit drop glue, as far as rust is concerned.

That is, for -> impl ... types, as well as for dyn ... types, "going out of scope" constitutes a use, which therefore requires the lifetimes not to dangle:

    let first_plus_one = compose(first, increment); // may have / conservatively has drop glue.
    {
      let vals = vec![5, 8, 9];

      let result = first_plus_one(&vals); // `'vals : 'inferred` for this call to be OK.

      assert_eq!(6, result);
    } // drop(vals);
} // drop(first_plus_one); /* Conservative drop glue means `'inferred` must be valid here! */

Hence the error.

5 Likes