Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Specify Rust closures lifetime

I was making the executor/reactor while discovered this a lifetime problem. It is not related to async/Future and can be reproduced without async sugar.

use std::future::Future;

struct Runtime;

fn start_with_runtime<C, F>(closure: C)
where
    C: for<'a> FnOnce(&'a Runtime) -> F,
    F: Future
{
    let rt = Runtime;
    let _future = closure(&rt);
    // block_on(future); 
}

async fn async_main(_rt: &Runtime) {
    // I can use _rt to do async stuff here
}

fn main() {
    start_with_runtime(|rt| { async_main(rt) });
}

I would like to the start_with_runtime() to run the future and provide the async Runtime reference as parameter.

It does not compile:

error: lifetime may not live long enough
  --> src/main.rs:17:31
   |
17 |     start_with_runtime(|rt| { async_main(rt) });
   |                         ---   ^^^^^^^^^^^^^^ returning this value requires that `'1` must outlive `'2`
   |                         | |
   |                         | return type of closure is impl std::future::Future
   |                         has type `&'1 Runtime`

I think that this problem seems to be because how rust infers lifetimes for closures:

https://github.com/rust-lang/rust/issues/58052 :

fn main() {
    let f = |x: &i32| x;
    let i = &3;
    let j = f(i);
}

Does not compiles either:

error: lifetime may not live long enough
 --> src/main.rs:2:23
  |
2 |     let f = |x: &i32| x;
  |                 -   - ^ returning this value requires that `'1` must outlive `'2`
  |                 |   |
  |                 |   return type of closure is &'2 i32
  |                 let's call the lifetime of this reference `'1`

Looks like my closure signature inferred as |&'a Runtime| -> impl Future + 'b and thus the lifetime error. I feel that given correct expected signature for closure would help, but how do I provide the correct signature in start_with_runtime?

fn start_with_runtime<C>(closure: C)
where
    C: for<'a> FnOnce(&'a Runtime) -> (impl Future + 'a),

Does not work because impl Trait is not allowed here.

fn start_with_runtime<C,F>(closure: C)
where
    C: for<'a> FnOnce(&'a Runtime) -> F,
    F: Future + 'a

Does not work as well because 'a is not known outside of HRTB expression.

It works if I know the type:


struct MyType<'a> {
    _rt: &'a Runtime
} 
fn start_with_runtime<C>(closure: C)
where
    C: for<'a> FnOnce(&'a Runtime) -> MyType<'a>,

This is kind of sad when you've thought through all the lifetimes but language does not provide a way to express this. Perhaps there is a trick in rust to make this work?

like image 558
orcy Avatar asked Aug 21 '20 05:08

orcy


People also ask

What is a lifetime specifier in Rust?

Lifetimes are what the Rust compiler uses to keep track of how long references are valid for. Checking references is one of the borrow checker's main responsibilities. Lifetimes help the borrow checker ensure that you never have invalid references.

Why are lifetimes needed in Rust?

Explicit lifetimes allow the compiler to ensure that your program is doing what you intended. If you (the compiler) want(s) to check if a piece of code is valid, you (the compiler) will not have to iteratively look inside every function called.

What is the type of a closure rust?

A closure type is approximately equivalent to a struct which contains the captured variables. For instance, the following closure: #![ allow(unused)] fn main() { fn f<F : FnOnce() -> String> (g: F) { println!

Why Rust closures are somewhat hard?

Rust closures are harder for three main reasons: The first is that it is both statically and strongly typed, so we'll need to explicitly annotate these function types. Second, Lua functions are dynamically allocated ('boxed'.)


1 Answers

There seem to be two different questions in this one: can the required relationship be expressed in Rust syntax and will it work with closure type inference or not.

Let's start with the first one. You're right that this cannot be expressed with just where clauses. To express this one needs to add a helper trait

trait BorrowingFn<'a> {
    type Fut: std::future::Future<Output = Something> + 'a;
    fn call(self, arg: &'a Runtime) -> Self::Fut;
}

that allows the bound we need to be written as

    C: for<'a> BorrowingFn<'a>,

and provide a blanket implementation of this trait for all applicable functions

impl<'a, Fu: 'a, F> BorrowingFn<'a> for F
where
    F: FnOnce(&'a Runtime) -> Fu,
    Fu: std::future::Future<Output = ()> + 'a,
{
    type Fut = Fu;
    fn call(self, rt: &'a Runtime) -> Fu {
        self(rt)
    }
}

(playground)

Ok, so it works with an async function, but does it work with a closure that needs type inference? Unfortunately, the answer is "no"

error: implementation of `BorrowingFn` is not general enough
  --> src/main.rs:33:5
   |
5  | / trait BorrowingFn<'a> {
6  | |     type Fut: std::future::Future<Output = ()> + 'a;
7  | |     fn call(self, arg: &'a Runtime) -> Self::Fut;
8  | | }
   | |_- trait `BorrowingFn` defined here
...
33 |       start_with_runtime(|rt| async_main(rt)); // however, it does not work with closure type inference :-(
   |       ^^^^^^^^^^^^^^^^^^ implementation of `BorrowingFn` is not general enough
   |
   = note: `[closure@src/main.rs:33:24: 33:43]` must implement `BorrowingFn<'0>`, for any lifetime `'0`...
   = note: ...but `[closure@src/main.rs:33:24: 33:43]` actually implements `BorrowingFn<'1>`, for some specific lifetime `'1`

This is being tracked in rust-lang/rust#70263. The compiler is not smart enough yet to notice that this closure needs a higher-rank type. For fun I tried compiling with -Z chalk on Nightly, but it's not yet ready for this (internal compiler error).

like image 58
Tanriol Avatar answered Sep 21 '22 21:09

Tanriol