Programmers need to work with data whose type is decided at run-time: optional values, success/failure results, heterogeneous collections, and more. Cone's variant types, an enriched fusion of traditional sum types and object-oriented class techniques, are easy to define, safe to use, and remarkably versatile.
And yet, programmers are also short on time. They want to write minimal, straightforward logic that works gracefully across diverse data types and runtime environments. Cone offers a wide range of mechanisms to facilitate polymorphic reuse, including implicit coercions, generics, virtual references, and metaprogramming.
Traits are a cornerstone ingredient for both variant types and polymorphism. A trait defines a subset of fields and methods held in common across multiple variant types. Traits facilitate multiple roles; they are used to compose new variant types, establish the interface for virtual references, and constrain the use of generics.
The following sections elaborate on these variant type and reuse mechanisms.
Multiple variant types may be defined that inherit from the same base trait. These variant types share the base trait's fields and method signatures in common. Each variant then adds its own unique fields and method implementations to the mix.
Depending on how the base trait is defined, its variant types can behave like sum types or like object instances. For example, the base trait may require all variant types to be padded to be the same size, allowing variants to be used interchangeably as values or by reference. Similarly, the base trait may define a tag field that can be used by pattern matching to discriminate which variant is in play at runtime.
Variant type values are easily and implicit upcast by reference (or value) to its generic base trait form, where it can be handled by polymorphic logic. Pattern matching may then be employed to reverse the process, enabling a trait-based value to be downcast back to its specialized variant type.
In addition to base traits, Cone supports other reuse techniques when defining new types:
- Mixing in (inheriting) fields and methods from multiple traits (only one may be the base trait).
- "Inheriting" selected fields and methods from selected field types used to compose the type. These inherited fields and methods generate auto-delegation forwarding logic when used.
- Method overloading, which permits the creation of multiple methods that have the same name but different method signatures. This extends the ability of differently-typed values to participate in a named method's intended functionality, by dispatching to the method implementation whose type signature best matches the types of the method call arguments.
A reference to some variant type value may be implicitly coerced into a virtual reference for any trait whose interface it complies with. It need not have explicitly inherited from the trait. It is only necessary that the variant type implement every field and method defined by the trait in an exact type-compatible way. For virtual references, structural subtyping is sufficient.
In its virtual reference form, any method or function can freely access any object field or method defined by the trait. This form of reuse is called subtype polymorphism, and is widely supported by object-oriented languages. Under the covers, it makes use of a language-generated vtable to virtually dispatch to methods and indirectly access field values.
Pattern matching may also be used on any virtual reference to re-specialize it back to a normal reference to its concrete variant type, thereby re-opening access to all its fields and methods.
Implicit Type Coercions
As already mentioned, polymorphic logic reuse is facilitated by the fact that the language knows when it is possible to safely and implicitly coerce a value of some specialized type into a more generalized, abstract supertype. This typically happens when passing a value of some specific type to a function or method that can work with values of many different types. This makes it possible for a single function to serve the needs of diversely-typed values.
This is not only valuable for coercing some concrete value type to a trait-based value, reference, or virtual reference. It also makes it easier to work with references which vary in the regions or permissions they support:
- Owning references coerce to borrowed references
- Nearly all reference permissions coerce to const
- Non-nullable references can coerce to nullable references
If separate logic were needed for every combination of permissions and regions, code could get quite verbose! Better yet, use of implicit coercions is "free": it neither bloats the size of generated code nor carries a runtime performance penalty.
Generics enable the creation of type or function logic that works across all types that conform to specified trait guard(s). This form of reuse is called parametric polymorphism.
Collection types are well served by this approach, as a single generic can apply the same algorithmic logic across all types the collection stores or indexes with. Option, Result, and many other general-purpose variant types also take advantage of the power of generics.
Generics accomplish reuse using a different technique than virtual references. They use compile-time monomorphization, stamping out multiple copies of the same logic, each customized for the types it has been applied to. As compared to virtual references, this approach increases code size but can improve performance, as generics don't bear the runtime overhead of accessing values indirectly by reference. They allow logic to operate directly on values.
Metaprogramming supports polymorphism by enabling program logic to be re-shaped while it is being compiled. In effect, metaprogramming layers a syntactically-similar, compile-time "scripting language" over the core language, which alters or elaborates on the program's code being compiled.
Metaprogramming enables several helpful capabilities:
- Conditional compilation, which customizes a program's logic for its target runtime environment.
- Macros for concise specification of repetitive program logic or data.
- Compile-time execution for pre-calculating complex data structures.