Generic's help please

Hi, I'm confused as to why I'm running into the following problem. It's probably simpler to just show the code. I just don't understand why the resolve method can't figure itself out.

The actual use case has multiple implementations of Outer with different generic params in the DB, hence resolve references the generics.

Thanks!

#![allow(dead_code)]

use std::marker::PhantomData;

trait Inner<A, B> {
    fn do_inner(&self);
}

struct MyInner<A> {
    _a: PhantomData<A>
}
impl<A, B> Inner<A, B> for MyInner<A> {
    fn do_inner(&self) {
        println!("inner");
    }
}

struct Outer<A, B, C>
    where
        C: Inner<A, B>,
{
    x: C,
    _a: std::marker::PhantomData<A>,
    _b: std::marker::PhantomData<B>,
}
impl<A, B, C: Inner<A, B>> Outer<A, B, C> {
    fn do_outer(&self) {
        println!("outer");
        self.x.do_inner();
    }
}

struct DB {
    outer: Outer<String, usize, MyInner<String>>,
}
impl DB {
    fn resolve_subs(&self) -> &Outer<String, usize, MyInner<String>> {
        &self.outer
    }
    fn resolve<A,B,C: Inner<A, B>>(&self) -> &Outer<A, B, C> {
        // THIS FAILS TO COMPILE - WHY doesn't it correctly substitute it?
        // &self.outer
        todo!()
    }
}

#[test]
fn do_it() {
    let o: Outer<String, usize, MyInner<String>> = Outer { x: MyInner { _a: Default::default() }, _a: Default::default(), _b: Default::default() };
    let db = DB {
        outer: o,
    };
    db.resolve_subs().do_outer();
    db.resolve().do_outer();
}

This means that your function will return a &Outer<A, B, C> for whatever A, B and C the caller choose. However by returning &self.outer you're returning a &Outer<String, usize, MyInner<String>>, which only works when the user chooses those specific types. For example the user may call db.resolve::<i32, i32, MyInner<i32>>() and you would return the wrong type.

3 Likes

You didn't read the error message carefully enough. The error is not on the resolve method itself, it's when you try to call it.

Take the line in question:

db.resolve().do_outer();

So, first is the call to resolve. This requires three generic types. However, these types are not associated with the DB type, nor are they bound to any arguments. The only other place they appear is in the return value. So the compiler will try to infer those types from how the result is used.

Which brings us to the do_outer call. This also needs those three generic types, and also doesn't bind them to any arguments.

So the compiler has no way of knowing what those types are supposed to be. You could substitute pretty much anything, and it could work. Rather than guess, the compiler is demanding you specify, because explicit is (generally) better than implicit.

2 Likes

The error is on the resolve method:

error[E0308]: mismatched types
  --> analytocs-feeder-lib/src/lib.rs:49:9
   |
47 |     fn resolve<A,B,C: Inner<A, B>>(&self) -> &Outer<A, B, C> {
   |                - this type parameter         --------------- expected `&Outer<A, B, C>` because of return type
48 |         // THIS FAILS TO COMPILE
49 |         &self.outer
   |         ^^^^^^^^^^^ expected `&Outer<A, B, C>`, found `&Outer<String, usize, MyInner<...>>`
   |
   = note: expected reference `&Outer<A, B, C>`
              found reference `&Outer<std::string::String, usize, MyInner<std::string::String>>`

Here is the output I get from compiling the playpen you provided:

   Compiling playground v0.0.1 (/playground)
error[E0282]: type annotations needed
  --> src/lib.rs:54:8
   |
54 |     db.resolve().do_outer();
   |        ^^^^^^^ cannot infer type of the type parameter `A` declared on the method `resolve`
   |
help: consider specifying the generic arguments
   |
54 |     db.resolve::<A, B, C>().do_outer();
   |               +++++++++++

For more information about this error, try `rustc --explain E0282`.
error: could not compile `playground` (lib test) due to previous error
warning: build failed, waiting for other jobs to finish...

Edit: when you provide a playpen, make sure it actually produces the issue you're having, or you'll end up with people solving the wrong problem.

The error you just pasted is because you're trying to return types other than what the function signature says. Remember, when you say the resolve method has generic parameters <A, B, C> you do not get to pick them. The caller does.

If you're returning specific types, there's no need for generics.

3 Likes

"the caller choose". Ah, right - I thought the compiler would fill in the blanks based on the returned object, not the receiver.

Unfortunately, if I uncomment resolve and call it like db.resolve::<String, usize, MyInner<String>>().do_outer(); it still fails.

Rust generics are checked in isolation. The compiler neither knows nor cares how you use a generic, only whether the definition is correct in isolation.

To put it another way: if the compiler can't guarantee a generic is correct for all possible inputs, it's not correct at all.

3 Likes

Thanks @DanielKeep - I'm not sure what you mean about the playpen .. the problem I am trying to solve is making resolve work.

So what would the resolve type signature be?

So is what I'm trying to do impossible in Rust? I can achieve what I want using more traits if I need to, but I'm interested as to why this doesn't work...

My first response was written based on the code you provided. I opened up the playpen, hit compile, and read the error message. Essentially, you were asking about a specific problem, but provided code that exhibited a different problem.

Like I said, you want to double-check that the code you put in a post to make sure it exhibits the correct problem. :slight_smile:

It would be resolve_subs. You did say that there are multiple implementations of Outer with different generic parameters... but I have no idea what you're trying to accomplish. You absolutely can have methods whose generic parameters are independent of the self type, but without more context I can't advise you beyond "don't do this specific thing".

In general, it boils down to: the generic types have to be constrained by something somewhere.

2 Likes

I think I'm starting to get it.

Essentially I need to do something like:

struct DB {
    outer_a: Outer<String, usize, MyInner<String>>,
    outer_b: Outer<Vec<String>, usize, MyInner<HashMap<String, String>>>,
}
impl DB {
    fn resolve<A, B, C: Inner<A, B>> (&self) -> &Outer<A, B, C> {
        // I want this to return either outer_a or outer_b
        todo!()
    }
}

#[test]
fn do_it() {
    let a: Outer<String, usize, MyInner<String>> = Outer { x: MyInner { _a: Default::default() }, _a: Default::default(), _b: Default::default() };
    let b: Outer<Vec<String>, usize, MyInner<HashMap<String, String>>> = Outer { x: MyInner { _a: Default::default() }, _a: Default::default(), _b: Default::default() };
    let db = DB {
        outer_a: a,
        outer_b: b,
    };
    // I was hoping Rust magic would notice this can only be `outer_a` or `outer_b` and "fill in the gaps".
    db.resolve().do_outer();
}

Even in this case, it wouldn't work because you never indicate which Outer<A, B, C> you want from do_outer. Assuming the compiler was willing to "magically" pick one of the two (which it isn't), it still wouldn't have any way of knowing which one you wanted.

If your hope was to dynamically choose which one to return based on some runtime condition, that is straight up impossible. You can't have types chosen by runtime tests.

So, you have a few options.

1: Have the result of resolve be something like:

enum Either<X, Y> { Left(X), Right(Y) }

type OuterA = Outer<String, usize, MyInner<String>>;
type OuterB = Outer<Vec<String>, usize, MyInner<HashMap<String, String>>>;

impl DB {
    fn resolve(&self) -> Either<&OuterA, &OuterB> {
        todo!()
    }
}

This lets you decide at runtime, but callers have to check which one they got.

2: Use two different methods: one that returns outer_a, the other returns outer_b, and deal with the selection problem somewhere else.

3: Use a switch type:

trait Switch {
    type Output;
    fn select(&DB) -> &Self::Output;
}

enum SelectOuterA {
    type Output = OuterA;
    fn select(db: &DB) -> &OuterA { &db.outer_a }
}

enum SelectOuterB {
    type Output = OuterB;
    fn select(db: &DB) -> &OuterB { &db.outer_b }
}

impl DB {
    fn resolve<S: Switch>(&self) -> <S as Switch>::Output {
        <S as Switch>::select(self)
    }
}

Or something like that.

Basically: you have to decide who is responsible for making this decision and when. If it's made at compile-time, you can select the exact type. If it's runtime, you have to account for either possibility.

Edit: Oh, 4: have Outer implement some dynamic-compatible trait, and return &dyn OuterOps instead. That exchanges matching on an enum with dynamic call dispatch. The enum approach is probably faster, though.

2 Likes

Thanks @DanielKeep

1 Like