Author: Leaf Petersen
Status: In progress
Version 1.0 (see CHANGELOG at end)
This document is an overview of an approach to solving the following problems:
- The desire for a zero cost wrapper type (motivated largely by interop concerns), described briefly here
- The desire to have compact syntax for so called "data classes" or "value types", discussed among other places here
- The desire to have an accounting for data structures without identity as described here.
The proposal here takes two steps.
- The first is to add a restricted kind of class (provisionally describe as a "struct" here), which gives up some of the affordances of general Dart classes in exchange for compact syntax and automatic generation of useful methods. These restricted classes provide data class like functionality: they are immutable, they have structural identity, and they get a number of conveniently auto-generated methods.
- The second is to support a further restriction on structs with a single field which eliminates the wrapper object, representing the struct entirely as the underlying object (at the cost of making the abstraction entirely static).
This proposal builds on previous proposals in this space, including:
This overview is not intended to be a feature specification: it is designed to give a brief overview of the core proposal and set out some design points for discussion. If there is buy-in on these ideas, we may choose to incorporate them into an existing proposal, or add a new feature specification.
We aim to minimize the differences between structs and classes. As much as possible, structs should behave as restrictions of classes, and extension structs should behave as further restrictions of structs. We specifically aim to avoid as much as possible having different behaviors for the same concept (e.g. differences in scoping).
We also aim to minimize the amount of new syntax required, and to maximize the amount of new functionality that we can provide relative to the syntactic real estate consumed, and the new cognitive load imposed on users.
Concretely for the purposes of this proposal, we have started with the following goals:
- If a struct or an extension struct
B
says that itimplements
orextends
A
, then:- It should be the case that
B
is a subtype ofA
- It should be the case that
B
has a superset of the method names/signatures ofA
- It should be the case that assigning an instance of
B
to a location of typeA
, while it may change the set of members available, should not change the dispatch of any members (that is, iff
is available onB
, then callingf
through either interface reaches the same code).
- It should be the case that
It is not clear that all of these goals can be met while meeting requirements. In particular, the last goal is incompatible with overriding methods, given our intended semantics, and it is likely that allowing some form of overriding is a requirement.
The first section describes the full struct feature. The second section describes the restriction of the full struct to support static wrappers.
This section describes how structs look and behave, largely by example. In many cases, alternatives or extensions to the core proposal are described in line. Larger extensions are postponed to a later section.
Structs allow you to define simple classes containing immutable data very compactly. For example, we might model a component vector as follows:
struct Component(int x, int y, int z, int w = 1);
This short definition essentially defines a class Component
, with four fields,
and provides a number of convenient methods.
void test() {
// Call the default constructor, using the default value for w
var c1 = Component(0, 0, 0);
// Call the default constructor, explicitly passing w
var c2 = Component(0, 0, 0, 1);
assert(c1 == c2); // Equality is defined to be structural
assert(c1.hashCode == c2.hashCode); // With correspondind hashCode
print(c1.debugToString()); // Prints "Component(0, 0, 0, 1)"
print(c1.toString()); // Prints "struct"
// Generated copy method
var c2 = c1.copyWith(x : 2);
assert(c2.x == 2);
}
Structs may explicitly define constructors, static members, and normal class
members (except fields), and may implement interfaces. All fields must be
declared in the header (the "primary constructor"), and are implicitly final
(making all structs shallowly immutable). Fields may be marked as private as
usual by naming them with a leading _
in their name.
struct Component(int _x, int _y, int _z, int _w = 1)
implements Comparable<Component> {
double get x => _x/_w;
double get y => _x/_w;
double get z => _z/_w;
int compareTo(Component other) => throw "TODO: Unimplemented";
Component.zero() : _x = 0, _y = 0, _z = 0;
}
With the addition of pattern matching and switches, structs will support closed families (algebraic datatypes).
abstract struct Operand;
struct ConstantOperand(Value c) extends Operand;
struct IdentifierOperand(Identifier i) extends Operand;
Operand replaceIn(Operand oper, Identifier t, Value c) {
return switch (oper) {
case ConstantOperand(_) : oper;
case IdentifierOperand(var i) where i == t: ConstantOperand(c);
case IdentifierOperand(_) : oper;
}
}
We add a new keyword "struct", which is used in place of "class". The class name (plus generics if applicable) must be followed by a parenthesized list of field declarations (more on this below). There may optionally be an "extends" clause, and an "implements" list. No mixins are permitted. After the header, the usual brace delimited list of members may be provided subject to restrictions described below. An empty set of members may be elided in favor of a semi-colon. The simplest possible struct definitions then would look like:
struct Data();
struct GenericData<T>();
An alternative is to continue to use "class" and to use some other piece of syntax to indicate that the object in question is not a general class. For example, the parenthesized list might be enough:
class Data();
class GenericData<T>();
Alternatively, an additional keyword could be used:
class Data wraps ();
class GenericData<T> wraps ();
Other keywords could be considered.
Instead of "struct", we could use "view", or some other choice:
view Data();
view GenericData<T>();
Perhaps record:
record Data();
record GenericData<T>();
We could use "data class":
data class Data();
data class GenericData<T>();
Another option would be "view class".
The only new piece of syntax in this section of the proposal (other than the "struct" keyword) is the primary constructor which follows immediately after the class name + generics. The primary constructor serves both to define the fields, and to define the signature of the default generated constructor.
The primary constructor consists of a parenthesized list of comma separated
<type> <identifier> (= <expression>)?
entries. That is, a list of variable
declarations: with no modifiers; with types required; and with optional
initializer values.
Each entry in the list is a field in the struct. Every field is implicitly final. They may not be late, and a type must be provided.
COMMENTARY(leafp): *The restriction to final fields is both because that's the common use case, and because structural identity doesn't make much sense if we allow them to be mutable. We could potentially box each mutable field into heap allocated ref cell but that feels very unpleasant, and has perf implications that don't match the intended use cases. *
If an initializer is provided for an entry, all subsequent entries must also have an initializer provided (see generated members below).
COMMENTARY(leafp): This is to allow initializers to double as default values for the generated constructor. This may be too cute. Alternatively we could forbid initializers, or just make fields with initializers not available to the constructor (as with a normal class)
It is an error if two entries have the same name, and it is an error if one
entry consists of the name of another entry except prefixed with _
. That is,
all entries must be uniquely named after ignoring privacy.
COMMENTARY(leafp): This is to allow us to use the non-private version of the name as a named parameter in methods
Examples:
struct Data(int x, List<int> l = [3]);
struct GenericData<T>(T x, T y);
Ignoring generated members, these two structs are roughly equivalent to the following classes:
class Data {
final int x;
final List<int> l = [3]
}
class GenericData<T> {
final T x;
final T y;
}
We could choose to make this look more directly like a constructor argument list by allowing "named" parameters inside of a brace delimited set. These could then become named parameters in the default constructor. Example:
struct Component(int x, int y, int z, {int w = 1});
void test() {
// Call the default constructor, using the default value for w
var c1 = Component(0, 0, 0);
// Call the default constructor, explicitly passing w
var c2 = Component(1, 1, 1, w: 1);
...
}
We could choose to make the field declarations look exactly like classes instead of using a primary constructor syntax.
struct Component {
int x;
int y;
int z;
int w = 1;
}
COMMENTARY(leafp): Given the constraint that structs are immutable, I don't
like that int x
here means a different thing than it does in a class (since
it is implicitly final). We could therefore require each one to be written as
final int x
and make it an error to have a non-final variable. This feels
very boilerplate-heavy to me relative to the compact one line form.
A struct may contain static and instance member definitions in the same way as a class, with exactly the same syntactic resolution.
For scope resolution purposes, entries in the primary constructor list are treated exactly as if they were defined as instance members on the struct.
It is an error for a struct to define a field as a member.
Structs may be marked abstract, in which case no primary constructor may be provided. This is supported to allow families of structs sharing a common super-interface which provides clean support for algebraic data types (see the section on extension below).
COMMENTARY(leafp): We could possibly allow hierarchies of abstract super-structs. We could also potentially allow super-structs to define fields, which would be treated as abstract fields which the sub-structs must provide
A struct may extend another struct from the same library. It is an error if the super-struct is not abstract.
abstract struct Foo {
int foo() => 3;
}
struct Data(int x, List<int> l = [3]) extends Foo ;
struct GenericData<T>(T x, T y) extends Foo;
Members are inherited from super-structs as usual.
It is an error for a struct to extend a class, except Object.
It is an error for a struct to be extended outside of the defining library.
It is an error for a struct to be extended by a class.
Extension is supported primarily to allow compact definition of algebraic datatypes. We expect that structs would be incorporated into switches with extended exhaustiveness checking as proposed in the patterns proposal in the obvious way.
It is an error to implement a struct. Structs do not define interfaces.
Structs may implement interfaces, subject to the usual member conformance checks.
Modulo generated members (see below) and identity (see below), structs behave semantically exactly as if they were de-sugared into classes in the obvious way.
The identity operator on structs may return true
for structs such that:
- Both have the same runtime type
- For every pair of corresponding fields, identical could validly return true on that pair.
The identity operator on structs may always return false.
In other words, the identity operator may be used as a "fast" cut-off for equality, but compilers are free to box and unbox structs at will without preserving identity.
The intention is that the identity operator should simply serve as a fast check whether the two objects in question are "pointer equal", but it is valid for compilers to make other choices based on implementation concerns, pragmas, etc.
An abstract struct has no generated members. For non-abstract structs, the following members are automatically derived by the compiler.
Every non-abstract struct defines a new private constructor with a hidden compiler generated name. We refer to this constructor as the generated primary constructor.
The generated primary constructor has a single positional parameter for every entry in the primary constructor list, each with the obvious type.
If any entry has an initializer value provided, then every entry after (and including) that entry is an optional parameter with no default.
For every entry with an initializer value, if no argument is passed for that parameter, the initializer value is assigned to that parameter in the initializer list of the constructor. Note that this requires the ability to detect whether or not a parameter was passed, which is not expressible strictly as a de-sugaring.
Note that initializers are not required to be constant, so this de facto adds non-const default values in a very limited case.
If no default constructor is defined in the class, a default constructor is generated which forwards to the generated primary constructor.
COMMENTARY(leafp): The treatment of initializers as default values here is appealing, but it may be too cute. It essentially adds non-const default values only for this specific use case. There's also a question of whether we allow user defined constructors on the struct to also override the default initializer, and if so via what syntax.
These could be made named parameters. This feels heavyweight, but has some advantages. The restriction on names in the primary constructor would allow us to use the non-private versions of the field names as the parameter names.
If no equality method is defined in or inherited by the struct (except from Object), then an equality method will be defined which checks that its argument has the same runtime type as the receiver (actual runtime type, not the result of calling "runtimeType"), and that the fields of the two objects are pointwise equal.
If no hashCode getter is defined in or inherited by the struct (except from Object), then a hashCode getter will be defined which hashes the runtime type (again, actual type) together with the hashes of each of the fields.
If no toString method is defined in or inherited by the struct (except from Object), then a toString method will be defined which returns "struct".
If no debugToString method is defined in or inherited by the struct, then a
debugToString method will be defined which returns a formatted description of
the receiver, of the form <type>(<f0>, ..., <f1>)
where <type>
is the
runtime type of the receiver, and the fi
are the result of calling
"debugToString" on the i
th field if that field is (dynamically) a struct, and
otherwise the result of calling "toString".
COMMENTARY(leafp): This needs a bit of work. If we keep the dynamic check for "struct-ness", then we need do deal with the case that the user defines a debugToString thing with the wrong type.
COMMENTARY(leafp): If we keep this, we may wish to specify that compilers may choose to make it a link time error to have an invocation of debugToString in the program outside of asserts, and other debugToString methods.
If no copyWith method is defined in or inherited by the struct, then a copyWith
method will be defined with named parameters for every entry in the primary
constructor. For every entry, if the field name is not private, then the
parameter name is the field name. If the field name is private, then the
parameter name is the field name with all leading _
s removed.
The body of the method calls the generated primary constructor. For every argument which is explicitly passed to the copyWith method invocation, the corresponding parameter is passed on as the argument to the corresponding parameter of the generated primary constructor. For every argument which is not passed to the copyWith method invocation, the value of the corresponding field from the current instance is passed on as the argument to the corresponding parameter of the generated primary constructor.
COMMENTARY(leafp): As with constructors, this method cannot be generated as a strict de-sugaring, since the ability to detect whether or not an argument has been passed is not available in Dart. Implementations already support this for default values, so it is likely not problematic to implement.
We may wish to also define additional generated members. For example, we may
wish to have parse
and unParse
methods. TODO(leafp): consider fleshing
something out here.
It is an error to override the "runtimeType" method of a struct.
It is an error to override the "noSuchMethod" method of a struct.
TODO(leafp): This should work, write out the details.
Extension structs are restrictions of the core struct feature, designed to support wrapper-less views on an object. This section describes how extension structs look and behave, largely by example. In many cases, alternatives or extensions to the core proposal are described in line.
Extension structs are targeted at relatively niche uses where you wish to define a set of statically dispatched methods layered on top of an underlying representation, without introducing a wrapper object. A canonical use case driving this design is to be able define a Dart typed interface for methods on a Javascript object, providing a wrapperless interoperation capability.
extension struct Window(JSObject o) {
// Signatures provided here for methods to be delegated to the underlying
// JSObject
external bool get closed;
// etc
}
void test(Window w) {
// Window methods can be called using Dart syntax
if (w.closed) {...}
// Windows are represented as the underlying object
assert(w is JSObject);
// Windows can be cast to the underlying object
JSObject o = w as JSObject;
// Extension struct types are reified as the underlying representation type.
List<Window> l = [w];
assert(l is List<JSObject>);
}
Extension structs are also useful for providing a lightweight facade over an existing type.
// Natural numbers
extension struct Nat(int _x) {
Nat(int x) : assert(x >= 0), _x = x;
Nat.zero() : _x = 0;
Nat get succ => Nat(_x+1);
Nat plus(Nat other) => Nat._x + other._x;
// Override the underlying isNegative operation (incorrectly)
bool get isNegative => true;
}
void test() {
var n1 = Nat(3);
var n2 = Nat.zero();
assert(n2.succ.succ.succ == n1);
//The underlying representation is still as an int
assert(n1 is int);
// The static type is used to dispatch the method calls
assert(n1.isNegative);
// If the static type is lost, dispatch goes to the underlying object.
dynamic d = n1;
assert(!d.isNegative);
}
Extension structs may delegate members to the underlying field. They may also implement interfaces, but only if the underlying field implements the interface.
// Natural numbers
extension struct Nat(int _x) implements Comparable<num> {
// Constructors etc as above
bool get isEven; // Abstract definition delegates to _x.isEven
}
void test() {
var n = Nat(2);
// Same as 2.isEven
assert(n.isEven);
// Comparable interface allows access to the Comparable methods on int
assert(n.compareTo(2) == 0);
// Since Nat implements Comparable<num>, it may be assigned to it
Comparable<num> c = n;
// The representation is still as an integer
assert(c is int);
}
Extension structs have no inheritance.
// Static error
extension struct PositiveNumber(int _x) extends Nat {...}
Extension structs do not define a signature, and hence cannot be implemented.
// Static error
extension struct AlternativeNat(int _x) implements Nat { ...}
// Static error
class MockNat implements Nat {...}
Extension structs can define their own constructors as usual, replacing or adding to the generated constructors.
extension struct Window(JSObject o) {
Window.cons() : o = Window(js_util.callConstructor(...));
}
Extension structs are defined by adding the keyword "extension" before a struct definition. The syntax for extension structs is otherwise exactly identical to that of general structs. However, extension structs are subject to additional restrictions. The most important of these is that extension structs may only contain a single entry in their primary constructor.
extension struct Data(int x);
extension struct GenericData<T>(T y);
There are various alternatives for the core "struct" feature described in the previous section, and any alternative choice made there would impact this feature correspondingly. Ignoring that, several alternative syntaxes for extension structs are on the table.
The choice of the keyword "extension" is intended to leverage users existing intuitions about how extension methods work: that is, that they are largely statically dispatched. It's not clear to me that this intuition actually carries over, however. There are various alternatives to "extension".
We could use "static", reflecting the static nature of the dispatch.
static struct Data(int x);
static struct GenericData<T>(T y);
We could use "view" reflecting the fact that we are presenting a "view" on an object.
view struct Data(int x);
view struct GenericData<T>(T y);
We could use "typedef" reflecting the fact that we are largely defining a static construct:
typedef struct Data(int x);
typedef struct GenericData<T>(T y);
We could use "type", or "new type".
Primary constructor lists for extension structs are exactly identical to those of normal structs, with the restriction that they may contain only one entry.
Examples:
extension struct Data(int x);
extension struct GenericData<T>(T y);
COMMENTARY(leafp): The point here is that the actual runtime representation of the extension struct will simply be the value of the single unique field, with no wrapper object.
An extension struct may contain static and instance member definitions in the same way as a class, with exactly the same syntactic resolution.
For scope resolution purposes, entries in the primary constructor list are treated exactly as if they were defined as instance members on the extension struct.
It is an error for an extension struct to define a field as a member.
It is an error if an extension struct declares an abstract member, unless a member of the same name and kind is available on the unique field of the extension struct, and the type of the abstract member is a supertype of the type of the corresponding member in the unique field.
COMMENTARY(leafp): Abstract members here allow delegation of methods without having to write an explicit forward. We could elide this.
Extension structs may not be be marked abstract.
An extension struct may not be extended.
COMMENTARY(leafp): This may be contentious. If we end up needing some form of inheritance, either to build up a subtype hierarchy or to allow code re-use, there are probably paths we can take here, but it will be important for the purposes of this design to keep this consistent with the behavior of general structs.
It is an error to implement an extension struct. Extension structs do not define interfaces.
It is an error to use the type defined by an extension struct as a bound on a generic type parameter.
COMMENTARY(leafp): This is probably harmless, maybe we should allow it.
Extension structs may implement interfaces if and only if each implemented interface type is a supertype of the type of the unique field in the primary constructor list. That is, implemented interfaces must be implemented by the field, which will serve as the underlying object representation.
COMMENTARY(leafp): The driving motivation for this design choice is keep
the behavior of extension structs consistent with general structs. For
general structs and classes, implementing an interface means that the newly
defined type is both a subtype of that interface, and supports all of the
methods of that interface. If we wish to preserve the former for extension
structs, then we must ensure that the underlying representation object also
implements the same interface, so that when we assign it, we do not break
soundness. We could give up on fully subtyping and instead only allow
assignability, with conversion to the implemented interface requiring boxing,
but I have chosen not to do that, since it still makes subtyping unavailable.
That is, under this proposal, for an extension struct Foo that implements Bar,
Foo
is assignable to Bar
with no boxing, and List<Foo>
is assignable to
List<Bar>
. If we auto-boxed on assignment to Bar
, we could preserve the
former, but not the latter.
An extension struct is a subtype of each of its implemented super-interfaces.
An extension struct implements Object.
It is an error if any member of an implemented interface has a non-abstract definition in the body of the extension struct.
COMMENTARY(leafp): This restriction is to avoid the confusing behavior that
would result from different dispatch behavior depending on whether the member
is accessed via the extension struct interface, or via the implemented
super-interface. That is Foo
is an extension struct that implements
Comparable<Foo>
and defines its own compareTo
method, then accessing the
compareTo method on a value of type Foo
will call a different method than
first assigning the value to a variable of type Comparable<Foo>
and then
calling the method. It may be that it is too important to support
"overriding" here though, and so we may need to relax this restriction.
Extension structs have no representation at runtime. The values of an extension struct type are the values of the single unique field in the extension struct. We refer to this unique field in the following section as the "underlying representation" of the extension struct.
The identity operator on an instance of an extension struct is defined to return the same result as applying the identity operator to the underlying representation of the extension struct.
Members of extension structs are statically dispatched. That is, any member defined in the body of the extension is only reachable via invoking the member name on a value of the static type of the extension struct, and the member which is invoked is always exactly that which is defined in the extension struct.
Abstract members of extension structs delegate directly to member on the underlying representation.
For every member in the combined super-interface of an extension struct, the extension struct is treated as defining a member whose signature is given by the combined super-interface, and which delegates to the underlying representation.
The type introduced by an extension struct is entirely static, and is replaced at runtime by the type of the unique field in the extension struct. All runtime instance tests and casts are done using the resulting erased type.
For extension structs, the following members are automatically derived by the compiler.
The default generated constructor for an extension struct is simply a degenerate single field version of the standard default generated constructor defined for a normal struct.
Equality delegates to the underlying representation.
The hashCode getter delegates to the underlying representation.
The toString method delegates to the underlying representation.
If no debugToString method is defined in the extension struct, then a
debugToString method will be defined which returns a formatted description of
the receiver, of the form <type>(<f0>)
where <type>
is the extension struct
type via which the receiver is called, and <f0>
is the result of calling
"debugToString" on the unique field if that field is (dynamically) a struct, and
otherwise the result of calling "toString".
No copyWith method is generated for an extension struct.
It is an error to define any of the object members in the extension struct.
COMMENTARY(leafp): This restriction is to avoid the same confusing behavior
described above in the section on implementing interfaces. Almost all uses of
hashCode
will be done via the Object
interface, and it seems dangerous to
allow users to define a hashCode
getter that will be ignored when the value
is used as (e.g.) a key in a map.
TODO(leafp): This should work, write out the details.
We could choose to allow abstract structs to define a subset of the fields in a primary constructor, interpreting them as abstract fields. e.g.
abstract struct ColorPoint(int color);
struct ColorPoint2D(int color, int x, int y) extends ColorPoint;
would be roughly equivalent to:
abstract class ColorPoint {
abstract int color;
}
class ColorPoint2D extends ColorPoint{
final int color;
final int x;
final int y;
ColorPoint2D(this.color, this.x, this.y);
// More generated methods here
}
We could allow extending concrete structs.
abstract struct ColorPoint(int color);
struct ColorPoint2D(int color, int x, int y) extends ColorPoint;
struct ColorPoint3D(int z) extends ColorPoint2D;
which would be roughly equivalent to:
abstract class ColorPoint {
abstract int color;
}
class ColorPoint2D extends ColorPoint{
final int color;
final int x;
final int y;
ColorPoint2D(this.color, this.x, this.y);
// More generated methods here
}
class ColorPoint3D extends ColorPoint2D {
final int z;
ColorPoint3D(super.color, super.x, super.y, this.z);
// More generated methods here
}
COMMENTARY(leafp): I would prefer, at least as a starting point, to forbid overriding of the fields in sub-structs, to make it easier to compile structs to something with a predictable memory layout. For the same reason, I would propose to continue to treat these as non-extensible outside of the defining library
To support non-trivial subtyping hierarchies using extension structs, we could choose to allow extension structs to extend other extension structs, subject to the requirement that the field type remains the same.
extension struct Nat(int _x) {
Nat(int x) : assert(x >= 0), _x = x;
Nat.zero() : _x = 0;
Nat get succ => Nat(_x+1);
Nat plus(Nat other) => Nat._x + other._x;
}
extension struct Pos extends Nat {
Pos(int x) : assert(x >= 1), super(x);
}
The semantics of extension would be, as with implements
, that all methods on
the super-struct type are statically available on the sub-struct, but no
overriding is allowed.
We could choose to allow extension structs to require a more specific type for the unique field.
extension struct Number(num _x);
extension struct Integer(int _x) extends Number;
The core proposal forbids overriding, to avoid the surprising behavior where the same object resolves methods differently depending on the static type. We could choose to relax this, at the cost of some surprising behavior.
extension struct EvenInteger(int x) {
bool get isEven => true;
}
// Truly odd integers.
extension struct OddInteger(int x) extends EvenInteger {
bool get isEven => false;
}
void test() {
OddInteger i = OddInteger(2);
assert(!i.isEven); // Dispatch goes to OddInteger.isEven
EvenInteger e = i; // Ok
assert(i.isEven); // Dispatch goes to EvenInteger.isEven
}
COMMENTARY(leafp): We could in principle not enforce the usual subtyping constraints on overriding, but in practice I think this should be done.
The close correspondence between structs and extension structs suggests the
possibility of saying that every extension struct
declaration implicitly
defines a corresponding struct
declaration, which behaves exactly as if the
same declaration had been made except with the extension
prefix removed. For
an extension struct Foo
, we might choose to name these implicit types as
Foo.struct
.
extension struct Nat(int _x) {
Nat(int x) : assert(x >= 0), _x = x;
Nat.zero() : _x = 0;
Nat get succ => Nat(_x+1);
Nat plus(Nat other) => Nat._x + other._x;
// Override the underlying isNegative operation (incorrectly)
bool get isNegative => true;
}
void test() {
Nat n = Nat(2);
n.succ(); // Returns Nat(3)
// (n as dynamic).succ(); // noSuchMethod
assert(n.isNegative);
int i = n as int; // Succeeds
assert(!i.isNegative); // Dispatch goes to the integer method
// Nat.struct is the type which would be defined by the same struct
// definition above, with the extension prefix removed.
Nat.struct b = n.struct;
b.succ(); // Returns Nat(3)
(b as dynamic).succ(); // Returns Nat(3)
assert(b.isNegative);
// int i = n as int; // Case fails
}