Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using enums for dynamic polymorphism in Rust

Tags:

When one already knows all the finite number of types involved in some code which needs dynamic polymorphism, using enum can be better for performances compared to using Box since the latter uses dynamic memory allocation and you'll need to use trait objects which have virtual function call as well.

That said, compared to the equivalent code in C++ using std::variant and std::visit, looks like Rust in this scenario has more boilerplate coding involved, at least for me (I have not yet learned to use procedural macros). Making some example here: I have a bunch of struct types:

struct A {
    // ...
}

struct B {
    // ...
}

// ...

struct Z {
    // ...
}

They all implement the trait AlphabetLetter which is:

trait AlphabetLetter {
    fn some_function(&self);
}

Since the set of types involved is known and limited, I want to use enum:

enum Letter {
    AVariant(A),
    BVariant(B),
    // ...
    ZVariant(Z),
}

Already here we have the first boilerplate: I need to add a name for the enum value for every type variant involved. But the real issue is: enum Letter is itself an AlphabetLetter, it just represent the fact that we do not know at runtime which letter it is. So I started implementing the trait for it:

impl AlphabetLetter for Letter {
    fn some_function(&self) {
        match self {
            Letter::AVariant(letter) => letter.some_function();
            Letter::BVariant(letter) => letter.some_function();
            // ...
            Letter::ZVariant(letter) => letter.some_function();
        }
    }
}

And yes, this can become easily a lot of code, but I found no other way of doing it. In C++, thanks to generic lambdas, one can just std::visit a std::variant and it's a one liner. How can I do the same without manually writing all the pattern matching for every function in the traits X every variant in the enum?

like image 998
nyarlathotep108 Avatar asked Sep 11 '20 13:09

nyarlathotep108


2 Answers

You can use a macro by example (rather than a procedural macro) to avoid the boilerplate:

macro_rules! make_alphabet {
    ($($x:ident),*) => {
        enum Letter {
            $(
                $x($x),
            )*
        }

        impl AlphabetLetter for Letter {
            fn some_function(&self) {
                match self {
                    $(
                        Letter::$x(letter) => letter.some_function(),
                    )*
                }
            }
        }
    };
}

Then you call it to generate everything:

make_alphabet!(A, B, C, ..., Z);

And now you can visit it any time you have a letter: Letter:

letter.some_function();

For methods that do not need to operate on all the variants, you can have an impl outside.

like image 172
Acorn Avatar answered Sep 30 '22 20:09

Acorn


The polymorphic_enum macro generates an enum with the chosen name and variants, as well as another macro with a chosen name. This generated macro is specific to the generated enum since it repeats the same block of code (closure like) for all the variants (exactly what you did explicitly). It supposes that all the variants can be used in the exact same manner; hence the name polymorphic_enum.

You don't have to write a new macro for each enum you want to handle this way since the macro specific to each particular enum is generated. You don't even have to implement the trait on the enum (welcome back duck-typing ;^) but you can if you want to. You just have to declare your enum in an uncommon way...

The invocation of the code which is supposed to be polymorphic is similar to what you do when providing a generic lambda-closure to std::visit() on a single std::variant in C++ (no multiple dispatch here however).

trait AlphabetLetter {
    fn some_function(&self) -> String;
    fn something_else(
        &self,
        arg: usize,
    ) {
        println!("--> {}, arg={}", self.some_function(), arg);
    }
}

struct A {
    // ...
}

struct B {
    // ...
}

// ...

struct Z {
    // ...
}

impl AlphabetLetter for A {
    fn some_function(&self) -> String {
        format!("some function on A")
    }
}

impl AlphabetLetter for B {
    fn some_function(&self) -> String {
        format!("some function on B")
    }
}

// ...

impl AlphabetLetter for Z {
    fn some_function(&self) -> String {
        format!("some function on Z")
    }
}

macro_rules! polymorphic_enum {
    ($name:ident $macro:ident, $($variant:ident($type:path),)*) => {
        enum $name { $($variant($type)),* }
        macro_rules! $macro {
            ($on:expr, |$with:ident| $body:block) => {
                match $on {
                    $($name::$variant($with) => $body )*
                }
            }
        }
    }
}

polymorphic_enum! {
    Letter use_Letter,
    AVariant(A),
    BVariant(B),
    // ...
    ZVariant(Z),
}

fn main() {
    let letters = vec![
        Letter::AVariant(A {}),
        Letter::BVariant(B {}),
        // ...
        Letter::ZVariant(Z {}),
    ];
    for (i, l) in letters.iter().enumerate() {
        let msg = use_Letter!(l, |v| { v.some_function() });
        println!("msg={}", msg);
        use_Letter!(l, |v| {
            let msg = v.some_function();
            v.something_else((i + 1) * msg.len())
        });
    }
}
like image 21
prog-fh Avatar answered Sep 30 '22 19:09

prog-fh