How to specify that two generic type parameters are the same type but have different lifetimes

I am trying to use phf to create a compile time made hashmap, but this requires that the generic type has a static lifetime. I need to compare against the output of this hashmap, but it isn't working because (in my understanding) they are no longer the same type now. Something akin to this:

fn some_func<A: SomeTrait + PartialEq, B: SomeTrait + PartialEq + 'static>(map: phf::Map<&'static str, B>, a_val: A) {
    let b_val = map.get("some val").unwrap();
    if a_val == b_val { // causes error here
        ...
    }
}

From my googling on this issue it seems that in the past it likely wasn't possible to do this directly (e.g. this stackoverflow link), but I am curious to know if this has changed since then. I believe I can manually implement partialeq between the two types for now to workaround this, but I was wondering if there was a way to directly specify that these two types are in fact the same, just differing in lifetime

In Rust, in order to compare two types with the == operator, an implementation of PartialEq for such types must exist.

I'm aware, I've edited the original post to include the trait explicitly. I have a lot of trait bounds so I've enabled trait aliases, seems I've gotten a little too used to have everything under a "single trait". My apologies, I was just trying to quickly come up with an abstract example before. I still have the same problem as before though because I don't know how to specify that the two types are the same type and should only differ in lifetime

To compare A with B, A: PartialEq<B> must hold. Function bounds are asserting A: PartialEq<A> and B: PartialEq<B> though, which allows A=i32, B=String which cannot be compared to each other.

Then you face another series of problems:

  • Traits in Rust are not types
  • Traits that take Self (Such as PartialEq) are not dyn-compatible

Here's a neat example of a workaround, courtesy of @quinedot: How to support `derive(PartialEq)` for "dyn PartialEq"`? - #3 by quinedot

Which only means that the type must be self-contained, and not & any values outside of itself. Types don't have or "possess" lifetimes, 'static or otherwise, per se: they can only be valid and/or well defined for the duration of a given 'l. Which is generally the shortest lifetime of the reference they happen to borrow, in the case of Struct<'short, 'longer, 'static> or similar.

In your case, if I'm reading this well, what you want is as simple as:

fn some_func<T>(map: phf::Map<&'static str, T>, a_val: &T) 
where T: PartialEq + 'static {
    let b_val: &'static T = map.get("some val").unwrap();
    if a_val == b_val { // does this still throw an error?
        ...
    }
}

I (believe I) need owned values due to the way I'm parallelizing the work, however, I think I found a way to do what I wanted to. Completely by accident while throwing stuff at the wall and seeing what stuck, but regardless. I believe? if you attach the bounds onto a struct, then put the same type into both generic parameters, the compiler will then recognize them as the same type. As an example:

// Example with plausible trait bounds
struct Example<A: Num + Unsigned + PrimInt + PartialEq, B: Num + Unsigned + PrimInt + PartialEq + 'static> {
    mapping: phf::Map<&'static str, B>,
    a_val: A
}

fn some_func<T: Num + Unsigned + PrimInt + PartialEq>(input: &'static str, an_example: Example::<T,T>) {
    let mapping = an_example.mapping;
    let a_val = an_example.a_val;
    let b_val = *mapping.get(input).unwrap();
    if a_val == b_val { // This no longer throws an error
      ...
    } 
}

I'd like to take a look at compiler explorer and see what the output looks like doing this, but it seems to achieve what I was hoping for

You don't need the trait bounds on the struct, you could use

struct Example<A, B: 'static> {
    mapping: phf::Map<&'static str, B>,
    a_val: A
}

// `T: 'static` is implied due to the lifetime bound on `struct Example`
fn some_func<T: Num + Unsigned + PrimInt + PartialEq>(
    input: &'static str,
    an_example: Example::<T,T>
) {

Note that you only have one type T here -- they have to be the same including any lifetime parameters. And it has to meet a 'static bound, which I'm not sure you want.

In the OP, this is as if you did:

fn some_func<B: SomeTrait + PartialEq + 'static>(
    map: phf::Map<&'static str, B>, 
    a_val: B
) {

Which also works; it's a fix to what @ProgramCrafter was referring to.


So the code compiles now, but this is not a solution to the title of this thread as I read it:

How to specify that two generic type parameters are the same type but have different lifetimes

I read that as:

How do I write generic bounds to specify A = Thing<'a> and B = Thing<'static>?

(How to specify that two generic type parameters are the same type constructor but have different lifetime parameters)

But maybe you meant something different? I'm not sure.

Yeah that checks out, you're right that static becomes an implied bound. I did not know/think about that being possible behavior. I think my confusion here was at least partially coming from the editor I'm using, Zed. It's a bit misleading here, because if you hover over Example and display the type it shows:

struct Example<A, B>
where
    A: Num + Unsigned + PrimInt + PartialEq,
    B: Num + Unsigned + PrimInt + PartialEq + 'static,
{
    mapping: Map<&'static str, B>,
    a_val: A,
}

Made me think it had worked the way I'd thought it would. I probably got into this rabbit hole when I got an error about not having a static lifetime bound, and created a separate generic bound to add it to rather than adding it onto the original one

This is a better way to frame the question yes

Okay, and just to be sure, you do have some type that you know does not meet a 'static bound but does satisfy your other bounds like PrimInt? I'm asking because the num trait only implements PrimInt for the, well, primitive integer types, which all meet a 'static bound, and

The intention is that all types implementing this trait behave like primitive types that are passed by value by default and behave like builtin integers.

(A static lifetime bound doesn't mean a value lives forever, so String: 'static for example. Sometimes people get confused about this.)


If you're sure, the short answer is that Rust doesn't have "generic type constructors"...

fn non_existent_feature<T<'..>: SomeTrait + PartialEq<T<'..>>(
    map: phf::Map<&'static str, T<'static>>,
    a_val: T<'_>,
)

...but sometimes you can work around it for set of types using a GAT or similar...

trait Trait {
    type Ty<'a>: 'a + for<'b> PartialEq<Self::Ty<'b>> + SomeTrait;
}

fn foo<Repr: Trait>(
    map: phf::Map<&'static str, Repr::Ty<'static>>,
    a_val: Repr::Ty<'_>,
)
{
    map.get("").unwrap() == &a_val;
}

...but on the other hand this is not a fully general solution and so may not be what you want either.

Is there any reason why you can't provide the type [constructor] to the function itself, then?

#[derive(Debug, PartialEq)]
struct Type<'p, T: ?Sized>(&'p T);

fn some_func<'a, T: SomeTrait + PartialEq + ?Sized>(
    a_val: &Type<'a, T>, // A: 'scope
    map: phf::Map<&'static str, Type<'static, T>> // B: 'static
) {
    let Some(b_val) = map.get("val") else {
        unreachable!();
    };
    // no compile error here
    if b_val == a_val { }
}

For any TypeConstructor<'a, 'b, 'c, ..> you can simply extend the signature to account for all the lifetimes involved in your "type constructor's capture". On the other hand, if you need to keep the type generic as well, I don't think there's way around it without custom traits and GAT's, as @quinedot mentioned. Link to the playground for the example above here.

I guess I'll also throw out that this is maybe closer to what you were looking for...

fn some_func<
    A: SomeTrait + PartialEq<B>,
    B: SomeTrait + 'static
>(
    map: phf::Map<&'static str, B>, 
    a_val: A
) {
    let b_val = map.get("some val").unwrap();
    if a_val == *b_val { // causes error here
    }
}

...but you may find that it also has some limitations.