3

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.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
joergbrech
  • 2,056
  • 1
  • 5
  • 17
  • You've stumbled a decent rabbit hole here. First, Rust does not have SFINAE like C++ does, which is the thing you're assuming in this question. Second, the general solution to this problem is called GADTs, a feature that Haskell supports but Rust does not. Third, you may be able to fake it with the experimental feature [GATs](https://github.com/rust-lang/rust/issues/44265). If I come up with something more complete, I'll post it as an answer, but that's the basic gist of it. – Silvio Mayolo Dec 08 '21 at 17:36
  • 3
    You appear to be using the term "monomorphization" differently from the usual Rust meaning. To me, it only means "code specific to the concrete type is generated"; it doesn't have any reflection on what code is valid / well-formed. *I know from context* — that's a key point: **you** know, but the compiler does not. – Shepmaster Dec 08 '21 at 18:09
  • Sure, but why is the code not generated specific to `T=(A,B)`, where A and B are any concrete types? I don't see why the compiler needs to know about how I intend to use `E` at this point. I suppose, I still nees to wrap my head around how C++ templates are different from rust generics, like Silvio said, I was expecting domething like SFINAE. Still, is there a way to get my code to work? – joergbrech Dec 08 '21 at 19:36
  • I admit to being surprised that _you_ are surprised that the compiler prevents this ^_^. I think that SFINAE is definitely the core of the misunderstanding — Rust's generics and traits are not textual substitution. I *think* C++'s concepts are more relevant — [What are the similarities and differences between C++'s concepts and Rust's traits?](https://stackoverflow.com/q/56045846/155423) – Shepmaster Dec 08 '21 at 20:11
  • Thanks! The answer there is very well written, especially the part on nominal vs structural typing helped me understand things better. – joergbrech Dec 08 '21 at 21:13

2 Answers2

4

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.

Kevin Reid
  • 37,492
  • 13
  • 80
  • 108
  • Stated differently, there are no monomorphization-time errors in Rust, but in C++ there are. – Sven Marnach Dec 08 '21 at 20:19
  • @SvenMarnach I wouldn't say _no_, but we try **really** hard to prevent them. – Shepmaster Dec 08 '21 at 20:32
  • Thanks a lot for the clarification! The differences between C++ templates and Rust generics are slowly beginning to dawn on me. :) I am still wondering if there is some Rust magic to get the dispatch from `foo` to the `bar`s to work properly. I don't mind a different design, but I would like to keep one `foo` function and have `E` not generic over two types (I want to call `foo` from `bar` in recursion, see the second playground link). – joergbrech Dec 08 '21 at 21:12
  • @SvenMarnach there are still some monomorphization-time errors, although the only intentional ones I know of come from misusing compiler intrinsics (eg using `bitreverse` on a `bool`). – Aiden4 Dec 09 '21 at 03:08
0

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.

joergbrech
  • 2,056
  • 1
  • 5
  • 17
  • The use of the turbofish syntax is completely unnecessary here (You can use `E::NeverForTuple` instead of `E::<(u32,)>::NeverForTuple`) as rust has type inference for that. The reason why trait is required is because any generic function need to be valid for any concrete type that satisfy the constrains. By using a trait you limit the possibility of the input type so that only valid type can be passed into the function. – attempt0 Aug 03 '22 at 10:12