How to name a lifetime coming from a trait method but appearing in a type parameter bound

I'm stuck on a problem with naming lifetimes.

I have the following trait:

pub trait Matcher {
    type ActualT: Debug + ?Sized;

    fn matches(&self, actual: &Self::ActualT) -> bool;
}

I have implemented this for a struct called IsEncodedStringMatcher. This struct makes reference to another nested Matcher implementation:

struct IsEncodedStringMatcher<ActualT, InnerMatcherT> {
    inner: InnerMatcherT,
    phantom: PhantomData<ActualT>,
}

The nested Matcher must match against string slices, which are constructed in the matches implementation:

    fn matches(&self, actual: &Self::ActualT) -> bool {
        std::str::from_utf8(actual.as_ref()).map(|s| self.inner.matches(&s)).unwrap_or(false)
    }

To implement this, there must be a trait bound on InnerMatcherT specifying that it implements Matcher and that it's associated ActualT is a string slice. I must then specify a lifetime for that string slice.

impl<ActualT, InnerMatcherT> Matcher for IsEncodedStringMatcher<ActualT, InnerMatcherT>
where
    ActualT: AsRef<[u8]>,
    InnerMatcherT: Matcher<ActualT = &'actual str>,
    //                                ^^^^^^^ What goes here?
{...}

But that lifetime must actually come from the parameter actual to the matches method, which I can't reference from the impl's block trait bounds. So I don't know how to name that lifetime.

Thus far I've tried:

  • Using the for <'actual> construct. But then one can't use the matcher, since any candidate inner matcher will be bound to a specific lifetime for ActualT.

  • Adding the lifetime parameter as a parameter to the struct itself. But then the lifetime on the struct has no relationship to that of the actual parameter on the matches method.

  • Adding a lifetime parameter to the Matcher trait itself. But this makes the lifetimes too rigid. Other parts of the same library create local variables and run the inner matcher against those.

    For example:

    struct DisplayMatcher<ActualT, InnerMatcherT> {
        inner: InnerMatcherT,
        phantom: PhantomData<ActualT>,
    }
    
    impl<'a, ActualT: Debug + Display + 'a, InnerMatcherT> Matcher<'a> for DisplayMatcher<ActualT, InnerMatcherT>
    where
        InnerMatcherT: Matcher<'a, ActualT = String>,
    {
        type ActualT = ActualT;
    
        fn matches(&self, actual: &'a Self::ActualT) -> MatcherResult {
            self.inner.matches(&format!("{actual}"))
        }
    }
    

    This fails with:

    error[E0716]: temporary value dropped while borrowed
      --> src/lib.rs:28:29
       |
    28 |         self.inner.matches(&format!("{actual}"))
       |         --------------------^^^^^^^^^^^^^^^^^^^-
       |         |                   |
       |         |                   creates a temporary value which is freed while still in use
       |         argument requires that borrow lasts for `'a`
    29 |     }
       |     - temporary value is freed at the end of this statement
       |
    

This feels like the kind of problem generic associated types were intended to solve, but I don't see how to use them to solve this.

Any guidance would be appreciated. Thanks!

Please see also a playground.

For types appearing in function arguments, not return values, of trait methods, making them an a generic type argument to the trait instead of an associated type can often be a good choice.

This could also help your use-case here since a for<'a> …: Matcher<ActualT = &'actual …> bound of course runs into the problem that one type cannot support multiple ActualT associated types, whereas with a type parameter, it can. Also, your implementation based on PartialEq uses the type parameter of PartialEq, i.e. that’s a generic parameter, too, not an associated type.

So far for the theory, let me not try doing the actual refactor and see if switching from associated type to generic argument does work in your actual playground you shared, or if there's any issues that pop up where the associated type was beneficial.


So the first try of just making it a type parameter of the trait and rewriting the trait impls and trait bounds accordingly doesn't quite work yet. Makes sense.. EqMatcher still defines a single, concrete ActualT value. Removing that still doesn't work. The problem is the eq function still requiring a concrete choice of ActualT parameter and returning an impl Trait return type for that. If we just remove the parameter (and the PartialEq bound at that place) and make its return type explicit, now it compiles :tada:. I’m not sure there'd be any good way to write this in terms of a impl Trait return type though, but maybe it isn’t too bad?

1 Like

It feels like the appropriate trait bound should be Matcher<str> instead of Matcher<&str> since the trait requires taking a temporary reference anyway, but my mind has been drawing a blank on how to write the trait bound for *OwnedT: PartialEq<ActualT>.


Edit: The closest I’ve gotten is this, but it changes the semantics slightly and occasionally requires a turbofish at the call site.

Ah, something like this?

struct EqDerefMatcher<RefExpectedT> {
    expected: RefExpectedT,
}

impl<ActualT: ?Sized, RefExpectedT> Matcher<ActualT> for EqDerefMatcher<RefExpectedT>
where
    RefExpectedT: std::ops::Deref,
    RefExpectedT::Target: PartialEq<ActualT>,
{
    fn matches(&self, actual: &ActualT) -> bool {
        &*self.expected == actual
    }
}

fn eq_deref<RefExpectedT>(expected: RefExpectedT) -> EqDerefMatcher<RefExpectedT> {
    EqDerefMatcher { expected }
}
impl<ActualT, InnerMatcherT> Matcher<ActualT> for IsEncodedStringMatcher<ActualT, InnerMatcherT>
where
    ActualT: AsRef<[u8]>,
    InnerMatcherT: Matcher<str>, // !!
{
    …
}

fn main() {
    assert!(is_encoded_utf8(eq_deref("An encoded string")).matches(&value));
}

(I haven't actually used this change to change it back to an impl Trait return type; the usage of Matcher<str> has other benefits, too, anyways, as it could also support e.g. eq(s) for s: String this way, since things like String and Box<str> are PartialEq<str> whereas &str is PartialEq<&str>.)

1 Like

Thanks for the suggestion. It looks as though avoiding the reference in ActualT is the way to go. I think I'll pursue a solution along these lines.

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.