Type parameter not used on enums

Currently there are some problems with type parameters on enums:

enum Msg<R: Route, P: Page<R>> {
    Page(<P as Component>::Msg),
    UrlChanged(subs::UrlChanged),
}

throws R: parameter is never used, but it is required for the P: Page<R> trait bound.

Actually this same problem exists for structs, but here it can be easily mitigated:

struct Foo<R: Route, P: Page<R>> {
    page: <P as Component>::Msg,
    _ty: std::marker::PhantomData<R>, // use R in phantom data field

}

For enums, there seems to be no good solution. The best I found is:

enum Msg<R: Route, P: Page<R>> {
    Page(<P as Component>::Msg),
    UrlChanged(subs::UrlChanged),
    _Unreachable(std::marker::PhantomData<R>),
}

But the _Unreachable variant should never be used, and there is no easy way to enforce this, and all of this still requires us to use this variant in any exhaustive patterns.

discussion on the user forum

It would be nice if the compiler could recognize that the type parameter is used by a trait bound. It would also be useful to be able to disallow enum variants from being used at all.

1 Like

Playing around on play.rust-lang.org it seems that there is an unstable feature "Associated type bounds" (https://github.com/rust-lang/rust/issues/52662) which seems like it might solve your problem.

1 Like

Thank you I will try it out soon

You could put a ! (or std::convert::Infallible) in it, which would keep it from ever being constructed. (That of course doesn't solve the latter problem.)

Can you elaborate why you need the bound on the type? Can it just go on the methods instead?

I am writing a generic Router type which should serve as a component in a frontend framework.

pub struct Router<R: Route, P: Page<R>> {
    page: P,
    _ty: std::marker::PhantomData<R>,
    ...
}

It should store the current page. Pages also implement Component. Components communicate via messages which are propagated through the component tree.

This is why the Router message type Msg has to be able to store messages to its page children:

pub enum Msg<R: Route, P: Page<R>> {
    Page(<P as Component>::Msg), // <- message to child page
    UrlChanged(subs::UrlChanged),
    GoTo(R), // <- this actually solved the issue for me now :D
}

so the Msg type must be generic, as far as I can tell.

(The issue is solved for me now, because I found a variant I need that actually uses R. But the general problem remains.)

Expanding on the previous comments that say to use std::convert::Infallible, here is how you would pattern match on that enum:

enum Msg<R: Route, P: Page<R>> {
    Page(<P as Component>::Msg),
    UrlChanged(subs::UrlChanged),
    _Unreachable(std::convert::Infallible, std::marker::PhantomData<R>)
}

impl Msg {
    fn process(msg: Msg) -> Response {
        match msg {
            Msg::Page(x) => x.process(),
            Msg::UrlChanged(x) => x.process(),
            Msg::_Unreachable(x, _) => match x {},
        }
    }
}

The Msg::_Unreachable(x, _) => match x {}, is boilerplate that can be repeated everywhere that enum is matched.


Also, here's a compilable example of what I did there with Infallible: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=e98798e77ea2bca37494bb3680abe0a6

pub enum Foo<T, U>{
    Bar(Vec<T>),
    _Impossible(std::convert::Infallible, std::marker::PhantomData<U>),
}

pub fn foo<T, U>(foo: Foo<T, U>) -> String
where
    T: std::fmt::Debug
{
    match foo {
        Foo::Bar(list) => format!("{:#?}", list),
        Foo::_Impossible(x, _) => match x {},
    }
}

Is there some reason you can't just use enum Msg<P: Component>?

2 Likes

Well, instead of putting the PhantomDatainto a completely new variant, why don't you put it into one of the existing variants?

enum Msg<R: Route, P: Page<R>> {
    Page(<P as Component>::Msg, PhantomData<R>),
    UrlChanged(subs::UrlChanged),
}
3 Likes

The Msg::_Unreachable(x, _) => match x {}, is boilerplate that can be repeated everywhere that enum is matched.

If you're on nightly you can also use #[feature(exhaustive_patterns)] which makes this unnecessary. You can just omit the branch for _Unreachable.

3 Likes

No, that would actually work in this case. But there is a reason why I use the Page<R> trait and that is to define a set of types that can be used here, and they might get tighter requirements as my code develops. In this case simply using Component would work, but it would not really make sense because Router<R, P> requires P: Page<R> already. So this would really only be a workaround.

Also a good way of mitigating this.

In general though, I think Rust should recognize that R is being used in another trait bound. What is the reason why it is not doing that right now?

Related: Rust complains about unused type parameter which is only used to parameterize another #23246

It is a common pattern to defer strong bounds to implementations. So in this case, you could demand Msg<P: Component> on definition, and only add impl<R: Route, P: Page<R>> in the impls where you need this.

This also makes sense: if you're not using R in the definition, why are you declaring it there? Note that adding PhantomData might change your code's behaviour (re: Drop), and rightly so. That's it's purpose, not to quiet the "unused types" warning. That said, if you do want to use it that way, it's probably better to use some weaker type, like PhantomData<&R>, or maybe even PhantomData<fn(R)->R>.

1 Like

But the "unused type parameter" is an error, not a warning. That is why it is a problem, and that is why I am advocating to remove that error.

There doesn't seem to be an RFC for this, but I think it would really help users of the higher level type system. I also don't understand the reason why this should be an error. @alexcrichton said it was implemented to ensure that the compiler knows which constraints it can put on the type parameter. But I don't understand why these can not be derived by a trait bound for another type parameter.

I think the main issue is that R is not well defined. If you have more than one R: Router where also P: Page<R>, which one is the one relevant for this instance? This question can't be answered, because R is not actually relevant to the enum. It is, however, relevant for the method, so that's where it needs to be decided, with which R are you trying to run the method.

1 Like

I mean, it works now, with some workarounds. But it isn't really pretty and carries a maintenance cost, also for structs, because you have to include the PhantomData field in all constructions and some patterns.

It would be great if this burden could be carried by the compiler. If there are multiple parameters i.e. <R1: Route, R2: Route, P1: Page<R1>, P2: Page<R2>> the compiler can just treat it like there is a PhantomData for each parameter, if that is necessary. There are not really any decisions to be made here, as far as I can tell. All that is needed would be a design proposal and someone to make the changes to the compiler. I would love to do this, but I have never worked on the rust compiler. It does seem like a good starting point though.. Maybe I will take some time for it.

There are decisions to make, and that's what makes e.g. PhantomData<T> different from e.g. PhantomData<fn() -> T>: Subtyping and Variance - The Rustonomicon

The correct way to communicate to the compiler that you just want P to inform the storage of Msg<R, P> is to make the type just Msg<P>.

You'll find that most types in Rust under-bound their storage definition and only add most relevant bounds for impl blocks. (Whether that's desirable is a different question; I flip-flop often, and it's highly dependent on the individual type.)

But defining Msg as enum Msg<P: Component> would also require that other types in Msg may only expect Component from P, nothing more. For example, maybe Page has an associated type Title and I want to have a Msg::SetTitle(<P as Page<R>>::Title) to set the title of a page. I would have to define enum Msg<R: Route, P: Page<R>> to access that. That is just one example where relaxing the trait bound would not work.

Thanks for the link, I will read into it and try to understand better.

I didn't mean multiple parameters. If we have:

struct MyPage;
struct SomeRouter;
struct OtherRouter;
trait Page<T> { }
impl Page<SomeRouter> for MyPage { }
impl Page<OtherRouter> for MyPage { }

And you have your enum which is defined as

enum MyEnum<R, P: Page<R>> { ... }

Is the current instance of the enum a MyEnum<SomeRouter, MyPage> or a MyEnum<OtherRouter, MyPage>? If R isn't used anywhere, it doesn't make sense to distinguish between those two types. The instance is just MyEnum<MyPage>, and then it can be called with the method .route<SomeRouter> or .route<OtherRouter>.

1 Like

It could make a difference. Consider this:

trait Page<R> {
    type Title: Into<R>;
}
struct MyPage;
struct A;
struct B;
impl Page<A> for MyPage {
    type Title = A;
}
impl Page<B> for MyPage {
    type Title = B;
}
enum Msg<R, P: Page<R>> {
    SetTitle(<P as Page<R>>::Title),
}

Here Page's associated type is used in the enum, and is dependent on its type parameter R. So R is in fact constraining the type of Msg, in that SetTitle has to contain a Page<R>::Title which has to be Into<R>.

So Msg<A, MyPage> and Msg<B, MyPage> are not generally equivalent.

1 Like

Given the current state of the language (and the lack of implied bounds), I think it's currently clearly good. Otherwise one needs to repeat so many bounds all over the place.

For example, it's really nice that I can debug-print a HashSet<T> with just T: Debug now -- when I used to have to say T: Eq + Hash too it was just pointless noise that kept #[derive] from working.

(There are language changes that could affect things here -- like the previously-mentioned implied bounds -- but I've been skeptical of the proposals thus far because of the semver implications. Maybe one day someone will come up with an awesome fix, though.)