Table of Contents
- 1. Foreword
- 2. A Solution
- 3. Tradeoffs of
{impl enum T}
- 3.1. The Rules
- 3.2. Welcome
dyn*
- 3.2.1
{impl enum T}
vsdyn* T
- 3.2.1
- 4. Current Solutions
- 4.1.
Box<dyn T>
- 4.2. Using
enum
Variants as Types
- 4.1.
- 5. Alternatives
- 6. Origin
- 7. Further Reading
Foreword
[NOTE: impl enum T
will have {}
around it (in most cases) to show it's a placeholder type (like {integer}
).]
So, maybe you're a beginner in Rust that wants to return multiple different error struct
s in a fn(...) -> Result<T, impl Error>
. Or maybe you're someone advanced working with impl Iterator
s.
Whatever the case is, you've probably run into the following compiler error... E0308.
Consider this code I wrote on the Rust Playground:
trait Test {
fn test(&self) { println!("Default impl!"); }
}
struct Wibble;
impl Test for Wibble {
fn test(&self) { println!("Wibble!"); }
}
struct Wobble;
impl Test for Wobble {
fn test(&self) { println!("Wobble!"); }
}
fn get_test_value(wibble:bool) -> impl Test {
if wibble { Wibble } else { Wobble }
}
fn main() {
get_test_value(true).test();
}
This code produces the following error:
error[E0308]: `if` and `else` have incompatible types
--> src/main.rs:20:33
|
20 | if wibble { Wibble } else { Wobble }
| ------ ^^^^^^ expected struct `Wibble`, found struct `Wobble`
| |
| expected because of this
|
help: you could change the return type to be a boxed trait object
|
19 | fn get_test_value(wibble:bool) -> Box<dyn Test> {
| ~~~~~~~ +
help: if you change the return type to expect trait objects, box the returned expressions
|
20 | if wibble { Box::new(Wibble) } else { Box::new(Wobble) }
| +++++++++ + +++++++++ +
Whatfor does this error happen?
It happens because Rust understands returning impl Test
, but it believes that impl Test
can/should only refer to one type. This can be an issue because sometimes you'd like to return multiple types that implement Test
.
A Solution
So... What would a possible solution be? Other than using Box<dyn Test>
(which is not very beginner friendly in my opinion).
Well, that's where RFC #3367 comes in. It introduces the idea of static-dispatch in the form of -> impl Trait
or -> impl enum Trait
which would act as an alternative to using Box<dyn Trait>
.
So, what would this theoretical solution look like in action?
Here's an example of what my code from earlier would look like:
trait Test {
fn test(&self) { println!("Default impl!"); }
}
struct Wibble;
impl Test for Wibble {
fn test(&self) { println!("Wibble!"); }
}
struct Wobble;
impl Test for Wobble {
fn test(&self) { println!("Wobble!"); }
}
fn get_test_value(wibble:bool) -> impl enum Test {
if wibble { Wibble } else { Wobble }
}
fn main() {
get_test_value(true).test();
}
Notice the difference?
... Barely? Good. That's the point.
With this you'd be getting *basically* the same results as dyn Test
without the vtable shenanigans.
(Here is what the theoretically generated code would look like.)
How would this work?
When we tell the compiler we would want an impl enum T
, we would be telling it we want any type that implements the trait T
, so it knows it must be any of those types.
This would be able to work for the same reason @chayimfriedman2 suggested enum impl
in their comment on the RFC, because (it is like) the compiler is making an enum
for us.
How is this like the compiler making an example?
Well. Using my code as an example again, consider what the program may look like if we make in enum
for all types that implement Test
.
trait Test {
fn test(&self) { println!("Default impl!"); }
}
enum Wrapper {
Wibble(Wibble),
Wobble(Wobble)
}
struct Wibble;
impl Test for Wibble {
fn test(&self) { println!("Wibble!"); }
}
struct Wobble;
impl Test for Wobble {
fn test(&self) { println!("Wobble!"); }
}
fn get_wrapper_value(wibble:bool) -> Wrapper {
if wibble { Wrapper::Wibble(Wibble) } else { Wrapper::Wobble(Wobble) }
}
fn main() {
match get_wrapper_value(true) {
Wrapper::Wibble(w) => w.test(),
Wrapper::Wobble(w) => w.test()
}
}
The increase in code is negligible here, and we could even make it so Wrapper
implements Test
and delegates the job of .test(&self)
to whatever the given variant would do, but this isn't a good solution because it scales for every type that has implements Test
. Not to mention that you'd have to (manually) account for blanket implementations.
I think what this kind of static-dispatch brings to the table is invaluable, it's an efficient and typesafe shorthand that would come with virtually zero performance cost.
Tradeoffs of {impl enum T}
While having an {impl T}
or {impl enum T}
type would be nice, it comes with a few drawbacks.
The Rules
On the RFC, as @cad97 said in one of their comments, the rules for a Trait
being "enum-dispatch safe" would be very similar to those of object safety.
Everything listed below is a compilation of known "rules"/"laws" about how this type should/may work.
Musts
- To obey the rules, any super
trait
s must also obey these rules. -
T
must not be a DST. (I.e.T: Sized
.)
Cans
- For any
{impl enum T}
,T
can have associated types (with or without generics), associated constants, generics, and non-receiver methods. - Methods callable from
{impl enum T}
may have have type-parameters. -
T
may use&self
, and&mut self
as method receivers. - An
{impl enum T}
may be the type of a variable. (The only effective way to obtain a value is via a function that returns{impl enum T}
.)
Can Nots
- No method usable from
T
can returnSelf
. (However a value of{impl enum T}
can be captured.)
Revisions
If you have any ideas for changes or additions to these set of rules to better refine or extend them, let me know in the comments and further discussion can be done from there.
Welcome dyn*
What is dyn*
?
dyn*
(read as "dyn
-star") was a feature proposed by @nikomatsakis in their blogpost “dyn*: can we make dyn sized?”.
The proposal basically states that any type dyn* T
, where T
is a trait
, is a sized type that can refer to any value that implements that trait
so long as is pointer-sized or smaller. While this solution is can be what we're looking for, its size-limit can be very... well... limiting. A lot of struct
types will probably easily surpass that threshold (especially on 32-bit targets).
{impl enum T}
vs dyn* T
Ultimately, in my opinion, {impl enum T}
easily wins, however both concepts could be really helpful.
I'm not a programming(language) expert or know how things work behind the scenes, and quite honestly I could use the reading, but it seems like {impl enum T}
has some advantages over dyn* T
. The best thing I can think of being the size limitation.
Another reason I can't really get a good comparison of them is because in the article Matsaki only discusses use of dyn* T
as function parameter and not as a return type. (The implications of this type being used as a returntype aren't mentioned either.)
However, as so they can be compared, even if not by me, I will leave some resources under Further Reading so you can may judge for yourself.
Current Solutions
Here are some current solutions to get around this.
Box<dyn T>
The, in my opinion unfriendly, Box<T>
is the compiler's goto recommendation to solve the issue.
Let's see how the code would look then:
trait Test {
fn test(&self) { println!("Default impl!"); }
}
struct Wibble;
impl Test for Wibble {
fn test(&self) { println!("Wibble!"); }
}
struct Wobble;
impl Test for Wobble {
fn test(&self) { println!("Wobble!"); }
}
fn get_test_value(wibble:bool) -> Box<dyn Test> {
if wibble { Box::new(Wibble) } else { Box::new(Wobble) }
}
fn main() {
get_test_value(true).test();
}
Now it prints "Wibble!
" with no fuss.
This is dynamic dispatch.
Unfortunately, one drawback to Box<T>
is that beginners might not understand.
Sure, all you need to know is that it's "like a pointer", but there's some things about it (and its usage) that may be hard for someone new to grasp.
Using enum
Variants as Types
This case would look very similar to the one where we wrote Wrapper
, except Wibble
and Wobble
are no longer their own types.
They are reduced to unit variants in Wrapper
, and wrapper uses a match
to determine what case to use.
Sometimes this is okay, but its not always a valid solution.
Alternatives
This is where ideas for alternative implementations would go, but I don't have any nor could I find any other really good ideas. (I'll take suggestions in the comments.)
[There is technically one idea, as linked to in the next section; anonymous enum
s as shown in the pre-RFC “anonymous enums”. However it has a flaw I will point out in the next section.]
Origin
The prototype version of the idea started all the way back on Septemeber 23rd, 2014 with RFC #294, “Anonymous sum types”, by @glaebhoerl, which had a completely different syntax.
To me it looks unrecognizable. Alien. But the syntax reads like this:
let foo:(~str|int|int) = (!|!|666);
match foo {
(s|!|!) => println!("{s}"),
(!|n|!) => println!("{n}"),
(!|!|m) => println!("{m}")
}
It's not explained what the tilda means or what it does when it is or isn't present.
Eventually this at some point evolved into “pre-RFC: anonymous enums”, which introduces... anonymous enum
s.
This follows a more understandable syntax:
let v:Option<i32> = /* ... */
// Type of `result`: enum{i32, &'static str}.
let result = match v {
Some(v) => v+2,
None => "Error!"
}
match result {
#0(v) => println!("{}", v), // `v`: i32
#1(v) => println!("{}", v) // `v`: &'static str
}
I like this. This is certainly a step in the right direction compared to the last iteration; however, it has two flaws.
One, "What should the ordering of the variants be?".
Two, there is no shared common functionality between v
in #0
and v
in #1
. With {impl enum T}
you're promised shared functionality with very similar benefits to this iteration of the idea.
Eventually this impl Trait
syntax got people excited, especially with the idea of some kind of "anonymous enum
", and yielded us with RFC #2414, which is pretty similar to what our current #3367 ended up proposing.
Further Reading
The Rust Programming Language
“The Rust Programming Language”
dyn T
and dyn* T
“Using Trait Objects That Allow for Values of Different Types” of The Rust Programming Language.
“dyn*: can we make dyn sized?” by Niko Matsakis
“dyn* doesn't need to be special ” by Christopher Durham
impl enum T
RFC #3367, “RFC: Multi-Type Return Position Impl Trait (MTRPIT)”
Cheers!
Top comments (1)
Thanks kindly to "Madfrog" (@konnorandrews), who wrote an example of what the generated output code may look like in this Discord message:
(From the Rustlang Discord guild.)