#[enumerate]Expand description
Generates enums holding the types a trait is sealed to and macros rules to work with these enums.
Reads its type list from the #[sealed(..)] attribute below it, so it must be written above;
attribute macros run top down, and #[sealed] consumes itself when it expands.
// on a concrete type `into_enum` needs the trait in scope; a generic
// `S: Shape` gets it from the supertrait bound
use closed_trait::Enumerable;
struct Square;
struct Circle;
#[enumerate]
#[sealed(Square, Circle)]
trait Shape {}
impl Shape for Square {}
impl Shape for Circle {}
let shape: AnyShape = Square.into_enum();
match shape {
AnyShape::Square(_) => {},
AnyShape::Circle(_) => {},
}§The three enums
All three are generated by default, each with one variant per entry named after the type’s last
path segment, and each taking the trait’s visibility. A type not in upper camel case therefore
gives a variant that is not either (i32 yields an i32 variant), so the enums carry
#[allow(non_camel_case_types)]: the name came from a type, not from a choice the caller made.
as Name is there for anyone who would rather write I32. Given the base Shape sealed
trait:
| enum | holds | reached from |
|---|---|---|
AnyShape | Square | into_enum, or From |
AnyShapeRef<'a> | &'a Square | as_enum_ref, From, or owned.as_ref() |
AnyShapeMut<'a> | &'a mut Square | as_enum_mut, From, or owned.as_mut() |
Each brings a supertrait with it: Enumerable<AnyShape> and the higher-ranked for<'a> EnumerableRef<'a, AnyShapeRef<'a>> and its Mut counterpart, which is what makes the enums
reachable from a generic S: Shape without naming them.
The borrowing pair is what into_enum cannot give you: taking self, it needs the value moved
in, so a &S has no route to the owned enum at all. They are also cheaper to pass, being a
pointer and a discriminant rather than as wide as the largest permitted type. The shared one
derives Clone and Copy.
Conversions between the three (as_ref and as_mut on the owned enum, and as_ref on the
unique one, which reborrows) come as inherent methods. no_bridge leaves them out.
§Options
Written bare, an option applies to all three enums. Written inside owned(..), ref(..) or
mut(..) it applies to that one, and a specific option beats a grouped one.
§name = ..
Names the enums, which are Any{Trait}, Any{Trait}Ref and Any{Trait}Mut by default.
Grouped, it is a base that each kind extends, so name = "Shapes" gives Shapes,
ShapesRef and ShapesMut. Specific, it is the name itself: ref(name = "ShapeView") gives
exactly ShapeView.
#[enumerate(name = "Shapes", ref(name = "ShapeView"))]
#[sealed(Square)]
trait Shape {}
impl Shape for Square {}
let mut owned: Shapes = Shapes::Square(Square);
let mutable: ShapesMut<'_> = owned.as_mut();
let reference: ShapeView<'_> = mutable.as_ref();§no_bridge
Prevents generating the conversion methods written on an enum. owned(no_bridge) drops
as_ref(&self) and as_mut(&mut self); mut(no_bridge) drops the reborrowing as_ref(&self);
a bare no_bridge drops all three. Nothing is written on the shared enum, so ref(no_bridge)
is refused rather than silently doing nothing.
§attrs = ".."
Attributes to put on a generated enum, verbatim: derives, #[non_exhaustive], #[repr(..)],
anything. It is the one option that must be specific. What is valid differs between
the three: the shared enum already derives Copy, and the unique one cannot derive Clone at
all, so one spelling spread across them would be a trap. A bare attrs is an error saying so.
#[enumerate(owned(attrs = "#[derive(Clone, Debug, PartialEq)] #[non_exhaustive]"))]
#[sealed(Square)]
trait Shape {}
Documentation is the exception: the enum’s docs are generated and identical for every sealed
trait, so a /// or #[doc = ".."] here is an error.
§crate = ".."
Where the generated code should look for Enumerable. Defaults to ::closed_trait, which is
right whenever closed-trait is a direct dependency under its own name.
It is wrong in two cases, and both fail with cannot find closed_trait in the crate root even
though nothing in the caller’s source names it:
- the dependency was renamed, as in
st = { package = "closed-trait" }; - the macros are reached through a re-export, so the caller does not depend
on
closed-traitat all.
Point it at whatever crate re-exports Enumerable:
#[enumerate(crate = "::closed_trait")]
#[sealed(Square)]
trait Shape {}
§match_any
Generates match_any_{trait}!, a macro that expands to a match over every variant. A match
on the enum already tells the variants apart; what this adds is one body that runs against the
concrete type, which a closure cannot express because Rust has no generic closures.
struct Square { side: i32 }
struct Circle { radius: i32 }
#[enumerate(match_any)]
#[sealed(Square, Circle)]
trait Shape { fn area(&self) -> i32; }
let shape = AnyShape::from(Square { side: 3 });
let area = match_any_shape!(shape, s => s.area());
assert_eq!(9, area);The value may be given by &, by &mut or by value; match ergonomics make
the binding follow it, so one macro covers all three. The binding has to be
named by the caller.
It is a match, not a closure, and the difference is the point:
fn first_greater(shapes: &[AnyShape], value: i32) -> Option<i32> {
for shape in shapes {
// `return` leaves `first_big`, which a method taking the body could
// never do
match_any_shape!(shape, s => if s.area() > value { return Some(s.area()) });
}
None
}
The body may also move anything it owns, since only one arm ever runs, and it can be async.
To handle some variants differently, match first and let the last arm fall through:
let cost = match &shape {
AnyShape::Square(_) => 0,
other => match_any_shape!(other, s => s.area()),
};Two things follow from it being a macro. The enum and the trait must be in scope where it is
called, because a macro_rules! body resolves paths at the call site. And the body is copied
into every arm.
That copying is worth being deliberate about. It costs compile time in proportion to the number of variants, since the body is type-checked once per arm, and one mistake in it is reported once per arm too. Nesting one of these inside another squares the count.
The option takes an optional name, so match_any("match_shape") generates match_shape!
instead. Whether the macro can leave the crate depends on the trait’s visibility, and so does
whether it can collide, see Visibility.
§Visibility
Everything generated takes the trait’s own visibility: the three enums, the conversions between them, and the names the match macros are reached through. There is no option to change it: the enums appear in the trait’s supertrait bounds, so anything narrower would put a private type in a public interface.
It decides one thing beyond reach, though. A macro_rules! cannot leave the crate that defines
it without #[macro_export], and that always plants it at the crate root. So a macro generated
for a pub trait goes there under a hidden name and is aliased beside the enum, while one for
any narrower trait simply stays where it was written:
| the trait is | the macro is | usable from another crate | can collide |
|---|---|---|---|
pub | at the crate root, aliased beside the enum | yes | yes |
| anything narrower | where it was written | no | no |
Colliding means two traits of the same name, in different modules, both asking for match_any:
their hidden root names would be the same, and one of them needs match_any("other_name").
§Generics
A generic trait’s enum declares the parameters its variants are generic over, with their bounds, not those of the trait: an enum may not declare a parameter no variant uses. Where every entry pins its arguments, none is left to declare and the enum is plain:
// every entry fixes its argument, so `AnyValue` is a plain enum
#[enumerate]
#[sealed(
i32: Value<i32>,
f64: Value<f64>
)]
trait Value<T> {}
impl Value<i32> for i32 {}
impl Value<f64> for f64 {}
let _: AnyValue = AnyValue::i32(6);§for<..> entries
Parameters declared in a for<..> binder are the entry’s own and never reach the enum. Each is
passed to the trait as an argument, and the parameter it lands on, carrying the bounds the
binder gave it, is what the enum declares:
struct Boxed<U>(U);
#[enumerate]
#[sealed(for<U> Boxed<U>: Store<U>)] // here `U` is used in place of `T` declared by Store
trait Store<T> {}
impl<V> Store<V> for Boxed<V> {}
let _: AnyStore<u8> = Boxed(1u8).into_enum();A name never passed that way lands on no parameter, so the variant has nothing to be generic
over and the entry is refused: under a trait declaring none at all, for<U> Boxed<U>: Shape
leaves U free. #[sealed] accepts it, but the enum cannot.
§Pinned entries and match_any
An entry pins its arguments when it names a concrete instantiation instead of the trait’s parameters. Such an entry becomes a variant like any other, and the enum does not record which instantiation that variant belongs to.
into_enum and From exist only at the instantiations the entry named, so nothing ever builds
a variant that does not belong. Pinning therefore works with enumerate.
struct Plain;
#[enumerate]
#[sealed(Plain: Store<i32>)]
pub trait Store<T> {}
impl Store<i32> for Plain {}
let _: AnyStore = Plain.into_enum();What such an entry rules out is match_any. Nothing stops the trait from being named at an
instantiation no permitted type implements, and that is precisely where a body may ask the macro
to expand. This is what it would become there, written out by hand:
#[enumerate]
#[sealed(i32: Value<i32>)]
trait Value<T> {}
impl Value<i32> for i32 {}
fn describe(value: impl Value<String>) {
// what `match_any_value!(value.into_enum(), v => takes(v))` becomes
match value.into_enum() {
// error: the trait bound `i32: Value<String>` is not satisfied
AnyValue::i32(v) => takes(v),
}
}
fn takes<V: Value<String>>(_: V) {}The enum is fine, and so is the signature: Value<String> is a legal bound, merely one that
nothing satisfies. Drop the match and it compiles on its own:
fn describe(_: impl Value<String>) {}What cannot hold is the match_any. AnyValue::i32 hands back an i32, which is a Value<i32>
and nothing else, so a body written against Value<String> cannot use it. Rather than generate
that and let it fail inside the caller’s code, #[enumerate] refuses it where the list is
written.
A permitted type that is not Sized cannot be held in a variant. That one is rustc’s to
report rather than this macro’s, since sizedness is not visible in the tokens.