Traits were introduced earlier in terms of their role as "mixins". Let's expand on the usefulness of traits by showing how base traits may be used as the foundation for defining more flexible variant types than are possible using enum.
Although enum is sufficient for simple variant types, we sometimes need more complex variant types that share fields in common or support same-named methods which are implemented differently for each variant. We accomplish this by defining each variant types as extending (inheriting from) a common base trait. For example:
// Common base trait for variant event types // '@samesize' pads all variants to be the same size trait @samesize Event: enum u8 // explicit tag field to discriminate variants time Time // All events are time-stamped // Mouse event type, inheriting from Event struct MouseEvent extends Event: button u8 x i32 y i32 // Keystroke event type struct KeyEvent extends Event: key KeySym // Quit event type struct QuitEvent extends Event:
This example defines three variant event types (for mouse-button clicks, keyboard changes, and quit requests), all built on a common Event base trait. These event types share some fields in common, such as time's timestamp info. Each event variant may also have its own specific fields. All event values take up the same amount of memory.
A trait that is used as a base trait is in fact a mixin, as if defined as such always at the start of some struct. However, being a base trait brings more to the table than just being a mixin.
Let's focus first on the definition of the trait type.
Without @samesize every variant built from a trait may have a different size. @samesize changes this to ensure all variants take up exactly the same amount of space in memory. (This is the same guarantee enum makes for its variants.) Since MouseEvent is the largest variant type in the above example, Event's use of @samesize ensures the size of KeyEvent and QuitEvent are padded to be as large. To successfully calculate this, a trait specifying @samesize requires that all variant types be defined in the same module as the trait.
If all variants have the same size, one may work with trait-typed values either as values (Event) or as references (&Event). When variants have different sizes, a trait type may only be used as a reference. Being able to work with a trait type as a value offers some performance and other benefits:
- One variant type's value may be replaced (in place) with a value of a different variant type.
- The pool region may be used to more quickly allocate space for any new value, regardless of the concrete variant type.
- Access to field information is faster for values than when using references.
When trait does not specify @samesize, two key benefits accrue:
- Variant types may be defined in different modules than the trait type.
- Memory waste may be less than with fixed-size variant types, as each variant type only takes as much space as it requires. This may matter when variant types vary greatly in size and are used in great numbers.
The Event trait above uses enum u8 to explicitly specify that all its variant types will carry a tag field at this position and size. (This is the same tag field that enum implicitly defines for all its variants.)
The tag field's only use is to make pattern matching possible on a variant type value (Event) or basic reference (&Event), as the tag's integer value uniquely discriminates which variant type a value currently holds. So, if no such pattern matching is expected (e.g., because virtual references will be used instead), there is no need to define a tag field in the trait.
The primary benefit of having a tag field, is that tag-based pattern matching will likely perform better than virtual reference dispatch. The presence of a tag field also means one can use a basic reference to the trait type (&Event), rather than having to use a more expensive virtual reference (&<Event).
The downsides of the tag field is the extra memory space it requires and the restriction that all variant types must be defined in the same module as the trait they inherit from. This requirement ensures the compiler can easily calculate exactly how many variants there are and assign each one a different unique integer number.
Although it occupies space like other fields, it has no name because its contents are not accessible nor changeable in the same way as other fields. The tag field's content is implicitly and correctly initialized by the variant type's constructor.
Because a base trait is a mixin, its fields are inherited by all its variants, accessible just like any other field in the variant. Because a tagged trait's common fields are located in the same place across all variants (at the beginning), pattern matching is never required to access them.
As with all mixins, base traits may define methods. These methods may have implementations, but they are not required to. A trait's method without an implementation must be implemented by all variant types. A trait's method with an implementation is used as the default implementation for a variant type, if that variant type does not override it with its own implementation.
Variant Types from base traits
Now let's examine the definition and use of concrete variant types which inherit from a base trait type. These are defined using struct, thereby specific values of these variant types can be instantiated.
Inheriting from a Base Trait
A variant struct type explicitly inherits from a base trait by specifying it after the struct name and a colon. A trait's fields are folded into the struct at the start, as if individually specified there.
There are restrictions to base trait inheritance:
- Only the base trait may specify a tag field (and only one of them).
- Only the lowest base trait may be @samesize.
- The name of every field must be unique, whether inherited or part of the variant type.
Fields and Methods
Each variant type may define its own unique fields. Similarly, it may define its own methods. Every trait method without an implementation must be implemented by the variant type. Any implemented trait method may be overridden by the variant type, otherwise the default trait's method applies.
Variant Type Values
Since a variant type is a struct, one works with it as with most any struct. A new value is created using a struct constructor specifying both the common fields and the variant type's unique fields (any tag is automatically filled in with the correct integer):
// Notice that all constructors must provide a timestamp for the common time field mut event = MouseEvent[time: now, button: 0, x: 140, y: 170] event = KeyEvent[now, 'a'] event = QuitEvent[now]
If the variant types are based on a trait specifying @samesize, the type of its values could be the name of the trait (e.g., Event). Common fields may be accessed in the usual way. However, unique fields and methods are only accessible after pattern matching.
match event: imm mbutton MouseButton: mbutton.x imm key Keyboard: i32[key.keysym] is Quit: 0
If the variant type is based on trait, its type is the name of the variant type (e.g., QuitEvent). Common and unique fields and methods are accessed in the usual way.
Upcasting to a Trait Reference
A basic reference to a base trait may be safely coerced from a reference to any variant type's value.
imm refevent &Event = &event
(Note: On the next page, we also show that a trait-based virtual reference may be created.)
A trait-based basic reference only has access to the trait's common fields and any trait-based implemented methods never overwritten by any variant type.
imm when = refevent.time
Note that a struct may be upcast by value or simple reference to its base trait, and not to any other traits it may mix in. However, we will later see that a struct may be upcast to any of its mixin traits using a virtual reference.
Use pattern-matching to downcast an upcast value or reference to its original, specialized variant type.
if imm butevent &MouseButton = refevent: // We can safely use butevent here as a reference to a event known to be a MouseButton event