Quite often I find the need to return values of different types from a function, where all the possible return types implement some common traits. This comes up frequently with futures, where different code paths need to return a different type of future.
The current approach is to return Box<Trait>
or define an enum manually and write a forwarding implementation of the correct traits, both of which are suboptimal for different reasons.
My suggestion is to add an anonymous enum type (A | B)
which automatically implements every trait which is implemented by both A
and B
, but user code is not allowed to match manually on the enum.
// the enum keyword packs the value into an anonymous enum
let x : (&str | u64) = if cond { enum "Foo" } else { enum 5 };
println!("{}", x); // x implements Display, since both &str and u64 do
This fits really well with impl Trait
, even if they are orthogonal features.
In this example, the handle
functionâs real return type is
(future::Map<db::DBQuery, {closure}> | future::FutureResult<String, Error>)
fn handle(path: &str) -> impl Future<Item=String, Error=Error> {
match path {
// DB::query returns some future with type DBQuery
"/" => enum self.db.query().map(|data| data.to_string()),
_ => enum future::finished("404 Not found".to_owned()),
}
}
We can even go a step further, using an anonymous enum as the futureâs item type.
The real return type is now
( future::Map<(&'static str | DBData), {closure}> | future::FutureResult<(&'static str | DBData), Error>)
fn handle(path: &str) -> impl Future<Item=impl Display, Error=Error> {
match path {
// DB::query returns some future with type DBQuery
"/" => enum self.db.query().map(|data| enum data),
_ => enum future::finished(enum "404 Not found"),
}
}
Yay, no boilerplate and no allocations required !
Unfortunately, itâs not all as nice as it seems. I ran into issues trying to determine which traits can be automatically implemented, and how.
I initially thought object-safe traits would be fine. Just match on *self
and call the method on the underlying type.
However object-safe traits can still have where Self: Sized
non object-safe methods which cannot (systematically) be implemented. Since the enum would be Sized
, it needs to define these methods. Restricting automatically implemented traits to those which only contain âobject-safe methodsâ would exclude Future
and Iterator
, defeating the whole point.
For these two traits, all the where Self: Sized
methods also have a default implementation. Therefore instead of dispatching to the underlying types the default implementation can be used instead. That is how the impl <T: ?Sized + Future> Future for Box<T>
works for instance. However while it is fine with Future
and Iterator
I donât think prioritising the default implementation over the underlying typesâ implementations is desirable in all cases, so it shouldnât be used for automatic generation of the trait implementations.
There are also non object-safe but very useful traits which could be implemented, albeit maybe not automatically by the compiler. Clone
and Into
are some examples.
Overall I think
- Traits which only have âobject-safe methodsâ (this is a stronger requirement than object-safe trait) can be implemented automatically,
Deref
orDisplay
for example - Trait authors should have a way to provide an implementation manually (
Future
,Iterator
,Clone
,Into
) - Some traits simply cannot be implemented at all (eg static methods, methods which take two
Self
values)
Note that the manual implementation only needs to be done once per trait, as opposed to once per enum like it is with manually defined enums.
Regarding manual implementation, Iâve been thinking about something like that :
// Implements Clone for anonymouse enums of any arity
// where all underlying types are Clone
impl <T: Clone> Clone for (T|...) {
// self has type &(T|...)
// gets replaced by the real type of the enum during reification
fn clone(&self) {
// Ugly imaginary syntax ahead, please bikeshed
// v is the underlying value, X the underlying type
enum_match *self => ref v : X {
// v.clone() returns an X
// The enum keyword packs it into a anonymous enum
enum v.clone()
}
}
}
impl <T: Future> Future for (T|...) {
type Item = T::Item;
type Error = T::Error;
fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
enum_match *self => ref v : X {
v.poll()
}
}
/*
* Rely on default impl for all the other methods of the Future trait,
* rather than forward to the underlying value. If it wasn't for these
* methods, the compiler could have generated this impl block
*/
}
enum_match
can only be used in a impl Trait for (T|...)
Given anonymous union (A | B)
, this gets reified into the following
impl <A, B> Clone for (A | B)
where A: Clone, B: Clone
{
fn clone(&self) {
// enum_match gets expanded into a match with one an arm
// per underlying type
match *self {
(A | B)::A(ref v) => (A | B)::A(v.clone()),
(A | B)::B(ref v) => (A | B)::B(v.clone()),
}
}
impl <A, B> Future for (A | B)
where
A: Future,
// Associated types must match
B: Future<Item=A::Item, Error=B::Error>,
{
type Item = A::Item;
type Error = A::Error;
fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
match *self {
(A | B)::A(ref v) => v.poll(),
(A | B)::B(ref v) => v.poll(),
}
}
Note that this cannot be done in user code, since enums canât normally be pattern matched on.
Anyway, just some idea I had. I interested to see what people think