DEV Community

Cover image for My advocation for MultiType Return-Position `impl Trait` (MTRPIT) in Rust.
Calin Baenen
Calin Baenen

Posted on • Updated on

My advocation for MultiType Return-Position `impl Trait` (MTRPIT) in Rust.

Table of Contents


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 structs in a fn(...) -> Result<T, impl Error>. Or maybe you're someone advanced working with impl Iterators.
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();
}
Enter fullscreen mode Exit fullscreen mode

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) }
   |                 +++++++++      +          +++++++++      +
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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()
  }
}
Enter fullscreen mode Exit fullscreen mode

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 supertraits 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 return Self. (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();
}
Enter fullscreen mode Exit fullscreen mode

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 enums 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}")
} 
Enter fullscreen mode Exit fullscreen mode

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 enums.
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
}
Enter fullscreen mode Exit fullscreen mode

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)”


Thanks for reading!
Cheers!

Top comments (1)

Collapse
 
baenencalin profile image
Calin Baenen • Edited

Thanks kindly to "Madfrog" (@konnorandrews), who wrote an example of what the generated output code may look like in this Discord message:

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 {
  mod __impl_enum {
    use super::*;

    pub enum __return {
      _0(Wibble),
      _1(Wobble),
    }

    impl Test for __return {
      fn test(&self) {
        match self {
          Self::_0(value) => value.test(),
          Self::_1(value) => value.test(),
        }
      }
    }
  }

  if wibble {
    __impl_enum::__return::_0(Wibble)
  } else {
    __impl_enum::__return::_1(Wobble)
  }
}

fn main() {
    get_test_value(true).test();
    get_test_value(false).test();
}
Enter fullscreen mode Exit fullscreen mode

(From the Rustlang Discord guild.)