Hello Rustaceans,
I’m thrilled to announce nodyn
, a new crate that makes it easy to create wrapper enums for a fixed set of types with automatic From
, TryFrom
, and delegated methods or traits. The nodyn::nodyn!
macro is designed for type-safe, zero-cost polymorphism using enums without the boilerplate.
Features
- Delegates methods and traits to wrapped types.
- Generates enums with variants for specified types (paths, references, arrays, slices, tuples).
- Implements
From<T>
for each variant type and TryFrom<Enum> for T
for non-reference types.
- Provides introspection methods:
count()
, types()
, and type_name()
.
Example
nodyn::nodyn! {
#[derive(Debug)]
pub enum Article {
NewsArticle,
SocialPost,
}
impl Summary {
fn summarize(&self) -> String;
}
}
pub trait Summary {
fn summarize(&self) -> String;
}
pub struct NewsArticle { /* fields */ }
impl Summary for NewsArticle {
fn summarize(&self) -> String { "News summary".to_string() }
}
pub struct SocialPost { /* fields */ }
impl Summary for SocialPost {
fn summarize(&self) -> String { "Post summary".to_string() }
}
fn main() {
let article = Article::NewsArticle(NewsArticle {});
assert_eq!(article.summarize(), "News summary");
}
Links
I’d appreciate feedback, use cases, or suggestions for nodyn
.
2 Likes
As usual the question is: how does it compare with alternatives, for example enum_dispatch
?
2 Likes
It looks very handy, thanks for sharing @franklaranja! I yet have to test it.
EDIT: I've been wondering about one issue: you're relying on features to generate code (from, try_into, ...). But:
- Because of feature unification, a crate user might get more than desired. I haven't tested if that could lead to unused warnings, so perhaps it's much of a concern. See also the last point in light of this, though, as it may lead to hard-to-find incompatibilities if several dependencies use your crate.
- It seems that specifying explicitly what's to be generated is also desirable for the reader's comprehension, when someone tries to understand the code later.
- Finally, if the macro is used for multiple enums, fine-grained code generation might be desirable; e.g. if the user wants to create it manually in some of the enums but not all of them.
PS: I spotted a wrap
instead of nodyn
in the README, probably a remnant from an earlier version?
Yes, that does look odd, but procedural macros don't have any knowledge of the code outside the braces, so the code needs that signature as a template to generate the implementation. An alternative would be to hard-code the signatures of known standard traits, but it'd be tedious.
[quote="jumpnbrownweasel, post:4, topic:130672"]
- But when a trait is used, why do the trait method signatures need to be repeated in a
nodyn!
impl
block?[/quote]
The macro doesn't know about the trait, especially when it is out of scope. See enum_dispatch
for an alternative solution. There you put an attribute on the trait. This works only when the trait is in scope: the macro kniws the code of the trait. I don't like that to see which traits are delegatedfrom an enum you have to look at the traits. It also rules out any third party traits.
You are right that method delegation doesn't work for trait objects.
Thanks for your feedback!
It is possible to turn of the automatic method generation on a crate using one of cargo's feature options.
It might indeed be nice to give more control over which code is generated. I find it difficult to judge when macros are too magical, so I appreciate you're suggestion about readability.
Thanks for your feedback!
I'm not sure we understood each other.
Maybe I wasn't clear, but if you read the document I've linked, you'll see that, even if a direct user of your crate doesn't enable a feature, say 'from', one of the other dependencies might. If that happens, the feature from
will be enabled for that direct user's code, too (because Cargo compiles your crate with the set of all the features it has found in all the dependencies). If they implemented their own, custom version of From
for that type, it'll conflict with what is generated by the macro, and the code won't compile.
It's not as straightforward to put parameters on a function-like macro, unfortunately. It's more natural on an attribute macro or a derive macro, but those are not well-suited to your situation.
One way to do it is by faking derive macros attached to the type, and removing them in the generated code. That solves the unification problem above and shows explicitly to the reader what is generated. It also allows crate users to select what's generated on a case-by-case basis (e.g. if they want to generate From
for type A
but not for type B
because it has a custom implementation).
For example:
nodyn! {
#[derive(Debug, PartialEq; From, TryInto)]
// ^^^^^^^^^^^^^^^ parameters read & removed by nodyn
pub enum Value {
i32,
String,
f64,
}
}
Try to choose identifiers that aren't likely to collide with derive identifiers of other crates, maybe by adding a prefix (NodynFrom
), or maybe separate them with a semicolon as I did above.
Thanks for the clearification. I don't like to include them in the derive attribute. I could create a custom attribute on the enum or a custom command after the enum (e.g. impl FromInto/impl Introspection).
I suppose it's not a problem that's likely to happen soon, so you have some time to think about it. I just wanted to make sure you were aware of that. 