Trait or Enum for sum types?

#1

Coming from Scala world, I am used to use “sealed class” a lot. I think that’s the feature I miss the most in Rust.
I am wondering whether someone has a good alternative.

Basically both traits and enum have drawbacks for my usual use case:
Traits:

  • No exhaustive pattern matching
  • All the complications of traits in rust… (unsized, trait object safety limitations, etc…)

Enums:

  • Syntax is absurd if you just want to wrap a type (enum MyEnum{ MyType1(MyType1), MyType2(MyType2)}})
  • Each variant isn’t a type so you cant implement a trait for just a variant.
  • Implementing a trait for the enum (like to_string) has to done using a match block while I would rather implementing it for each variant separately.
  • You lose the nice monomorphization that can do with Traits

I am aware of some of the thing that make it easier (or will…):

  • sum_type crate. Far from solving all my problems and I really dont like macros that wraps a lot of code, it affects readability, tools behaviour, etc…
  • Enum variant types RFCs. Doesn’t solve all my issue and add explicitely a restriction Variant types may not have inherent impls, or implemented traits

Is there someone else feeling the pain? Anyone having an alternative pattern?

3 Likes

#2

Hmm, I don’t see a clear way out of this, but perhaps the following may be of use:
First, define a trait with a function do_something:

trait Implementors {
    fn do_something(&self);
}

Then, define a generic function like so:

pub fn execute_a_something<T: Implementors>(obj: &T){
    obj.do_something();
}

This essentially does the same as a match, but it’s possibilities aren’t defined in a match block, instead they are defined as "whatever implements Implementors", and therefore the function in Implementors becomes your “match” case.
Note: untested, as this was written on the mobile page, and I can’t be bothered to deal with the playground on mobile

0 Likes

#3

There is a Pre-RFC for bringing sealed traits to Rust, https://internals.rust-lang.org/t/pre-rfc-sealed-traits/3108.

4 Likes

#4

What are you trying to do? What’s your use case?

1 Like

#5

Quoted from EnumX test case, hope this helps:

            fn foo<'a>( data: &'a [u32], f: bool ) -> Sum2!( impl Iterator<Item=u32> + 'a ) {
                if f {
                    Enum2::_0( data.iter().map( |x| 2 * x ))
                } else {
                    Enum2::_1( data.iter().map( |x| x + 2 ))
                }
            }

            let data = [ 1, 2, 3 ];

            let mut iter = foo( &data[..], true );
            assert_eq!( deref2!( iter,size_hint() ), (3,Some(3)) );
            assert_eq!( deref_mut2!( iter,next() ), Some(2) );
            assert_eq!( deref_mut2!( iter,next() ), Some(4) );
            assert_eq!( deref_mut2!( iter,next() ), Some(6) );
            assert_eq!( deref_mut2!( iter,next() ), None    );

            let mut iter = foo( &data[..], false );
            assert_eq!( deref2!( iter,size_hint() ), (3,Some(3)) );
            assert_eq!( deref_mut2!( iter,next() ), Some(3) );
            assert_eq!( deref_mut2!( iter,next() ), Some(4) );
            assert_eq!( deref_mut2!( iter,next() ), Some(5) );
            assert_eq!( deref_mut2!( iter,next() ), None    );
1 Like

#6

I don’t have a single use case, I feel like that’s something I need very often.
Today for example I have started a project to collect metrics from different system.
This is meant to grow, have many different system and many different kind of events.

So I want something like :

trait Event<T> where T: serde::Serialize {
  fn get_event_type_id(&self) -> String,
  fn get_content(&self) -> T;
}

trait EventCollector<E, T> where E:Event<T>, T: serde::Serialize {
  fn collect_events(&self) -> Iter<E>
}

Obviously this won’t compile, because all the traits I am returning. Those traits are not even object safe. Serde objects themselves are not object safe, you have to use erased-serde which fills like a big hack and would confuse even more my colleagues…

So 3 choices I can think of:

  • Make my trait object safe, box everything. Besides all the obvious complications, there’s some negative effect on the rest of my code because in some scenarios I want to retrieve one specific events and the boxing is unnecessary. Designing for both scenarios complicates my code by a lot.
  • Have an enum containing all my events and enum of enums to be able to scale with many events; Define get_event_type_id at the enum level (which in my case is awkward because the type_id is deduce from the content); having this weird enum, that I was referring to
enum JenkinsEvents{
     StartBuildEvent(StartBuildEvent),
     SuccesBuildEvent(SuccesBuildEvent),
     ....
}
  • Redesign how collecting events works to use a visitor-like pattern. Won’t go in details about that, Rust is not the problem here, it just that it make the code unnecessarily tightly coupled and makes it much less nicer for the consumer of the API.

If I had sealed trait or something similar almost all my problems are gone. I can return a sized type (because we know all the variants) which implements a trait. The trait is implemented per Event type (no awkward pattern matching like with enum).

What I want is a sum type (sized, like enum) that can implement a trait by forcing all its member to implement the trait.
Cherry on top, similar to traits, you can start do monomorphization and have zero cost abstraction. All the benefits of both traits and enums in one…

2 Likes

#7

My problem just got solved by the crate:
https://docs.rs/enum_dispatch/0.1.0/enum_dispatch/

The developer of this crate emphasize the performance reason but I think it’s so much more than that.

0 Likes