Typing is strict: You cannot put a round peg in a square hole.
Since all values, expressions and variables have a type, the compiler can (and will) check whether the use of types is consistent. In most cases, an error will be reported wherever a program:
- Stores a value of one type into a variable or collection declared for a different type.
- Passing an argument of one type to a function that expects a value of another type in that position.
- Returns a value of one type when the function's signature declares a different type for the return value.
- Creates a reference (or pointer) to an object whose type does not match the reference's type declaration.
These constraints protect against the program manipulating a value in ways that make no sense or could be harmful, due to acting as if the value has a different meaning than it actually does.
That said, Cone can handle certain type mismatches safely by explicitly or implicitly coercing or converting a value from one type to another. Conversion and coercion improve flexibility and can make code easier to write and more concise.
Type conversion happens whenever a value of one type is transformed to an "equivalent" value of another type. A common example of this would be serializing some encoded data value (like a number) into human-readable characters (e.g., converting the integer 1 to the text "1").
In most cases, the type conversion of a value is explicitly requested as part of the expression. Since the purpose of an initializer is to create a new value of a specific type, defining a new initializer for each source value type (e.g., +Text(1)) is a straightforward approach. Alternatively, it can be more convenient to define a method for the source type able to return the conversion of the object's value to the desired type (e.g., 1.toText).
Implicit Number Conversion
When it comes to converting between Cone's built-in primitive number types (integers and floating point numbers), no initializer or method is required; conversion happens implicitly and automatically.
mut v = 4.0 v = 3 // implicitly converted from integer to 3.0 (floating point)
Most of the time, this will not result in the loss of significant information (e.g., u8 to u16 is lossless). But it can. Converting a negative number to an unsigned one could trigger undesirable behavior. Converting a float to an integer will round the number (and going the other way can experience rounding errors). Implicit number conversion should be handled with care.
Coercion works differently than conversion. As shown above, conversion changes the way a value is encoded. Coercion reinterprets an existing value (without altering it) to mean something similar in the context of a different type.
Explicit coercion uses the as infix operator. Unlike other infix operators, as expects a type (rather than a value or expression) to the right of the operator. Coercion is mostly used on references, such as when coercing a reference to a pointer (or vice versa).
imm ptr = &x as *Vector3
Coercion can easily be dangerous, as an unchanged value could behave badly when treated and manipulated within the constraints of a different type. For example, a reference coerced to a pointer loses permission constraints, thereby making it possible for a pointer to be used to alter an immutable object. Because of this safety exposure, coercions are sometimes restricted to trust blocks, where it becomes the responsibility of the programmer to ensure the coerced value is handled in a safe and useful manner.
Implicit Subtype Coercion
Cone supports subtypes, essentially types that are a perfect subset of other types. Being perfect subsets, a reference to a value of a certain type can safely act as if it were a reference to its subtype. Thus, if type S is a valid subtype of some type T. one may pass a value of type T to a function expecting a value of type S. This is implicit subtype coercion.
Implicit subtype coercion is valuable in several situations:
- Interfaces and Traits act as a viewing window into a number of different structs they are valid subtypes of.
- Slices enable "windowed" access to a subset of a collection's values.
- Some permissions have a subtyping relationship. This allows polymorphic functions to safely and temporarily override the permissions of a passed reference value. (e.g., mut to const).