Why do adding references to trait objects, on structs, cause lifetime issue

When adding {field-name}: Option<&'a dyn Fn(T) -> Result<...> + Send + Sync> to a struct type, lifetime requirements cause the program to fail (playground link at bottom of post):

(Problem field pub validators: Option<Vec<&'a Validator<T>>>, (comment it on and off to see the issue))

use std::fmt::{Display};
use std::borrow::Cow;

/// Represent some scalar values for test.
///
trait InputValue: Copy + Default + Display + PartialEq + PartialOrd {}

impl InputValue for isize {}
impl InputValue for usize {}
impl InputValue for &str {}

// ...
// Set up some types
// ----
type ViolationMessage = String;
type ValidationResult = Result<(), Vec<ViolationMessage>>;
type Validator<T> = dyn Fn(T) -> ValidationResult + Send + Sync;

/// Used for input validation
trait InputConstraints<T>
  where T: InputValue {
        
  fn _validate_t_strategy_1(&self, t: T) -> ValidationResult {
      println!("Incoming value: {}", t);
      
      Ok(())
  }
  
  fn validate(&self, value: Option<T>) -> ValidationResult {
      self._validate_t_strategy_1(value.unwrap())
  }
}

/// Extends `InputConstraints` in order to be used for validation; 
///  Instances expected to be long lived.
struct Input<'a, T>
    where T: InputValue 
{
    pub validators: Option<Vec<&'a Validator<T>>>,
}

impl<'a, T> InputConstraints<T> for Input<'a, T>
    where T: InputValue {}

/// Faux data model, for validation test
struct SomeModel {
    name: String
}

impl SomeModel {
    fn validate(&self, rules: &InputRules) -> ValidationResult {

        // mimick some lifetime redirection
        some_deep_ctx_validate_model_name(rules, self.name.as_str())
    }
}

fn some_deep_ctx_validate_model_name(rules: &InputRules, name: &str) -> ValidationResult {
    rules.name.validate(Some(name))
}

struct InputRules<'a, 'b> {
    name: Input<'a, &'b str>
}

fn app_runtime_ctx(rules: &InputRules, data: String) {
    let model = SomeModel {
        name: data,
    };
    model.validate(&rules);
}

fn main() {
    let rule: Input<&str> = Input {
        validators: None
    };
    let rules = InputRules {
        name: rule,
    };

    app_runtime_ctx(&rules, "Hello".to_string())
}

Errors:

   Compiling playground v0.0.1 (/playground)
warning: unused import: `std::borrow::Cow`
 --> src/main.rs:2:5
  |
2 | use std::borrow::Cow;
  |     ^^^^^^^^^^^^^^^^
  |
  = note: `#[warn(unused_imports)]` on by default

error: lifetime may not live long enough
  --> src/main.rs:59:5
   |
58 | fn some_deep_ctx_validate_model_name(rules: &InputRules, name: &str) -> ValidationResult {
   |                                      -----                     - let's call the lifetime of this reference `'1`
   |                                      |
   |                                      has type `&InputRules<'_, '2>`
59 |     rules.name.validate(Some(name))
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ argument requires that `'1` must outlive `'2`

warning: `playground` (bin "playground") generated 1 warning
error: could not compile `playground` (bin "playground") due to 1 previous error; 1 warning emitted

(Playground)

One of these is not like the others. It looks like you’re getting tripped up somehow on the fact that &'a str is not the same type as &'b str. If you use one of the other InputValue types, everything works as expected.


Edit: You can also make this compile by implementing InputValue for str, and then making sure that you only ever deal with an InputValue from behind a reference.

1 Like

In this implementation:

impl<'a, T> InputConstraints<T> for Input<'a, T>
    where T: InputValue {}

The signature of validate is:

fn validate(self: &Input<'a, T>, value: Option<T>) -> ValidationResult

and when T is &'b str, it is

fn validate(self: &Input<'a, &'b str>, value: Option<&'b str>) -> ValidationResult {

and the 'b lifetimes must match, or coerce to the same lifetime.

As a parameter of a dyn Trait, T is invariant (so the lifetimes can't coerce).

1 Like

Gotcha, makes sense:

  1. By moving Validator<T>/ValidatorRef<T> upward we remove some lifetime complexity and introduce some flexibility.
    2 . Seems the dream of building an Input type that can work for all scalar types is out of the question unless we force users to wrap their value in a wrapper type (ValidatorRef/Validator`, etc.).

Nice (at bit about Shush56105)!! Will analyze the updated code, and think about the problem I'm trying to solve, further.

Thank you for your help thus far!

P.S. source of the original problem (in case you're inclined to want to know what the intent of the original code was):

Thanks,
Ely

@2e71828 Right, gotcha, however since &str is also Copy that type should also work.

Revisiting this from a "what did you want to happen" view point, it's important to note that &str is not a type, it's a type constructor. So when you're dealing with &strs, you usually don't want a dyn Fn(&'b str), you want a dyn Fn(&str) -- aka dyn for<'any> Fn(&'any str) -- that can take a &str with any lifetime as an argument.

Type parameters like T in Validator<T> can only represent a single type (e.g. a &'b str with a single lifetime), not type constructors. So you need something different.

Something like:

type ValidatorRef<T> = dyn Fn(&T) -> ValidationResult + Send + Sync;

You would also need your Input<..> struct to support this; sometimes you'll want Validator<_> and sometimes you'll want ValidatorRef<_>. So let's move the generic up a level:[1]

-struct Input<'a, T> where T: InputValue {
-    pub validators: Option<Vec<&'a Validator<T>>>,
+struct Input<'a, V: ?Sized> {
+    pub validators: Option<Vec<&'a V>>,
 }

And adjust the trait implementations:

-impl<'a, T> InputConstraints<T> for Input<'a, T>
+impl<'a, T> InputConstraints<T> for Input<'a, Validator<T>>
 where
     T: InputValue
 {}

+impl<'a, T: ?Sized> InputConstraints<&T> for Input<'a, ValidatorRef<T>>
+where
+    for<'x> &'x T: InputValue,
+{}

(This produces a warning, which I will talk about below.)

And adjust InputRules<..> to match:

-struct InputRules<'a, 'b> {
-    name: Input<'a, &'b str>
+struct InputRules<'a> {
+    name: Input<'a, ValidatorRef<str>>,
 }

And finally adjust main.

 fn main() {
-    let rule: Input<&str> = Input {
+    let rule: Input<ValidatorRef<str>> = Input { validators: None };

This all compiles and runs (albeit with the aforementioned warning):


Ok, about the warning:

warning: conflicting implementations of trait `InputConstraints<&_>` for type `Input<'_, (dyn Fn(&_) -> Result<(), Vec<String>> + Send + Sync + 'static)>`
  --> src/main.rs:38:1
   |
37 |   impl<'a, T> InputConstraints<T> for Input<'a, Validator<T>> where T: InputValue {}
   |   ------------------------------------------------------------------------------- first implementation here
38 | / impl<'a, T: ?Sized> InputConstraints<&T> for Input<'a, ValidatorRef<T>>
39 | | where
40 | |     for<'x> &'x T: InputValue,
   | |______________________________^ conflicting implementation for `Input<'_, (dyn Fn(&_) -> Result<(), Vec<String>> + Send + Sync + 'static)>`
   |
   = warning: the behavior may change in a future release
   = note: for more information, see issue #56105 <https://github.com/rust-lang/rust/issues/56105>
   = note: this behavior recently changed as a result of a bug fix; see rust-lang/rust#56105 for details
   = note: `#[warn(coherence_leak_check)]` on by default

The thing it doesn't like is that you have these two implementations:

// When T = &'b str in the first implementation
impl<'a, 'b> InputConstraints<&'b str> for Input<'a, dyn Fn(&'b str) ...
// When T = str in the second implementation
impl<'a, 'b> InputConstraints<&'b str> for Input<'a, dyn Fn(&   str) ...

And it doesn't like it because dyn Fn(&str) is a subtype of dyn Fn(&'b str), and there was a period where they didn't want to allow implementing a trait for both the subtype and the supertype. That's what issue 56105[2] is about. However, if you read through the issue, they found that it broke too many things. As far as I know, the current plan is to keep accepting this case.

That said, it hasn't been formally committed to, so you're still taking a risk of future breakage if you go with this pattern.

Here's one workaround using a new-trait pattern:

trait Shush56105<T: ?Sized>: Fn(&T) -> ValidationResult + Send + Sync {}
impl<T: ?Sized, F: ?Sized> Shush56105<T> for F 
where
    F: Send + Sync + Fn(&T) -> ValidationResult
{}

type ValidatorRef<T> = dyn Shush56105<T>;

  1. Incidentally, you generally don't want bounds like T: InputValue on struct declarations unless it's strictly necessary. ↩︎

  2. linked from the warning ↩︎

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.