Despite the significant performance and safety benefits of static typing, there are downsides. The intentional rigidity of static types can mean that general-purpose algorithms (e.g., sort or hashed collections) have to be replicated and customized by hand for each type the algorithms are applied to, adversely impacting programmer productivity and code size. Type inflexibility also makes it harder to support heterogeneous collections, where each value in the collection could have a different type.
To support greater code flexibility and reuse, while still enforcing type safety, Cone offers various techniques to improve type flexibility and help generalize algorithmic logic across diverse types and conditions.
Multiple methods in a type can be given the same name but different type signatures and implementations. When a method of a certain name is used, the most appropriate implementation is automatically picked and dispatched based on the types of the passed arguments and the get/set context.
Interfaces and Traits
When multiple types offer methods with the same names and signatures, an interface type may be defined declaring this common subset of methods. Any object whose type complies with the interface can be coerced to an interface type reference. By collapsing the logic for multiple types through common interface dispatch, less code is needed for each distinct type.
Traits work similarly to interfaces, but add support for code and property inheritance for types that explicitly implement them. A type can mix in methods and properties from multiple traits. This further improves code reuse over interfaces.
Fluid Permissions and Borrowed References
To improve safety, every reference must specify its permission type. Similarly, to support multiple memory management strategies, every reference also specifies its allocator type. If separate logic were needed for every combination of permission and allocator types, this could get messy.
Fortunately, static permissions allow a reference to be safely treated as if it had a lesser permission. For example, const is the popular default permission for a method's reference parameter, because it is a valid substitute for most static permissions (when we know a method or function won't change the value).
Similarly, any reference may be coerced to a borrowed reference, regardless of which allocator manages it. Designing logic around the preferred use of borrowed const references, wherever lifetime and mutability constraints allow, helps significantly reduce the complexity of type libraries and program functions.
Variant types are useful when a value might be one of several, seemingly-unrelated types (but we won't know which until run-time). Variant types are useful for optional values (and nullable references), returned result/error, and many other uses. The type flexibility of dynamic languages, including their support for heterogeneous collections, is based on the use of variant types. Downcasting an interface reference to its original tagged struct requires the use of variant types.
Information about the value's current type is carried as a part of the value. Pattern matching is used to safely extract a properly typed value for appropriate manipulation.
Similar to interfaces, variant types can help reduce redundant logic. Variant types are more flexible in some ways than interfaces, since the types contained within a variant type need not share common methods. However, variant types do bear some added memory and performance costs.
Generics enable the creation of type or function logic that works across a range of compliant types. A type's compliance is measured similarly to a type's conformance to an interface or trait, but is not restricted to just references. Collection types are well served by this approach, as a single generic can encapsulate the same algorithmic logic across all the types the collection stores or indexes with.
Depending on the nature of the generic's parametric types, the instantiation of a generic can vary. Sometimes, instantiation is a compile-time mechanism, using a macro-like mechanism to generate multiple versions for each type. Other times, the generic is transformed into a runtime mechanism similarly to interfaces, where a single piece of runtime logic can use runtime type information to generalize the logic successfully across values from many different types.
Metaprogramming involves the use of a scripting language "on top" of the programming language. This scripting language, whose syntax looks nearly identical to Cone, provides programmatic control over the program's compilation, offering such features as:
- 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
Metaprogramming is valuable for customizing, replicating or calculating logic or data at compile-time. The downsides of metaprogramming are: the increased complexity of writing generalizable code, slower compilation times, and generated-code size increases due to code expansion.