Impl specialization


#1

Hi!
So I am trying to emulate something akin to C++'s explicit template specialization in Rust :smile:

The idea of the code below is to have a set of functions (f1 and f2 here) which are defined only for certain combinations of type parameters T and U. On top of that, it should be possible to write generic functions that use f1 and f2 internally (bar).

trait Foo {
    type T;
    type U;
    fn f1(Self::T);
    fn f2() -> Self::U;
}
impl Foo for (i32,u32) {
    type T = i32;
    type U = u32;
    fn f1(x: i32) {}
    fn f2() -> u32 { 1 }
}
impl Foo for (i64,u64) {
    type T = i64;
    type U = u64;
    fn f1(x: i64) {}
    fn f2() -> u64 { 2 }
}
// Should work for (A,B) being either (i32,u32) or (i64,u64).
fn bar<A,B>(x: A) -> B where (A,B): Foo {
    <(A,B) as Foo>::f1(x);
    <(A,B) as Foo>::f2()
}
fn main() {
    let x:u32 = bar(1_i32);
    let y:u64 = bar(2_i64);
}

Sadly, this doesn’t compile:

 <anon>:21:24: 21:25 error: mismatched types:
 expected `<(A, B) as Foo>::T`,
    found `A`
(expected associated type,
    found type parameter) [E0308]
<anon>:21     <(A,B) as Foo>::f1(x);
                                 ^
<anon>:21:24: 21:25 help: see the detailed explanation for E0308
<anon>:22:5: 22:25 error: mismatched types:
 expected `B`,
    found `<(A, B) as Foo>::U`
(expected type parameter,
    found associated type) [E0308]
<anon>:22     <(A,B) as Foo>::f2()
              ^~~~~~~~~~~~~~~~~~~~
<anon>:22:5: 22:25 help: see the detailed explanation for E0308
error: aborting due to 2 previous errors

Shouldn’t the compiler have figured out that <(A, B) as Foo>::T is the same type as A?


#2

I can’t comment on whether it should or not, but it doesn’t. You need to be both more and less specific about what type you expect:

trait Foo {
    type T;
    type U;
    fn f1(Self::T);
    fn f2() -> Self::U;
}
impl Foo for (i32,u32) {
    type T = i32;
    type U = u32;
    fn f1(x: i32) {}
    fn f2() -> u32 { 1 }
}
impl Foo for (i64,u64) {
    type T = i64;
    type U = u64;
    fn f1(x: i64) {}
    fn f2() -> u64 { 2 }
}
// Should work for (A,B) being either (i32,u32) or (i64,u64).
fn bar<X>(x: X::T) -> X::U where X: Foo {
    X::f1(x);
    X::f2()
}
fn main() {
    let x = bar::<(_, u32)>(1_i32);
    let y = bar::<(_, u64)>(2_i64);
}

#3

Yeah, that works, but kinda spoils the fun for callers of bar, as they in turn will need to specify its type parameter(s) explicitly. I’d like to encapsulate the specialization shenanigans from consuming code (save for where (A,B): Foo constraint, which is probably unavoidable), such that one could write:

fn baz<C,D>(x:C) -> D where (C,D): Foo {
    bar(x) as D
}

#4

Well yes, but it doesn’t work. The compiler (at the moment) can’t tell that A is the same as (A, B)::T, because that would require it to exhaustively check every possible implementation of Foo for two-element tuples and verify that it’s actually true, which it might not be.

So you have to specify that you mean (A, B)::T, but now you have the opposite problem: given X::T and X::U, it has to work out what X is from the trait implementation. In theory, it could use the information provided by the where clause to constrain the search, but it doesn’t, so it’s not relevant to the question “how do I do this?”. In general, there’s a theoretically infinite list of types (including ones that haven’t been defined yet) that could satisfy those requirements.

I had a similar problem with conv, which I solved by redesigning the interface to not have the problem in the first place. I suspect this is just something Rust simply can’t model right now.


#5

It’s actually possible to specify that T=A and U=B in the where clause:

fn bar<A,B>(x: A) -> B where (A,B): Foo<T=A, U=B> {
    <(A,B) as Foo>::f1(x);
    <(A,B) as Foo>::f2()
}

There is otherwise nothing that hits to them being the same, so this is about as fun as it gets.


#6

Ahh, cool, that’s what I was looking for!