Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

where should I add an explicit lifetime bound?

Tags:

rust

The compiler tells me to add an explicit lifetime bound, but I can’t figure out how I’m supposed to that.

    error[E0309]: the parameter type `E` may not live long enough
      --> src/main.rs:39:9
       |
    34 | impl<S: Into<juniper::Value>, E: Into<juniper::FieldError>> Registrable for FieldInfo<S,E>
       |                               -- help: consider adding an explicit lifetime bound...: `E: 'a +`
    ...
    39 |         Box::pin(to_graphql((self.resolver)(executor)))
       |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
       |
    note: ...so that the type `impl std::future::Future` will meet its required lifetime bounds
      --> src/main.rs:39:9
       |
    39 |         Box::pin(to_graphql((self.resolver)(executor)))
       |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
server_1
    error[E0309]: the parameter type `S` may not live long enough
      --> src/main.rs:39:9
       |
    34 | impl<S: Into<juniper::Value>, E: Into<juniper::FieldError>> Registrable for FieldInfo<S,E>
       |      -- help: consider adding an explicit lifetime bound...: `S: 'a +`
    ...
    39 |         Box::pin(to_graphql((self.resolver)(executor)))
       |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
       |
    note: ...so that the type `impl std::future::Future` will meet its required lifetime bounds
      --> src/main.rs:39:9
       |
    39 |         Box::pin(to_graphql((self.resolver)(executor)))
       |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

This is the minimal code I came up with reproducing the error. It was working when ReturnType1 was not generic over V and E but I’m now trying to make it work with multiple types:

use juniper;

#[derive(Clone)]
struct Context {}

impl juniper::Context for Context {}

type ReturnType1<'a, V: Into<juniper::Value>, E: Into<juniper::FieldError>> = juniper::BoxFuture<'a, Result<V,E>>;
type InnerReturnType = juniper::ExecutionResult<juniper::DefaultScalarValue>;
type ReturnType<'a> = juniper::BoxFuture<'a, InnerReturnType>;

type Resolver<V: Into<juniper::Value>, E: Into<juniper::FieldError>> = for<'a> fn(
    &'a juniper::Executor<Context, juniper::DefaultScalarValue>
) -> ReturnType1<'a, V, E>;

async fn to_graphql<'a, V: Into<juniper::Value>, E: Into<juniper::FieldError>>(f: ReturnType1<'a, V, E>) -> InnerReturnType {
    f.await
        .map(|scalar| scalar.into())
        .map_err(|err| err.into())
}

trait Registrable: Send + Sync
{
    fn resolve<'a>(self: &Self, executor: &'a juniper::Executor<Context, juniper::DefaultScalarValue>) -> ReturnType<'a>;
}

struct FieldInfo<S, E>
where S: Into<juniper::Value>,
    E: Into<juniper::FieldError>
{
    resolver: Resolver<S,E>
}

impl<S: Into<juniper::Value>, E: Into<juniper::FieldError>> Registrable for FieldInfo<S,E>
where S: juniper::GraphQLType<TypeInfo=()> + Send + Sync
{
    fn resolve<'a>(self: &Self, executor: &'a juniper::Executor<Context, juniper::DefaultScalarValue>) -> ReturnType<'a>
    {
        Box::pin(to_graphql((self.resolver)(executor)))
    }
}

fn main() {}

Cargo.toml:

[package]
name = "pgql"
version = "0.1.0"
authors = ["Mathieu Rochette <[email protected]>"]
edition = "2018"

[dependencies]
juniper = { git = "https://github.com/graphql-rust/juniper", branch = "master" }

What’s bothering me is that the generic type is declared on the impl block but the issue seems to be on the fn within, so adding a lifetime at the impl level seems the wrong way to go.

If I try to add a where S: 'a, E: 'a clause on the fn resolve():

impl<S: Into<juniper::Value>, E: Into<juniper::FieldError>> Registrable for FieldInfo<S,E>
where S: juniper::GraphQLType<TypeInfo=()> + Send + Sync
{
    fn resolve<'a>(self: &Self, executor: &'a juniper::Executor<Context, juniper::DefaultScalarValue>) -> ReturnType<'a>
    where S: 'a, E: 'a {
        Box::pin(to_graphql((self.resolver)(executor)))
    }
}

it says the method does not match the trait declaration:

error[E0195]: lifetime parameters or bounds on method `resolve` do not match the trait declaration
  --> src/main.rs:37:15
   |
24 |     fn resolve<'a>(self: &Self, executor: &'a juniper::Executor<Context, juniper::DefaultScalarValue>) -> ReturnType<'a>;
   |               ---- lifetimes in impl do not match this method in trait
...
37 |     fn resolve<'a>(self: &Self, executor: &'a juniper::Executor<Context, juniper::DefaultScalarValue>) -> ReturnType<'a>
   |               ^^^^ lifetimes do not match method in trait

but I can’t add it on the trait since it does not know about S & E...

you can see the changes I’m trying to make in this PR : https://github.com/mathroc/pgql-rs/pull/1/files

like image 269
Mathieu Avatar asked May 06 '20 15:05

Mathieu


Video Answer


2 Answers

So, the await in to_graphql might resolve any time in the lifetime of f—and when that happens the ensuing into() calls obviously require data in the respective types to be valid. Ergo, to_graphql's type parameters V and E must outlive its lifetime parameter 'a. The borrow checker enforces this implicitly, as documented under async Lifetimes:

Unlike traditional functions, async fns which take references or other non-'static arguments return a Future which is bounded by the lifetime of the arguments:

You want to specify that your implementation block's generic type parameters S and E have such a lifetime, but as you have noted you cannot add such a constraint when it does not appear in the trait definition.

One possibility is to add a lifetime parameter to the Registrable trait itself, and on the resolve function specify that this new trait lifetime parameter is a subtype of (i.e. outlives) the function's lifetime parameter 'a:

trait Registrable<'b>: Send + Sync
{
    fn resolve<'a>(
        &self,
        executor: &'a juniper::Executor<Context, juniper::DefaultScalarValue>,
    ) -> ReturnType<'a>
    where 'b: 'a;
}

Then on your impl you can relate that lifetime parameter to your generic type parameters S and E:

impl<'b, S, E> Registrable<'b> for FieldInfo<S, E>
where
    S: juniper::GraphQLType<TypeInfo=()>,
    S: Send,
    S: Sync,
    S: Into<juniper::Value>,
    E: Into<juniper::FieldError>,
    S: 'b,
    E: 'b,
{
    fn resolve<'a>(
        &self,
        executor: &'a juniper::Executor<Context, juniper::DefaultScalarValue>,
    ) -> ReturnType<'a>
    where 'b: 'a
    {
        Box::pin(to_graphql((self.resolver)(executor)))
    }
}
like image 179
eggyal Avatar answered Sep 27 '22 19:09

eggyal


This is an interesting one. because we are implementing a trait, we can't add extra lifetime information to the function definition.

The issue at hand is that S and E are generic types, and therefore may contain references and hence be constrained by the lifetimes of those references. However they are generic and the compiler knows nothing about any constraints it may have.

At the same time, we are returning another generic type that depends on something with lifetime 'a and S and E. 'a is entirely unrelated to S and E, so the compiler looks and says: I can't tell if they live long enough because they are unrelated.

Now I haven't found the perfect way to describe this fully, however, we can make this work in a limited way with an easy code change - we can limit S and E to have static lifetimes, (which means they contain no references or only static references). This is usually sufficient in practice.

The code change in question is on FieldInfo:

struct FieldInfo<S, E>
where
    S: Into<juniper::Value> + 'static,
    E: Into<juniper::FieldError> + 'static,
{
    resolver: Resolver<S, E>,
}

I feel that there should be a way of expressing the lifetime relationships better, but I have yet to find it.

like image 45
Richard Matheson Avatar answered Sep 27 '22 18:09

Richard Matheson