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.
Extend an open type
Section titled “Extend an open type”An opentype can extend one or more parent open types. A subtype value flows
into its open type and transitively into every ancestor, and the child’s
effective interface set is its own list plus every inherited interface:
export opentype EventTarget end
export opentype Node extends EventTarget end
@js_recognizer("recognize_html_element")export extern datatype HtmlElement of Node @js_tag(7) HtmlElementend
func accept_event_target(_target : EventTarget) -> Unit doend
extern func get_html_element_raw() -> HtmlElement
func inheritance_probe() -> Unit do accept_event_target(get_html_element_raw()); ()endParents must be open types; self-parents, cycles, and duplicate parents are
rejected. Interface method sets must stay pairwise disjoint across the whole
union — there are no overrides. An extern datatype ... of leaf is admitted
on interface-less chains (no interfaces anywhere on the chain): obligations
pass vacuously, explicit @js_tag values are kept, and no vtable is installed
on host prototypes.
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 effective interface set — its own interfaces plus inherited ones —
and lowered to the corresponding prototype slot. When the subtree crosses
into host values, the call instead dispatches directly to the single covering
provider body, since host prototypes carry no installed slots.
An interface method wins over a same-named direct extension: the interface is
the only contract stable across every subtype. When no interface matches, a
direct extension for the receiver open type or one of its ancestors that is
lexically visible through with extensions resolves as a fallback to an
ordinary direct call. A direct extension method on a subtype is still never
visible through the open type, 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 datatype subtype chains, field inheritance,
super, or override priority (opentypeparents with transitive widening are supported, but without overrides); - no associated-type interfaces in an open type’s interface list;
- no contribution from lexically scoped
with extensionsproviders to 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.