Home > Net >  Why is my generic function not monomorphized for a tuple?
Why is my generic function not monomorphized for a tuple?

Time:12-10

I have a generic enum:

enum E<T> {
    NeverForTuple(T),
    TisTupleVariant1(T),
    TisTupleVariant2(T),
}

and I know from context that E<T> instances will always be TisTupleVariant1 or TisTupleVariant1, if T is a two-element tuple (T = (A, B)). I am using a tuple here to avoid making E generic over two types A and B.

Now I define two functions bar1 and bar2, where bar1 takes any E<T> instance as an argument, and bar2 only works on E<(A, B)> instances.

// bar1 takes any E<T> instance
fn bar1<T>(_: E<T>) {}

// bar2 only takes E instances over two-element tuples
fn bar2<A, B>(_: E<(A, B)>) {}

Finally, I have a third function foo that delegates to bar1 or bar2 depending on the variant:

fn foo<T>(e: E<T>) {
    match e {
        E::NeverForTuple(_) => bar1(e),
        _ => bar2(e),
    }
}

The compiler will complain:

error[E0308]: mismatched types
  --> <source>:24:19
   |
21 | fn foo<T>(e: E<T>){
   |        - this type parameter
...
24 |         _ => bar2(e),
   |                   ^ expected tuple, found type parameter `T`
   |
   = note: expected enum `E<(_, _)>`
              found enum `E<T>`

Here is the playground link.

  • Coming from C , I would have assumed that foo would get monomorphized for T = (A, B) just by existence of bar2 and the code would work. Why is this not true?
  • Is there a way to get my code to compile without the specialization or min_specialization features?

Finally, in order to avoid giving an xy-problem, here is a less contrived example of what I am trying to achieve. Uncommenting line 115 will give the same compiler error. I want to implement the fold pattern for generic expressions as an exercise and follow-up to Expression template implementation in Rust like in boost::yap.

CodePudding user response:

You write "I know from context that E<T> instances will always be TisTupleVariant1 or TisTupleVariant1". But that is a fact that you have not made explicit in your types, so the compiler will not use it to determine whether your program is valid. As far as your program's compile-time structure is concerned, the function foo might execute either of its match branches, and therefore both bar1 and bar2 must be callable for any value (concrete type) of the type variable T.

The fundamental difference between C templates with SFINAE and Rust is this:

  • In C , the compiler substitutes the given concrete types into a template and only then checks if the resulting code is valid.
  • In Rust, a generic item (function, struct, trait impl…) must be valid for all possible values of its generic parameters (as restricted by any declared bounds). If it is not, that is a compilation error for the generic item, not for its usage.

However, these are also two different problems. Even in C , you will get a compile-time error if you write code that has a type mismatch in a function call that you happen to know never runs.

CodePudding user response:

With the help of the SO answers and comments to this questions, I understood why my approach was wrong and now understand the differences of C templates with SFINAE and rust generics with nominal typing better. Thanks a lot for the clarification!

In the meantime, I came up with a hacky solution to my code problem. If there is any cleaner solution with stable rust, I am happy to hear about it!

My main point was that I wanted one foo function, that can handle both E<T> instances and E<(A,B)> instances, and delegates to the correct function bar for the first and bar2 for the latter case. I can find a solution if I relax the condition on E<T> instances and am fine with woking with E<(T,)> instances instead.

Let's start with the enum and the two barX functions.

enum E<T> {
    NeverForTuple(T),
    TisTupleVariant1(T),
    TisTupleVariant2(T)
}

fn bar1<T>(_: E<T>){
    println!("Hello from bar1.")
}

fn bar2<A,B>(_: E<(A,B)>){
    println!("Hello from bar2.")
}

Next I create a BarTrait and two implementations that do not conflict each other:

trait BarTrait {
    fn bar(self);
}

impl<T> BarTrait for E<(T,)> {
    fn bar(self) {
        bar1(self)
    }
}
impl<A,B> BarTrait for E<(A,B)> {
    fn bar(self) {
        bar2(self)
    }
}

As a consequence, I will have to instantiate all my non-tuple instances of E as instances over a single-element tuple type. This is a bit ugly, but for my application I can hide this ugliness behind the API and the user never needs to know about it. I suppose once something like the specialization feature lands this can be done more elegantly.

Finally I can change the foo function to accept anything that implements BarTrait

fn foo<B>(b: B)
where 
    B: BarTrait
{
    b.bar()
}

And this now works (Note the ugly instantiations of the single type E instance):

fn main() 
{
    let x = E::<(u32,)>::NeverForTuple((42,));
    foo(x);

    let y = E::<(bool,&str)>::TisTupleVariant2((false, "string"));
    foo(y)
}
Hello from bar1.
Hello from bar2.

Here is the obligatory playground link.

  • Related