Note: Most of this is implemented to some degree.
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.
- Passes 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 a 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.
However, sometimes we do want to transform a value of one type to another type. Cone supports three such capabilities:
- Coercion, which implicitly and safely converts a value to some other expected type (usually a supertype).
- Conversion, which explicitly constructs the closest "equivalent" value in another type. This mapping is imperfect, as some (or all) of the meaning of the original value could be lost.
- Reinterpretation, which explicitly preserves the original value but re-interprets it as if it has a different type. This potentially unsafe capability will be described on another page.
To distinguish between coercion and conversion, it is first necessary to understand how subtyping works in Cone.
Subtypes
Subtyping captures a simple but powerful idea: if we have two types named Base and Subtype, how do we determine when it is safe to substitute in a value of type Subtype whenever our program logic is looking for a value of type Base?
Here are two essential criteria (there are others):
- Every possible value in Subtype can be mapped uniquely to its semantically identical value in Base.
- The behavior of values in Subtype must correspond in every way to the behavior of equivalent values in Base. In our case, that means at minimum that for every method in Base, there exists an equivalent method in Subtype with the same type signature and constraints.
Many type pairs in Cone comply with these criteria and support a safe subtyping relationship:
- Any variant type is a subtype of any trait or struct type that it complies with, even if it does not inherit from it.
- Any non-nullable type is a subtype of its nullable equivalent (e.g., &Foo is a subtype of ?&Foo).
- A borrowed reference is a subtype of an equivalently-typed owning reference (e.g., &Foo is a subtype of &so Foo)
- A pointer is a subtype of any reference whose object type is the same.
- Some reference permissions are subtypes of others (e.g., ro is a subtype of imm or mut). Similarly, most permissions are a subtype of uni.
- A smaller number type is a subtype of a larger version of the same type (e.g., u8 is a subtype of u32).
- A struct is a subtype of any trait it conforms to.
An important characteristic of subtyping relationships is that it is trivially safe to transform a Subtype value into its equivalent Base value. However, the journey of a value back from Base to Subtype often requires some sort of pattern-matched guard, as there are likely to be values in Base that do not have a valid mapping from Subtype to Base. A successful pattern match guarantees we will get back a valid Subtype value when the reverse transformation is applied.
Implicit Coercion
We call it coercion when implicitly converting a value into an expected type (usually a supertype). Coercion is the one safe exception to the strict typing rule described earlier, given that the type of a value and its receiver are not strictly the same.
Since coercion is always safe, Cone performs it implicitly when copying or moving a subtype value to any container (variable, parameter, return value, field, reference) expecting a base value. This happens during assignments, function calls and returns. For example:
fn doStuff (): imm bigint u32 = 101u8 // Coerce a u8 to u32 mut matrix = &so Mat4[] // Allocate a matrix imm float = mat4det(matrix)? // Coerce &so uni Mat4 to ?&Mat4 fn mat4det(mat &Mat4) f32 throws DivByZero: // ... return 1.0 // Coerce f32 to Result<>
Coercion is more than just changing the type of some value. It usually means creating a new value whose encoding is different than its source value.
Coercion can also trigger compiler mechanics: In the case of coercing an owning reference to a borrowed reference, it triggers appropriate borrowing mechanics on the source variable. Similarly, when coercing a non-copy value, move semantics apply.
If an expression requires, it is possible to explicitly request coercion. For non-reference types, use the appropriate constructor (as described below for conversion). For references, use the as operator:
imm newref = oldref as &Point // Explicitly coerce to a borrowed reference
Implicit Coercion to Bool
In addition to supertype coercions, Cone supports the automatic coercion of some value to Bool, so long as the value's type implements the isTrue method. This is implemented by the number and pointer types. A number is true if it is not zero. A pointer is true if it is not null.
Implicit Coercion of 'self'
Cone offers a very different form of coercion when dealing with method calls (or field access) and the 'self' value. This convenience sugar automatically coerces self from a value to a reference (or the other way).
Cone will automatically transform 'self' to a mutable borrowed reference for operators that expect an lval, such as ++, +=, or a 'set' method.
++a // equivalent to: Type::`++`(&mut a)
Automatic de-reference of 'self' may happen when 'self' is a reference, but no method matches with a reference-based self. 'self' is then automatically dereferenced in hopes of finding a matching method that uses a value rather than a reference.
&a + 5 // may coerce to: a + 5
Explicit Conversion
Type conversion involves transforming a value of one type to the closest possible value in another type. For example, converting the integer 1 to the floating point number 1.0, or serializing it to text: "1". Unlike coercion, conversion may not map perfectly: A valid mapping may not exist or some precision may be lost.
Conversion is typically requested using the new type's constructor or invoking one of its initializers
imm float = f32[1] imm text = Text(1) // "1"
In some cases, it may be more convenient to request conversion by using some method in the old type:
imm text = 1.text