Cone's static types bring important benefits: optimal performance and compiler-assisted code quality and safety. However, static types often also constrain values and functions to overly specialized roles. When a program needs to repeat certain logic patterns over and over across many diverse types, it can become tedious and error-prone to handcraft many variations of the same code as needed to support every permissable combination of types.
To promote productivity and keep source code compact, Cone offers three distinct reuse mechanisms which allow creation of generalized, polymorphic logic that applies well across a useful range of concrete types:
- Static Subtyping
- Sum (variant) types
Static subtyping reuse
Subtypes work like this: if we have a value v of type T and we know that type S is a subtype of T, then we can safely treat v as if it were a type S value, including passing v to a function expecting a type S value. This simple principle means we can write functions that are not only usable for the declared type, but can also be used by values of any type that can be subtyped to that declared type. The Cone compiler is aware of subtyping relationships and will enforce valid substitutions.
Cone's static permissions take advantage of subtyping, allowing a value granted a certain permission to be safely treated as if it had another permission. const is the popular default permission for a reference parameter, because it is a valid subtype for most static permissions. In a similar manner, borrowed references are the popular default allocator for reference parameters, because a reference can be safely borrowed from any kind of allocator. Due to this subtyping flexibility, Cone library functions and methods accept const, borrowed references as much as possible, to maximize their reuse and universal utility.
struct values also benefit from subtyping, as they may be subtyped to traits (nominal subtyping) or interfaces (structural subtyping). Since traits are used to build structs, the subtyping relationship is implicitly understood. Interfaces are useful when you want to apply a subtype to a struct that knows nothing about it. Cone ensures the struct implements all fields and methods defined by any interface it is applied to. Both traits and interfaces facilitate reuse, since declaring a function or method's parameters as references to traits or interfaces allows every qualifying struct to participate.
Even better, Cone supports a form of inheritance (another reuse tool), allowing structs or traits to be built from other traits. And since a struct can be built from multiple traits, a safe form of multiple inheritance is also possible.
Lastly, Cone supports method overloading, the idea that more than one method in a type can have the same name but a different type signature. This not only enriches type flexibility, it also provides a graceful way to handle subtyped values. Since methods are declared in order, the first successful match wins. So methods can be ordered to match first on a specific type (e.g., struct) before falling through to match on a more-generic (and therefore less specific) subtype.
The rules for static subtypes are strict enough that most types can't substitute for each other. But when they can, subtype reuse is preferable, as generated code size stays lean and the runtime performance impact is typically negligible.
Sum type reuse
Sum types are useful when values might be one of several, seemingly-unrelated types (but we won't know which until run-time). This approach requires definition of a named sum type that lists all the potential types. A value's type is declared to be this sum type. Whenever we want to access its contents, the code must first pattern match to determine which type it actually is. Pattern matching safely extracts a properly-specialized type value which can then be directly manipulated.
When types have no subtyping relationship, sum types offer an alternative path for polymorphic functions, at the cost of a sum type declaration, a small runtime cost to pattern match the type, and separate logic in each function for each variant type.
Intriguingly, the subtyping and sum types reuse strategies may be combined. Tagged structs allows structural cousins to collapse to a common collection of traits and interfaces. Pattern matching can be applied to these subtypes to reverse direction and restore access to the original struct.
Metaprogramming is useful when we have many types whose underlying logic is quite similar, where we don't want to bear a runtime cost for logic variations, and where we can't anticipate and enumerate all variant types ahead of time. Collection types are well served by this approach, as a single template can generate a distinct, optimized code base for each base type whose values we want to gather together in a collection.
Cone's metaprogramming facilities are both friendly and flexible. The friendliness results from the metaprogramming grammar looking nearly identical to Cone's programming grammar, improving code readability. The flexibility comes from four well-integrated facilities:
- Compile-time execution, enabling conditional logic and calculation of values at compile-time
- Reflection for accessing to compiler information (esp. types) to compile-time execution
- Macros that expand code every time they are used
- Templates that expand code for each unique combination of type parameters
Metaprogramming is essential for customizing or replicating logic at compile-time. The downsides of metaprogramming are: the increased complexity of writing generalizable code, slower compilation times, and generated-code bloat triggered by each variation.