The Problem
If we have two functions with the same concrete signature:
struct Struct0;
fn make_struct_0() -> Struct0 {
Struct0
}
fn make_struct_0_too() -> Struct0 {
println!("Making Struct0.");
Struct0
}
Then, it is perfectly fine to conditionally assign them to a variable:
let my_fn/* : fn() -> Struct0 */ = match predicate {
true => make_struct_0,
false => make_struct_0_too,
};
However, if the functions return impl T
, we are screwed:
trait Trait {}
struct Struct1;
impl Trait for Struct0 {}
impl Trait for Struct1 {}
// `Trait` can instead be Iterator, Future, whatever. You get the idea.
fn make_trait_0() -> impl Trait {
Struct0
}
fn make_trait_1() -> impl Trait {
Struct1
}
let my_fn = match predicate {
true => make_trait_0,
false => make_trait_1,
};
// Compiler:
error[E0308]: `match` arms have incompatible types
41 | let my_fn = match predicate {
| _________________-
42 | | true => make_trait_0,
| | ------------ this is found to be of type `fn() -> impl Trait {make_trait_0}`
43 | | false => make_trait_1,
| | ^^^^^^^^^^^^ expected `make_trait_0::{opaque#0}`, found `make_trait_1::{opaque#0}`
44 | | };
| |_____- `match` arms have incompatible types
|
= note: expected fn item `fn() -> impl Trait {make_trait_0}`
found fn item `fn() -> impl Trait {make_trait_1}`
This does make sense. If my_fn
were to be created, calling it could result in either make_trait_0
or make_trait_1
being called, returning different types.
Current Workaround
To deal with this, to my knowledge, we need to heap allocate and dyn
the return values of these functions in some ways, for example:
// Double `Box`. The type is mandatory because the compiler cannot infer it.
let my_fn: Box<dyn Fn() -> Box<dyn Trait>> = match predicate {
true => Box::new(|| Box::new(make_trait_0())),
false => Box::new(|| Box::new(make_trait_1())),
};
// Single `Box` by introducing two functions.
fn make_dyn_trait_0() -> Box<dyn Trait> {
Box::new(make_trait_0())
}
fn make_dyn_trait_1() -> Box<dyn Trait> {
Box::new(make_trait_1())
}
let my_fn/* : fn() -> Box<dyn Trait> */ = match predicate {
true => make_dyn_trait_0,
false => make_dyn_trait_1,
};
Translating this to async
stuffs, we have lots of boxing. Currently, if I am not mistaken, most async runtimes box all their tasks.
The Idea
So, it occurred to me that the compiler must know the concrete return type of the function although it says impl Trait
:
fn make_trait_0() -> impl Trait /* Compiler: This is `Struct0` */ { /* … */ }
We just cannot refer to them because we don't have concrete names for them.
So, what if we can concretize the impl
return type of functions, say, by using a macro return_type_of!
?
// Concretize return values and put them into enums to avoid boxing.
enum ConcreteTrait {
Fn0(return_type_of!(make_trait_0)),
Fn1(return_type_of!(make_trait_1)),
}
fn make_enum_trait_0() -> ConcreteTrait {
ConcreteTrait::Fn0(make_trait_0())
}
fn make_enum_trait_1() -> ConcreteTrait {
ConcreteTrait::Fn1(make_trait_1())
}
let my_fn/* : fn() -> ConcreteTrait */ = match predicate {
true => make_enum_trait_0,
false => make_enum_trait_1,
};
Then, we can call my_fn
, and then pattern match on the return value to call methods on them directly, without any boxing.
Obviously, return_type_of!
would need internal support from the compiler to work.
Is this a solved problem? Is there any interest in this?
Playground for the code above.
After Notes
After I wrote up the above, Discourse recommended me Idea: syntax for function item types, which leads to Named existentials and impl Trait variable declarations #2071 and Tracking issue for RFC 1861: Extern types #43467 , but they seem to be different things then what I discuss here.