#enums #traits #performance #macro #optimization

macro enum_delegate

Easily replace dynamic dispatch with an enum, for speed and serialization

2 unstable releases

0.2.0 Nov 5, 2022
0.1.0 Nov 2, 2022

#1301 in Rust patterns

Download history 1569/week @ 2023-11-18 1104/week @ 2023-11-25 763/week @ 2023-12-02 1174/week @ 2023-12-09 810/week @ 2023-12-16 629/week @ 2023-12-23 844/week @ 2023-12-30 1426/week @ 2024-01-06 1368/week @ 2024-01-13 974/week @ 2024-01-20 889/week @ 2024-01-27 811/week @ 2024-02-03 872/week @ 2024-02-10 718/week @ 2024-02-17 828/week @ 2024-02-24 1036/week @ 2024-03-02

3,605 downloads per month
Used in 14 crates (5 directly)

MIT/Apache

55KB
1K SLoC

Easily replace dynamic dispatch with an enum, for speed and serialization.

What it Does

In Rust, you can use something like Box<dyn YourTrait> to hold any possible implementation of your trait. But this is a bit slow, and doesn't work well with serialization. The solution is to put all the implementations you need in an enum. This library can automatically implement the trait for the enum, as well as a bunch of useful conversions. Then, you can use your enum instead of using dynamic dispatch.

tldr: like the amazing enum_dispatch, but more amazing.

Walk-through

Add enum_delegate to each crate you plan to use it in:

enum_delegate = "0.1"

Annotate your trait with #[enum_delegate::register]:

#[enum_delegate::register]
trait SayHello {
    fn say_hello(&self, name: &str) -> String;
}

Create an enum with variants that all look like VariantName(VariantType). Each variant field must implement your trait. Now, annotate the enum with #[enum_delegate::implement(trait name)].

struct Arthur;
impl SayHello for Arthur {...}

struct Pablo;
impl SayHello for Pablo {...}

#[enum_delegate::implement(SayHello)]
enum People {
    Arthur(Arthur),
    Pablo(Pablo),
}

Your trait will be implemented for the enum, and conversions such as From<Arthur> and TryInto<Arthur> will also be generated. You can now use it similarly to a Box<dyn SayHello>, but it will be faster!

You can see this in action in the e1_simple.rs example.

3rd-party Traits

What if the trait comes from a 3rd-party crate, and you can't add enum_delegate::register to it? That's ok! Just pass the trait definition as the 2nd argument to enum_delegate::implement:

#[enum_delegate::implement(SayHello,
    trait SayHello {
        fn say_hello(&self, name: &str) -> String;
    }
)]
enum People {
    Arthur(Arthur),
    Pablo(Pablo),
}

Like before, your trait will be implemented for the enum. Conversions such as From<Arthur> and TryInto<Arthur> will also be generated.

3rd-party Enums

What if the enum comes from a 3rd-party crate, and you can't add enum_delegate::implement to it? That's also fine! Use the enum_delegate::implement_for attribute, and pass the trait name & trait definition to it:

#[enum_delegate::implement_for(People,
    enum People {
        Arthur(Arthur),
        Pablo(Pablo),
    }
)]
trait SayHello {
    fn say_hello(&self, name: &str) -> String;
}

This will implement the trait for the specified enum.

Note: since the enum comes from another crate in this case, enum_delegate can't and won't implement the From and TryInto traits for it.

Associated Types

enum_delegate can also handle traits with associated types.

Same Type

By default, enum_delegate requires all the involved implementers to provide the same value for the same associated type. If you specify different types, it won't compile.

Check the e2_associated_type.rs example for how to use simple associated types.

Mixed Types

If you need to mix different types in the same enum, you need to annotate your enum with #[enum_delegate(unify = "enum_wrap")]. This will generate a new enum wrapping the possible types for the associated type. From and TryInto conversions will be automatically generated to facilitate conversion between the enum and the different wrapper types.

This "secretly" generated enum's name is not a stable part of the public API. Use YourEnum::YourAssociatedType to refer to this type, and From conversions to construct it.

Check the e3_mixed_associated_type.rs example to see mixed associated types.

Limitations

Some edge cases, such as generic traits or supertraits, are currently not supported.

Alternatives

  • enum_dispatch, which this crate was inspired from, solves the same problem. However, as discussed below, enum_delegate has a few extra features.
  • Dynamic dispatch: instead of wrapping your types in an enum, you can use e.g. a Box<dyn YourTrait>. However, this will be slower (see benchmarks) and cannot be serialized by default. typetag can help with serialization though.
  • enum_derive: derive a method to return a borrowed pointer to the inner value, cast to a trait object, using enum_derive::EnumInnerAsTrait. This is slower though, and I am not aware of any advantages of doing this over enum_delegate.
  • Manually delegating the trait and other enum tricks: tedious, but might offer more flexibility in rare cases
Comparison with enum_dispatch

🟡 Performance: the same. This is expected, since they generate very similar code. (See benchmarks in the repo.)

✅ Works across crates. Due to technical limitations of how enum_dispatch is implemented, it can only be used if both the trait and enum are in the same crate. enum_delegate, however, allows you to put them in separate crates. (See cross_crate_example in the repo.)

✅ Better errors. Again due to technical limitations, in some cases enum_dispatch will quietly fail. With enum_delegate, your code will either succeed, or fail to compile. Admittedly, some of the error messages are not perfect, but at least you'll know something's up. (See tests_error in the repo.)

✅ Associated types. enum_delegate has some support for associated types, but enum_dispatch doesn't. (See examples in the repo.)

Dependencies

~1.5MB
~39K SLoC