#sealed #enums #variant #proxy #dynamic-dispatch

macro proxy-enum

Emulate dynamic dispatch and sealed classes using a proxy enum, which defers all method calls to its variants

4 releases (2 breaking)

0.3.1 Jan 7, 2021
0.3.0 Nov 24, 2020
0.2.0 Apr 30, 2020
0.1.2 Apr 28, 2020
0.1.1 Apr 28, 2020

#8 in #sealed

39 downloads per month

MIT license

18KB
239 lines

proxy-enum

Crate API

Emulate dynamic dispatch and "sealed classes" using a proxy enum, which defers all method calls to its variants.

Introduction

In rust, dynamic dispatch is done using trait objects (dyn Trait). They enable us to have runtime polymorphism, a way of expressing that a type implements a certain trait while ignoring its concrete implementation.

let animal: &dyn Animal = random_animal();
animal.feed(); // may print "mew", "growl" or "squeak"

Trait objects come with a downside though: getting a concrete implementation back from a trait object (downcasting) is painfull. (see std::any::Any)

If you know there are only a finite number of implentations to work with, an enum might be better at expressing such a relationship:

enum Animal {
    Cat(Cat),
    Lion(Lion),
    Mouse(Mouse)
}

match random_animal() {
    Animal::Cat(cat) => cat.feed(),
    Animal::Lion(lion) => lion.feed(),
    Animal::Mouse(mouse) => mouse.feed()
}

Some languages have special support for such types, like Kotlin with so called "sealed classes".

Rust, however, does not.

proxy-enum simplifies working with such types using procedural macros.

Usage

#[proxy_enum::proxy(Animal)]
mod proxy {
    enum Animal {
        Cat(Cat),
        Lion(Lion),
        Mouse(Mouse)
    }

    impl Animal {
        #[implement]
        fn feed(&self) {}
    }
}

This will expand to:

mod proxy {
    enum Animal {
        Cat(Cat),
        Lion(Lion),
        Mouse(Mouse)
    }

    impl Animal {
        fn feed(&self) {
            match self {
                Animal::Cat(cat) => cat.feed(),
                Animal::Lion(lion) => lion.feed(),
                Animal::Mouse(mouse) => mouse.feed()
            }
        }
    }  

    impl From<Cat> for Animal { /* ... */ }
    impl From<Lion> for Animal { /* ... */ }
    impl From<Mouse> for Animal { /* ... */ }
}

This, however, will only compile if Cat, Lion and Mouse all have a method called feed. Since rust has traits to express common functionality, trait implentations can be generated too:

#[proxy_enum::proxy(Animal)]
mod proxy {
    enum Animal {
        Cat(Cat),
        Lion(Lion),
        Mouse(Mouse)
    }

    trait Eat {
        fn feed(&self);
    }

    #[implement]
    impl Eat for Animal {}
}

Since the macro has to know which methods the trait contains, it has to be defined within the module. However, implementations for external traits can be generated too:

#[proxy_enum::proxy(Animal)]
mod proxy {
    enum Animal {
        Cat(Cat),
        Lion(Lion),
        Mouse(Mouse)
    }

    #[external(std::string::ToString)]
    trait ToString {
        fn to_string(&self) -> String;
    }

    #[implement]
    impl std::string::ToString for Animal {}
}

Dependencies

~1.5MB
~37K SLoC