Understanding unused type parameters


#1

I’m tinkering with a for-fun port of Redux to Rust, to try to get a better handle on the way Rust handles generics and closures. I’ve run into a bit of a snag.

This works, but requires an additional temporary variable that I’d like to eliminate:

pub trait Reducer<S, A> {
    fn reduce(&self, state: &S, action: &A) -> S;
}

impl<S, A, F: Fn(&S, &A) -> S> Reducer<S, A> for F {
    fn reduce(&self, state: &S, action: &A) -> S {
        self(state, action)
    }
}

pub struct Store<'a, S, A> where
    S: 'a,
    A: 'a {
    state: S,
    reducer: &'a Reducer<S, A>,
}

impl<'a, S, A> Store<'a, S, A> {
    pub fn from_state(initial_state: S, reducer: &'a Reducer<S, A>) -> Store<'a, S, A> {
        // …
    }
}

The offending temporary:

fn add(state: &i32, action: &i32) -> i32 {
    state + action
}

let reducer = &add; // <-- cannot be inlined
let mut store = redux::Store::from_state(5, reducer);

This doesn’t compile, and complains about A:

// Reducer is unchanged.

pub struct Store<S, A, R: Reducer<S, A>> {
    state: S,
    reducer: R,
}
error[E0392]: parameter `A` is never used
 --> src/store.rs:8:21
  |
8 | pub struct Store<S, A, R: Reducer<S, A>> {
  |                     ^ unused type parameter

Which is a pity, because I was hoping I could do this and eliminate the temporary.

fn add(state: &i32, action: &i32) -> i32 {
    state + action
}

let mut store = redux::Store::from_state(5, add);

As far as I can see, the A type parameter is actually used in the latter definition of Store, but only in the definition of further type parameters. How would I satisfy the compiler, here?


#2

That can become

use std::marker::PhantomData;
pub struct Store<S, A,R: Reducer<S, A>> {
    state: S,
    reducer: R,
    _phantom:PhantomData<A>
}

https://is.gd/4iu3UP
The code I’ve been writing is very generic heavy, and I’m often needing to use this. It’s an unfortunate amount of verbosity for something which feels like it should be easily inferred by the compiler.


#3

@tupshin Thanks. The compiler makes the same suggestion. Adding a dummy field works, but I was (and am) hoping for something more elegant.

I had the thought that the compiler is right, and that A really isn’t used in the body of struct Store. It, and the constraint that R be some type implementing Reducer<S, A>, are only relevant to the impl, not to the struct.

This is legal:

pub struct Store<S, R> {
    state: S,
    reducer: R,
}

fn new<S, R, A>(state: S, reducer: R) -> Store<S, R>
    where R: Reducer<S, A> {
    Store {
        state: state,
        reducer: reducer,
    }
}

This is not, for reasons I’m not 100% clear on but willing to accept on faith:

impl<S, R, A> Store<S, R>
    where R: Reducer<S, A> {
    fn new(state: S, reducer: R) -> Self {
        Store {
            state: state,
            reducer: reducer,
        }
    }
}
error[E0207]: the type parameter `A` is not constrained by the impl trait, self type, or predicates
  --> src/store.rs:13:12
   |
13 | impl<S, R, A> Store<S, R>
   |            ^ unconstrained type parameter

I finally settled on this:

impl<S, R> Store<S, R> {
    fn new<A>(state: S, reducer: R) -> Self
        where R: Reducer<S, A> {
        Store {
            state: state,
            reducer: reducer,
        }
    }
}

This works, although I find myself repeating the R: Reducer<S, A> constraint on each method in the impl.


#4

Does anyone know if this missing inferring by the compiler is being looked at? It seems very amateurish that the compiler can’t see the “where” condition clearly using a parameter.


#5

The compiler could see it, but the reason it doesn’t has to do with unsafe code and variance. If you get variance wrong it can lead to huge problems.

There was an RFC to handle this but i don’t know where it went


#6

I’ll say the same thing I said in… (okay, I can’t find it now, but I swear this came up very recently.)

Anyways:

The problem

Let’s look at those two examples:

// Example that compiles
impl<S, R> Store<S, R> {
    fn new<A>(state: S, reducer: R) -> Self
        where R: Reducer<S, A> {
        Store {
            state: state,
            reducer: reducer,
        }
    }
}

// Example that does not compile
impl<S, R, A> Store<S, R>
    where R: Reducer<S, A> {
    fn new(state: S, reducer: R) -> Self {
        Store {
            state: state,
            reducer: reducer,
        }
    }
}

So what is the fully qualified pathname for a monomorphic call to new? In the good example, it’s

Store::<S, R>::new::<A>

whereas in bad example that doesn’t compile, it is

Store::<S, R>::new

…which is why the compiler says that A is unconstrained, because it plays no role in selecting monomorphizations of the function. That is, supposing that R: Reducer<S, A1> + Reducer<S, A2>, there is literally no way for the compiler to tell which type to use for A at a callsite…

The solution

Adding A to the type as suggested above makes the qualified name

Store::<S, R, A>::new

which is once again sound.

However, suppose you only need one particular type for A for any given pair of S, R. In this case, the problem is that the compiler is worrying about a conflict that will never happen. If that is the case, then there is also another solution:

Another solution

This is why associated types exist. Replacing A with an associated type guarantees that there will never be two conflicting impls with different A types.

trait Reducer<S> {
    // replace A with an associated type
    type Assoc;
}

impl<S, R> Store<S, R>
    where R: Reducer<S> {
    fn new(state: S, reducer: R) -> Self {
        Store {
            state: state,
            reducer: reducer,
        }
    }
}