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 forT
=(A, B)
just by existence ofbar2
and the code would work. Why is this not true? - Is there a way to get my code to compile without the
specialization
ormin_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.