11

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?

5
  • 1
    There is no polymorphism nor dynamic dispatch since types are statically known. Commented Sep 11, 2020 at 14:00
  • 2
    IIUC, C++ templates are untyped, making std::visit functionally similar to using Rust macros (note that C macros are another thing entirely). The price you pay for this convenience in C++ is that type errors are reported at use rather than declaration, and the errors for doing something incompatible inside the lambda can be... opaque. Commented Sep 11, 2020 at 14:06
  • 1
    (Also note that procedural macros and declarative macros are different kinds of macros; declarative macros are easier, and sufficient to make this kind of thing work without too much repetition.) Commented Sep 11, 2020 at 14:09
  • I think the best solution to this will be local trait objects once they become available. This will remove the need for dynamic memory allocation, and in this case I think the virtual function call is very similar to what your match statement does (and possibly faster). Commented Sep 11, 2020 at 15:49
  • C++ class templates are structurally typed and a significant amount of type checks on quantified types (that is, on the template themselves) are delayed after the template being instantiated (on template instances instead) by default. It is still possible to opt-in the earlier check by concept-based constraints. There is no similar feature in Rust and procedural macros can work it around as it works similarly enough to template instantiation as in an earlier separate phase before regular type checks, but there is no chance to opt-in type checking in this phase (on macro arguments) at all. Commented 8 hours ago

2 Answers 2

5

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.

Sign up to request clarification or add additional context in comments.

Comments

2

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())
        });
    }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.