Open types
An opentype is an abstract nominal type whose set of concrete datatypes can
grow in other modules. It is useful when a value must hold several concrete
variants while each variant supplies its own implementation. An open type is
not a general inheritance hierarchy: it has no constructors or fields of its
own, and version 1 does not support generic open types.
Declare an open type
Section titled “Declare an open type”List the interfaces that every subtype must implement:
export interface IShape method area(self : Self) -> Intend
export opentype Shape interfaces IShape endAn interface listed by an open type must be usable as a runtime dictionary. In
particular, it cannot declare associated types, and the method sets of the
listed interfaces must not overlap. An open type with no interfaces clause
is still useful as an extensible tag type, but it has no method obligations.
Add a concrete subtype
Section titled “Add a concrete subtype”A normal datatype becomes a subtype with an of clause. Its extensions
list supplies exactly one provider for every interface required by the open
type:
export interface IShape method area(self : Self) -> Intend
export opentype Shape interfaces IShape end
datatype Circle of Shape extensions CircleShape Circle{radius: Int}end
extension CircleShape : IShape for Circle method area(circle : Circle) -> Int do circle.radius * circle.radius endend
func area_of(shape : Shape) -> Int do shape.area()end
func main() -> Unit do circle = Circle::Circle{radius: 3}; Debug.trace(area_of(circle));endWidening Circle to Shape does not allocate a wrapper. The value keeps its
datatype identity, and the constructor prototype carries the provider needed
for the open type’s interface slot. A subtype that omits a provider, provides
two providers for one required interface, or uses a provider with contextual
prerequisites is rejected at compile time.
method and func have different jobs
Section titled “method and func have different jobs”The method marker says that a member participates in receiver dot-call
lookup. Its first positional parameter is the receiver and must have the
interface’s Self type. An extension implementing a method interface member
must also write method:
interface IShape method area(self : Self) -> Intend
datatype Circle extensions CircleShape Circle{radius: Int}end
extension CircleShape : IShape for Circle method area(circle : Circle) -> Int do circle.radius * circle.radius endend
func main() -> Unit do circle = Circle::Circle{radius: 3}; Debug.trace(circle.area());endAn ordinary func member is still stored in the extension dictionary, but it
is called through that dictionary and is not a dot-call method. This is useful
for operations whose receiver is an open value and whose provider must be
selected explicitly:
export interface ILabel func label(value : Self) -> Stringend
export opentype Item interfaces ILabel end
datatype Number of Item extensions NumberLabel Number{value: Int}end
extension NumberLabel : ILabel for Number func label(number : Number) -> String do number.value.to_string() endend
func label_item( item : Item, ?{extension Label: ILabel for item},) -> String do Label.label(item)endThe extension question above is a value-level witness. At runtime it reads
the ILabel slot carried by the concrete subtype. Once a value has been
widened to Item, a caller can pass it to label_item; the omitted answer is
resolved from that value’s prototype. This is not the same as a compile-time
contextual question over a statically known T: the subject is the open value
itself, so the provider can vary from one value to the next.
Dispatch through an open value
Section titled “Dispatch through an open value”For a method member, a call such as shape.area() is resolved from the
open type’s fixed interface list and lowered to the corresponding prototype
slot. A direct extension method that is not part of that list is not visible
through Shape, even if one particular subtype defines it. This keeps the
open-type method set stable and prevents a subtype from changing the meaning
of a call through an already widened value.
For an ordinary func member, use the explicit witness form shown above:
Label.label(item). The dictionary field is called directly and does not add a
hidden receiver argument; pass the receiver as an ordinary argument.
Matching and limitations
Section titled “Matching and limitations”An open type has an unknown, extensible constructor set. A switch over an open type can safely test known subtype constructors, but it must include a wildcard or binder fallback. See Advanced pattern matching for downcast patterns and the exhaustiveness rule.
Open types intentionally leave several features out of the first version:
- no constructors or payloads on the
opentypedeclaration itself; - no generic open types;
- no subtype-of-subtype inheritance, field inheritance,
super, or override priority; - no associated-type interfaces in an open type’s interface list;
- no contribution from
enable extensionsto an open type’s required vtable; the provider must be associated on the subtype.
Use an ordinary datatype when the set of constructors is closed and exhaustive matching is the main operation. Use an interface and extension without an open type when the concrete subject type is already known at each call site.
Next: Structural capabilities.