Alternative to Java's generic wildcards in Rust?

I have a generic trait that has two methods, one uses the type parameter while the other doesn't:

pub trait Map<E: Encoder> {
    fn keys(&self) -> &[&str];

    fn values(&self) -> &[&dyn Value<E>];
}

pub trait Value<E: Encoder> { /* ... */ }

I have another non-generic trait:

pub trait PrintKeys {
    fn print(&self);
}

Finally, I have a type that I want to implement this trait for:

struct MapWrapper<M>(M);

I want to use Map::keys in my implementation of PrintKeys, like so:

impl<M> PrintKeys for MapWrapper<M> {
    fn print(&self) {
        for key in self.0.keys() {
            println!(key);
        }
    }
}

But in order to be able to do this, I would need to put a M: Map constraint somewhere, and in order to be able to do that I need to introduce another type parameter, which would be unbounded. Ideally I would like to use something like Java's generic wildcards:

struct MapWrapper<M: Map<?>>(M);

I have tried a few approaches and am not sure which one would be the most idiomatic.

First approach: constraint on MapWrapper

struct MapWrapper<E: Encoder, M: Map<E>>(M);

However this gives me an unbounded type parameter error. I considered using PhantomData but I'm not sure that it's an idiomatic or good way to handle this problem.

Second approach: constraint on trait implementation

impl<E: Encoder, M: Map<E>> PrintKeys for MapWrapper<E> {
    fn print(&self) {
        for key in self.0.keys() {
            println!(key);
        }
    }
}

This also gives me an unbounded type parameter error, only this time there is no way to suppress it.

Third approach: splitting the generic trait

I could split my Map trait into two, one generic and another not:

pub trait MapKeys {
    fn keys(&self) -> &[&str];
}

pub trait MapValues<E: Encoder> {
    fn values(&self) -> &[&dyn Value<E>];
}

Then constraint either the MapWrapper or the implementation of PrintKeys for MapWrapper with MapKeys. However I don't like the idea of splitting this trait in two because it is part of a public interface and I think that it would make implementing it more confusing.

What would you recommend?

It's not possible to have a wildcard like in Java, because a type parameter means that the trait could be implemented multiple times. That is, there could be impl Map<Encoder1> for Foo and impl Map<Encoder2> for Foo, and then it is ambiguous which keys() implementation should be called until you specify the Encoder.

Here are some possible solutions, assuming you don't want that:

  1. If each Map implementation is expected to work with exactly one Encoder implementation, then you should define the encoder type as an associated type, not a generic type parameter. This is closer to the semantics of type parameters on Java interfaces.

    pub trait Map {
        type Enc: Encoder;
        fn keys(&self) -> &[&str];
        fn values(&self) -> &[&dyn Value<Self::Enc>];
    }
    
  2. If implementations of Map should work with any Encoder, then the type parameter should be on the method, not the trait:

    pub trait Map {
        fn keys(&self) -> &[&str];
        fn values<E: Encoder>(&self) -> &[&dyn Value<E>];
    }
    

    This means keys() is not dependent on the encoder type at all, and values() implementations are obligated to be generic over any possible Encoder.

  3. Split the trait as you proposed. This is similar to option (2), but allows MapValues to be either implemented for all Encoders or only specific Encoders. (See the next message for more on that too.)

6 Likes

Splitting the trait has a semantic implication. As things are now, a user can have a different keys implementation for distinct E: Encoders that they implement for. If you change this to one of

pub trait MapKeys { /* ... */ }
pub trait Map<E: Encoder>: MapKeys { /* ... */ }
// or
pub trait MapKeys { /* ... */ }
pub trait MapValues<E: Encoder>: { /* ... */ }
pub trait Map<E: Encoder>: MapKeys + MapValues<E> { /* ... */ }

Then all implementations of Map<E> for a given struct must share the same keys implementation. I'm just guessing based on names here, but I suspect that doesn't make sense: maybe I'm implementing your trait for two distinct encoders that I carry around in my struct, with distinct keys depending on the encoder.

If that is in fact the targeted use case, you probably want the PhantomData.

@kpreid covered a couple other use case possibilities.

4 Likes

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.