Author: eernst@google.com
Status: Obsolete
This proposal is now obsolete. The mechanism has been renamed to extension types. Please see the extension types specification for the accepted specification proposal.
2022.12.09
- Added notification about renaming to inline classes.
2022.09.22
- Removed support for
export
, changedview
toview class
.
2022.09.20
- Updated the inheritance mechanism to fit in with a potential non-virtual
method mechanism for classes: Use
implements
, remove show/hide.
2022.08.30
- Used inspiration from the extension struct proposal and various discussions to simplify and improve this proposal.
2021.05.12
- Initial version.
This document specifies a language feature that we call "view classes".
The feature introduces view types, which are a new kind of type
declared by a new view class
declaration. A view type provides a
replacement of the members available on instances of existing types:
when the static type of the instance is a view type V, the available
instance members are exactly the ones provided by V (noting that
there may also be some accessible and applicable extension members).
In contrast, when the static type of an instance is not a view type, it is (by soundness) always the run-time type of the instance, or a supertype thereof. This means that the available instance members are the members of the run-time type of the instance, or a subset thereof (again: there may also be some extension members).
Hence, using a supertype as the static type allows us to see only a subset of the members. Using a view type allows us to replace the set of members, with subsetting as a special case.
This functionality is entirely static. Invocation of a view member is resolved at compile-time, based on the static type of the receiver.
A view class may be considered to be a zero-cost abstraction in the sense that it works similarly to a wrapper object that holds the wrapped object in a final instance variable. The view class thus provides an interface which is chosen independently of the interface of the wrapped object, and it provides implementations of the members of the interface of the view class, and those implementations can use the members of the wrapped object as needed.
However, even though a view class behaves like a wrapping, the wrapper object will never exist at run time, and a reference whose type is the view class will actually refer directly to the underlying wrapped object. Every member access (e.g., an invocation of a method or a getter) on an expression whose static type is a view type will invoke a member of the view class (with some exceptions, as explained below), but this occurs because those member accesses are resolved statically, which means that the wrapper object is not actually needed.
Given that there is no wrapper object, we will refer to the "wrapped" object as the representation object of the view class, or just the representation.
Inside the view class declaration, the keyword this
is a reference
to the representation whose static type is the enclosing view class. A
member access to a member of the enclosing view class may rely on
this
being induced implicitly (for example, foo()
means
this.foo()
if the view class contains a method declaration named
foo
). A reference to the representation typed by its run-time type
or a supertype thereof (that is, typed by a "normal" type for the
representation) is available as a declared name, which is introduced
by a new syntax similar to a parameter list declaration (for example
(int i)
) which follows the name of the view class. (This syntax is
intended to be a special case of an upcoming mechanism known as
primary constructors.) The representation type of the view class
(with (int i)
that's int
) is similar to the on-type of an
extension declaration.
All in all, a view class allows us to replace the interface of a given representation object and specify how to implement the new interface in terms of the interface of the representation object.
This is something that we could obviously do with a wrapper, but when
it is done with a view class there is no wrapper object, and hence
there is no run-time performance cost. In particular, in the case
where we have a view type V
with representation type R
we may be
able to refer to a List<R>
using the type List<V>
(using theRList as List<V>
), and this corresponds to "wrapping every
element in the list", but it only takes time O(1) and no space, no
matter how many elements the list contains.
A view class is a zero-cost abstraction mechanism that allows developers to replace the set of available operations on a given object (that is, the instance members of its type) by a different set of operations (the members declared by the given view type).
It is zero-cost in the sense that the value denoted by an expression whose type is a view type is an object of a different type (known as the representation type of the view type), and there is no wrapper object, in spite of the fact that the view class behaves similarly to a wrapping.
The point is that the view type allows for a convenient and safe treatment
of a given object o
(and objects reachable from o
) for a specialized
purpose. It is in particular aimed at the situation where that purpose
requires a certain discipline in the use of o
's instance methods: We may
call certain methods, but only in specific ways, and other methods should
not be called at all. This kind of added discipline can be enforced by
accessing o
typed as a view type, rather than typed as its run-time
type R
or some supertype of R
(which is what we normally do). For
example:
view class IdNumber(int i) {
// Declare a few members.
// Assume that it makes sense to compare ID numbers
// because they are allocated with increasing values,
// so "smaller" means "older".
operator <(IdNumber other) => i < other.i;
// Assume that we can verify an ID number relative to
// `Some parameters`, filtering out some fake ID numbers.
bool verify(Some parameters) => ...;
... // Some other members, whatever is needed.
// We do not declare, e.g., an operator +, because addition
// does not make sense for ID numbers.
}
void main() {
int myUnsafeId = 42424242;
myUnsafeId = myUnsafeId + 10; // No complaints.
var safeId = IdNumber(42424242);
safeId.verify(); // OK, could be true.
safeId + 10; // Compile-time error, no operator `+`.
10 + safeId; // Compile-time error, wrong argument type.
myUnsafeId = safeId; // Compile-time error, wrong type.
myUnsafeId = safeId as int; // OK, we can force it.
myUnsafeId = safeId.i; // OK, and safer than a cast.
}
In short, we want an int
representation, but we want to make sure
that we don't accidentally add ID numbers or multiply them, and we
don't want to silently pass an ID number (e.g., as actual arguments or
in assignments) where an int
is expected. The view class IdNumber
will do all these things.
We can actually cast away the view type and hence get access to the interface of the representation, but we assume that the developer wishes to maintain this extra discipline, and won't cast away the view type onless there is a good reason to do so. Similarly, we can access the representation using the representation name as a getter. There is no reason to consider the latter to be a violation of any kind of encapsulation or protection barrier, it's just like any other getter invocation. If desired, the author of the view class can choose to use a private representation name, to obtain a small amount of extra encapsulation.
The extra discipline is enforced because the view member
implementations will only treat the representation object in ways that
are written with the purpose of conforming to this particular
discipline (and thereby defines what this discipline is). For example,
if the discipline includes the rule that you should never call a
method foo
on the representation, then the author of the view class
will simply need to make sure that none of the view member
declarations ever calls foo
.
Another example would be that we're using interop with JavaScript, and
we wish to work on a given JSObject
representing a button, using a
Button
interface which is meaningful for buttons. In this case the
implementation of the members of Button
will call some low-level
functions like js_util.getProperty
, but a client who uses the view
class will have a full implementation of the Button
interface, and
will hence never need to call js_util.getProperty
.
(We can just call js_util.getProperty
anyway, because it accepts
two arguments of type Object
. But we assume that the developer will
be happy about sticking to the rule that the low-level functions
aren't invoked in application code, and they can do that by using view
classes like Button
. It is then easy to grep
your application code
and verify that it never calls js_util.getProperty
.)
Another potential application would be to generate view class
declarations handling the navigation of dynamic object trees that are
known to satisfy some sort of schema outside the Dart type system. For
instance, they could be JSON values, modeled using num
, bool
,
String
, List<dynamic>
, and Map<String, dynamic>
, and those JSON
values might again be structured according to some schema.
Without view types, the JSON value would most likely be handled with
static type dynamic
, and all operations on it would be unsafe. If the
JSON value is assumed to satisfy a specific schema, then it would be
possible to reason about this dynamic code and navigate the tree correctly
according to the schema. However, the code where this kind of careful
reasoning is required may be fragmented into many different locations, and
there is no help detecting that some of those locations are treating the
tree incorrectly according to the schema.
If view classes are available then we can declare a set of view types
with operations that are tailored to work correctly with the given
schema and its subschemas. This is less error-prone and more
maintainable than the approach where the tree is handled with static
type dynamic
everywhere.
Here's an example that shows the core of that scenario. The schema that
we're assuming allows for nested List<dynamic>
with numbers at the
leaves, and nothing else.
view class TinyJson(Object it) {
Iterable<num> get leaves sync* {
if (it is num) {
yield it;
} else if (it is List<dynamic>) {
for (var element in it) {
yield* TinyJson(element).leaves;
}
} else {
throw "Unexpected object encountered in TinyJson value";
}
}
}
void main() {
var tiny = TinyJson(<dynamic>[<dynamic>[1, 2], 3, <dynamic>[]]);
print(tiny.leaves);
tiny.add("Hello!"); // Error.
}
Note that it
is subject to promotion in the above example. This is safe
because there is no way to override this would-be final instance variable.
The syntax (Object it)
in the declaration of the view class causes
the view class to have a constructor and a final instance variable
it
of type Object
, and it can be used to obtain a value of the
view type from a given instance of the representation type. This
syntax is known as a primary constructor.
It is possible to declare other constructors as well; details are given in the proposal. A constructor body may be declared. It could be used, e.g., to verify that the given representation object satisfies some constraints.
In any case, an instance creation of a view type, View<T>(o)
, will
evaluate to a reference to the value of the final instance variable of
the view class, with the static type View<T>
(and there is no object
at run time that represents the view class itself).
The name TinyJson
can be used as a type, and a reference with that
type can refer to an instance of the underlying representation type
Object
. In the example, the inferred type of tiny
is TinyJson
.
We can now impose an enhanced discipline on the use of tiny
, because
the view type allows for invocations of the members of the view class,
which enables a specific treatment of the underlying instance of
Object
, consistent with the assumed schema.
The getter leaves
is an example of a disciplined use of the given object
structure. The run-time type may be a List<dynamic>
, but the schema which
is assumed allows only for certain elements in this list (that is, nested
lists or numbers), and in particular it should never be a String
. The use
of the add
method on tiny
would have been allowed if we had used the
type List<dynamic>
(or dynamic
) for tiny
, and that could break the
schema.
When the type of the receiver is the view type TinyJson
, it is a
compile-time error to invoke any members that are not in the interface of
the view type (in this case that means: the members declared in the
body of TinyJson
). So it is an error to call add
on tiny
, and that
protects us from this kind of schema violations.
In general, the use of a view type allows us to centralize some unsafe operations. We can then reason carefully about each operation once and for all. Clients use the view type to access objects conforming to the given schema, and that gives them access to a set of known-safe operations, making all other operations in the interface of the representation type a compile-time error.
One possible perspective is that a view type corresponds to an abstract data type: There is an underlying representation, but we wish to restrict the access to that representation to a set of operations that are independent of the operations available on the representation. In other words, the view type ensures that we only work with the representation in specific ways, even though the representation itself has an interface that allows us to do many other (wrong) things.
It would be straightforward to enforce an added discipline like this by
writing a wrapper class with the allowed operations as members, and
working on a wrapper object rather than accessing o
and its methods
directly:
// Emulate the view class using a class.
class TinyJson {
// `representation` is assumed to be a nested list of numbers.
final Object it;
TinyJson(this.it);
Iterable<num> get leaves sync* {
var localIt = it; // To get promotion.
if (localIt is num) {
yield localIt;
} else if (localIt is List<dynamic>) {
for (var element in localIt) {
yield* TinyJson(element).leaves;
}
} else {
throw "Unexpected object encountered in TinyJson value";
}
}
}
void main() {
var tiny = TinyJson(<dynamic>[<dynamic>[1, 2], 3, <dynamic>[]]);
print(tiny.leaves);
tiny.add("Hello!"); // Error.
}
This is similar to the view type in that it enforces the use of specific
operations (here we only have one: leaves
) and in general makes it an
error to use instance methods of the representation (e.g., add
).
Creation of wrapper objects takes time and space, and in the case where we
wish to work on an entire data structure we'd need to wrap each object as
we navigate the data structure. For instance, we need to create a wrapper
TinyJson(element)
in order to invoke leaves
recursively.
In contrast, the view class is zero-cost, in the sense that it does
not use a wrapper object, it enforces the desired discipline
statically. In the view class, the invocation of TinyJson(element)
in the body of leaves
can be eliminated entirely by inlining.
View classes are static in nature, like extension members: A view
class declaration may declare some type parameters. The type
parameters will be bound to types which are determined by the static
type of the receiver. Similarly, members of a view type are resolved
statically, i.e., if tiny.leaves
is an invocation of a view getter
leaves
, then the declaration named leaves
whose body is executed
is determined at compile-time. There is no support for late binding of
a view member, and hence there is no notion of overriding. In return
for this lack of expressive power, we get improved performance.
A rule for <viewDeclaration>
is added to the grammar, along with some
rules for elements used in view declarations:
<viewDeclaration> ::=
'view' 'class' <typeIdentifier> <typeParameters>?
<viewPrimaryConstructor>?
<interfaces>?
'{'
(<metadata> <viewMemberDeclaration>)*
'}'
<viewPrimaryConstructor> ::=
'(' <type> <identifier> ')'
<viewMemberDeclaration> ::=
<classMemberDefinition>
The token view
is made a built-in identifier.
A few errors can be detected immediately from the syntax:
If a view declaration named View
includes a
<viewPrimaryConstructor>
then it is a compile-time error if the
declaration includes a constructor declaration named View
. (But it
can still contain other constructors.)
If a view declaration named View
does not include a
<viewPrimaryConstructor>
then an error occurs unless the view declares
exactly one instance variable v
. An error occurs unless the declaration of v
is final. An error occurs if the declaration of v
is late.
The name of the representation in a view declaration that includes a
<viewPrimaryConstructor>
is the identifier id
specified in there, and
the type of the representation is the declared type of id
.
In a view declaration named View
that does not include a
<viewPrimaryConstructor>
, the name of the representation is the name
id
of the unique final instance variable that it declares, and the
type of the representation is the declared type of id
.
A compile-time error occurs if a view declaration declares an abstract
member. A compile-time error occurs if a view declaration has a
<viewPrimaryConstructor>
and declares an instance variable. Finally,
a compile-time error occurs if a view does not have a
<viewPrimaryConstructor>
, and it does not declare an instance
variable, or it declares more than one instance variable.
That is, every view declares exactly one instance variable, and it is final. A primary constructor (as defined in this document) is just an abbreviated syntax whose desugaring includes a declaration of exactly one final instance variable.
// Using a primary constructor.
view class V1(R it) {}
// Same thing, using a normal constructor.
view class V2 {
final R it;
V2(this.it);
}
There are no special rules for static members in views. They can be
declared and called or torn off as usual, e.g.,
View.myStaticMethod(42)
.
This document needs to refer to explicit view method invocations, so we
will add a special primitive, invokeViewMethod
, to denote invocations of
view methods.
invokeViewMethod
is used as a specification device and it cannot occur in
Dart source code. (As a reminder of this fact, it uses syntax which is not
derivable in the Dart grammar.)
We use
invokeViewMethod(V, <T1, .. Ts>, o).m(args)
where V
is a type name denoting a view to denote the invocation of
the view method m
on o
with arguments args
and view type
arguments
T1, .. Ts
.
Similar constructs exist for invocation of getters, setters, and
operators.
For instance, invokeViewMethod(V, <int>, o).myGetter
and
invokeViewMethod(V, <int>, o) + rightOperand
.
We need special syntax because there is no syntax which will unambiguously
denote a view member invocation. We could consider the syntax of explicit
extension member invocations, e.g.,
V<T1, .. Ts>(o).m(args)
,
but this is ambiguous since
V<T1, .. Ts>(o)
can be a view constructor invocation. Similarly,
V<T1, .. Ts>.m(o, args)
is similar to a named constructor invocation, but that is also
confusing because it looks like actual source code, but it couldn't be
used in an actual program.
The static analysis of invokeViewMethod
is that it takes exactly
three positional arguments and must be the receiver in a member
access. The first argument must be a type name View
that denotes a
view declaration, the next argument must be a type argument list, together
yielding a view type V (the type argument list may be empty, to
handle the non-generic case). The third argument must be an expression
whose static type is V or the corresponding instantiated
representation type (defined below). The member access must access a
member of the declaration denoted by View
, or a member of a
superview of that view declaration.
Superviews are specified in the section 'Composing view types'.
If the member access is a method invocation (including an invocation of an
operator that takes at least one argument), it is allowed to pass an actual
argument list, and the static analysis of the actual arguments proceeds as
with other function calls, using a signature where the formal type
parameters of V
are replaced by
T1, .. Ts
.
The type of the entire member access is the return type of said member if
it is a member invocation, and the function type of the method if it is a
view member tear-off, again substituting
T1, .. Ts
for the formal type parameters.
Let e0
be an expression of the form
invokeViewMethod(View, <T1, .. Ts>, e).m(args)
.
Evaluation of e0
proceeds by evaluating e
to an object o
and
evaluating args
to an actual argument list args1
, and then
executing the body of View.m
in an environment where this
and the
name of the representation are bound to o
, the type variables of
View
are bound to the actual values of
T1, .. Ts
,
and the formal parameters of m
are bound to args1
in the same way
that they would be bound for a normal function call. If the body completes
returning an object o2
, then e0
completes with the object o2
; if the
body throws then the evaluation of e0
throws the same object with the
same stack trace.
Getters, setters, and operators behave in the same way, with the obvious small adjustments.
Assume that
T1, .. Ts
are types and V
resolves to a view declaration of the
following form:
view class V<X1 extends B1, .. Xs extends Bs>(T id) ... {
... // Members
}
It is then allowed to use
V<T1, .. Ts>
as a type.
For example, it can occur as the declared type of a variable or
parameter, as the return type of a function or getter, as a type
argument in a type, as the representation type of a view, as the
on-type of an extension, as the type in the onPart
of a try/catch
statement, in a type test o is V<...>
, in a type cast o as V<...>
,
or as the body of a type alias. It is also allowed to create a new
instance where one or more view types occur as type arguments.
A compile-time error occurs if the type
V<T1, .. Ts>
is not regular-bounded.
In other words, such types can not be super-bounded. The reason for this
restriction is that it is unsound to execute code in the body of V
in
the case where the values of the type variables do not satisfy their
declared bounds, and those values will be obtained directly from the static
type of the receiver in each member invocation on V
.
When s
is zero,
V<T1, .. Ts>
simply stands for V
, a non-generic view.
When s
is greater than zero, a raw occurrence V
is treated like a raw
type: Instantiation to bound is used to obtain the omitted type arguments.
Note that this may yield a super-bounded type, which is then a
compile-time error.
We say that the static type of said variable, parameter, etc. is the
view type
V<T1, .. Ts>
,
and that its static type is a view type.
A compile-time error occurs if a view type is used as a superinterface of a class or a mixin, or if a view type is used to derive a mixin.
In other words, a view type cannot occur as a superinterface in an extends
,
with
, implements
, or on
clause of a class or mixin. On the other hand,
it can occur in other ways, e.g., as a type argument of a superinterface.
If e
is an expression whose static type V
is the view type
View<T1, .. Ts>
and m
is the name of a member declared by V
, then a member access
like e.m(args)
is treated as
invokeViewMethod(View, <T1, .. Ts>, e).m(args)
,
and similarly for instance getters, setters, and operators.
In the body of a view declaration DV with name View
and type parameters
X1, .. Xs
, for an invocation like
m(args)
, if a declaration named m
is found in the body of DV
then that invocation is treated as
invokeViewMethod(View, <X1, .. Xs>, this).m(args)
.
This is just the same treatment of this
as in the body of a class.
For example:
extension E1 on int {
void foo() { print('E1.foo'); }
}
view class V1(int it) {
void foo() { print('V1.foo'); }
void baz() { print('V1.baz'); }
void qux() { print('V1.qux'); }
}
void qux() { print('qux'); }
view class V2(V1 it) {
void foo() { print('V2.foo); }
void bar() {
foo(); // Prints 'V2.foo'.
it.foo(); // Prints 'V1.foo'.
it.baz(); // Prints 'V1.baz'.
1.foo(); // Prints 'E1.foo'.
1.baz(); // Compile-time error.
qux(); // Prints 'qux'.
}
}
That is, when the static type of an expression is a view type V
with representation type R
, each method invocation on that
expression will invoke an instance method declared by V
or inherited
from a superview (or it could be an extension method with on-type V
).
Similarly for other member accesses.
Let DV be a view declaration named View
with type parameters
X1 extends B1, .. Xs extends Bs
and primary constructor (R id)
.
Alternatively, assume that DV does not declare a primary
constructor, but DV declares a unique, final instance variable named
id
with declared type R
.
In both cases we say that the declared representation type of View
is R
, and the instantiated representation type corresponding to
View<T1,.. Ts>
is
[T1/X1, .. Ts/Xs]R
.
We will omit 'declared' and 'instantiated' from the phrase when it is clear from the context whether we are talking about the view itself or a particular instantiation of a generic view. For non-generic views, the representation type is the same in either case.
Let V
be a view type of the form
View<T1, .. Ts>
,
and let R
be the corresponding instantiated representation type.
V
is a proper subtype of Object?
. If R
is non-nullable then V
is a proper subtype of Object
as well.
That is, an expression of a view type can be assigned to a top type
(like all other expressions), and if the representation type is
non-nullable then it can also be assigned to Object
. Non-view types
(except bottom types) cannot be assigned to view types without a cast.
In the body of a member of a view declaration DV named View
and declaring the type parameters
X1, .. Xs
,
the static type of this
is
View<X1 .. Xs>
.
The static type of the name of the representation name is the
representation type.
For example, in view class V(R id) {...}
, id
has type R
, and
this
has type V
.
Again, let V
be a view type of the form
View<T1, .. Ts>
,
and let R
be the corresponding instantiated representation type.
If R
is not a view type then we say that V
is a view type
at level zero. If R
is a view type at level k then we say that
V
is a view type at level k + 1.
A compile-time error occurs if the level of V
is undefined.
In other words, cycles are not allowed.
A view declaration DV named View
may declare one or more
constructors. A constructor which is declared in a view declaration is
also known as a view constructor.
The purpose of having a view constructor is that it bundles an approach for building an instance of the representation type of a view declaration DV with DV itself, which makes it easy to recognize that this is a way to obtain a value of that view type. It can also be used to verify that an existing object (provided as an actual argument to the constructor) satisfies the requirements for having that view type.
A primary constructor (R id)
in DV is a concise notation that
gives rise to a constructor named View
(that is, it is not "named")
that accepts one parameter of the form this.id
and has no body.
Moreover, the primary constructor induces an instance variable
declaration of the form final R id;
.
A compile-time error occurs if a view constructor includes a
superinitializer. That is, a term of the form super(...)
or
super.id(...)
as the last element of the initializer list.
In the body of a generative view constructor, the static type of
this
is the same as it is in any instance member of the view, that
is, View<X1 .. Xk>
, where X1 .. Xk
are the type parameters
declared by View
.
An instance creation expression of the form
View<T1, .. Ts>(...)
or
View<T1, .. Ts>.name(...)
is used to invoke these constructors, and the type of such an expression is
View<T1, .. Ts>
.
In short, view constructors appear to be very similar to constructors
in classes, and they correspond to the situation where the enclosing
class has a single non-late final instance variable which is initialized
according to the normal rules for constructors (in particular, it must
occur by means of this.id
or in an initializer list).
This section describes the effect of including a clause derived from
<interfaces>
in a view declaration. We use the phrase
the view implements clause to refer to this clause, or just
the implements clause when no ambiguity can arise.
The rationale is that the set of members and member implementations
of a given view may need to overlap with that of other views. The
implements clause allows for implementation reuse by putting shared
members in a "super-view" V1
and putting V1
in the implements
clause of several view declarations V1
.. Vk
, thus "inheriting" the members of V1
into
all of V1 .. Vk
without code
duplication.
The reason why this mechanism uses the keyword implements
rather
than extends
to declare a relation that involves inheritance is that
it has the same semantics as that of class extension members (a
mechanism which is currently being considered), and view members are
similar to class extension members in that they are statically
resolved.
Assume that DV is a view declaration named View
, and V1
occurs as
one of the <type>
s in the <interfaces>
of DV. In this case we
say that V1
is a superview of DV.
A compile-time error occurs if V1
is a type name or a parameterized type
which occurs as a superview in a view declaration DV, but V1
does not
denote a view type.
A compile-time error occurs if any direct or indirect superview of DV
is the type View
or a type of the form View<...>
. As usual,
subtype cycles are not allowed.
Assume that DV has two direct or indirect superviews of the form
W<T1, .. Tk>
respectively
W<S1, .. Sk>
.
A compile-time error
occurs unless
Tj
is equal to
Sj
for each j in 1 .. k. The notion of equality used here is the same
as with the corresponding rule about superinterfaces of classes.
Assume that a view declaration DV named View
has representation
type R
, and that the view type V1
with declaration DV1 is a
superview of DV (note that V1
may have some actual type
arguments). Assume that S
is the instantiated representation type
corresponding to V1
. A compile-time error occurs unless R
is a
subtype of S
.
This ensures that it is sound to bind the value of id
in DV to id1
in V1
when invoking members of V1
, where id
is the representation
name of DV and id1
is the representation name of DV1.
Assume that DV declares a view named View
with type parameters
X1 .. Xs
and V1
is a superview of
DV. Then
View<T1, .. Ts>
is a subtype of
[T1/X1 .. Ts/Xs]V1
for all T1, .. Ts
where these types are regular-bounded.
If they aren't regular-bounded then the type is a compile-time error
in itself. In short, if V1
is a superview of V
then V1
is also
a supertype of V
.
A compile-time error occurs if a view DV has two superviews V1
and
V2
, where both V1
and V2
has a member named m with distinct
declarations, and DV does not declare a member named m.
In other words, if two different declarations of m are inherited
from two superviews then the subview must resolve the conflict. The
so-called diamond inheritance pattern can create the case where two
superviews have an m, but they are both declared by the same
declaration (so V
is a subview of V1
and V2
, and both V1
and
V2
are subviews of V3
, and V3
declares m, in which case there
is no conflict in V
).
Assume that DV is a view declaration named View
and that the view
type V1
, declared by DV1, is a superview of DV. Let m
be the
name of a member of V1
. If DV also declares a member named m
then the latter may be considered similar to a declaration that
"overrides" the former. However, it should be noted that view method
invocation is resolved statically, and hence there is no override
relationship among the two in the traditional object-oriented sense
(that is, it will never occur that the statically known declaration is
the member of V1
, and the member invoked at run time is the one in
DV). Still, a receiver with static type V1
will invoke the
declaration in DV1, and a receiver with static type View
(or
View<...>
) will invoke the one in DV.
Hence, we use a different word to describe the relationship between a member named m of a superview, and a member named m which is declared by the subview: We say that the latter redeclares the former.
In particular, if two different declarations of m is inherited from two superviews then the subview can resolve the conflict by redeclaring m.
Note that there is no notion of having a 'correct override relation' here. With views, any member signature can redeclare any other member signature with the same name, including the case where a method is overridden by a getter or vice versa. The reason for this is that no call site will resolve to one of several declarations at run time, each invocation will statically resolve to one particular declaration, and this makes it possible to ensure that the invocation is type correct.
Assume that DV is a view declaration and that the view types V1
and V2
are superviews of DV. Let M1
be the members of V1
, and
M2
the members of V2
. A compile-time error occurs if there is a
member name m
such that V1
as well as V2
has a member named m
,
and they are distinct declarations, and DV does not declare a member
named m
. In other words, a name clash among distinct "inherited"
members is an error, but it can be eliminated by redeclaring the
clashing name.
The effect of having a view declaration DV with superviews
V1, .. Vk
is that the members declared by DV as well as all
members of V1, .. Vk
that are not redeclared by a declaration in
DV can be invoked on a receiver of the type introduced by DV.
In the body of DV, a superinvocation syntax similar to an explicit
extension method invocation can be used to invoke a member of a
superview which is redeclared: The invocation starts with super.
followed by the name of the given superview, followed by the member
access. The superview may be omitted in the case where there is no
ambiguity.
For example:
view class V2(Object id) {
void foo() { print('V2.foo()'); }
}
view class V3(Object id) {
void foo() { print('V3.foo()'); }
}
view class V1(Object id) implements V2, V3 {
void bar() {
super.V3.foo(); // Prints "V3.foo()".
}
}
The dynamic semantics of view member invocation follows from the code transformation specified in the section about the static analysis.
In short, with e
of type
View<T1, .. Ts>
,
e.m(args)
is treated as
invokeViewMethod(View, <T1, .. Ts>, e).m(args)
.
Similarly for getters, setters, and operators.
Consider a view declaration DV named View
with representation name
id
and representation type R
. Invocation of a non-redirecting
generative view constructor proceeds as follows: A fresh, non-late,
final variable v
is created. An initializing formal this.id
has the side-effect that it initializes v
to the actual argument
passed to this formal. An initializer list element of the form
id = e
or this.id = e
is evaluated by evaluating e
to an object
o
and binding v
to o
. During the execution of the constructor
body, this
and id
are bound to the value of v
. The value of the
instance creation expression that gave rise to this constructor
execution is the value of this
.
At run time, for a given instance o
typed as a view type V
, there
is no reification of V
associated with o
.
This means that, at run time, an object never "knows" that it is being
viewed as having a view type. By soundness, the run-time type of o
will be a subtype of the representation type of V
.
The run-time representation of a type argument which is a view type
V
is the corresponding instantiated representation type.
This means that a view type and the underlying representation type are considered as being the same type at run time. So we can freely use a cast to introduce or discard the view type, as the static type of an instance, or as a type argument in the static type of a data structure or function involving the view type.
A type test, o is U
or o is! U
, and a type cast, o as U
, where U
is
or contains a view type, is performed at run time as a type test and type
cast on the run-time representation of the view type as described above.
Here is an overview of the subtype relationships of a view type
V0
with representation type R
and superviews V1 .. Vk
, as well
as other typing relationships involving V0
:
V0
is a subtype ofObject?
.V0
is a supertype ofNever
.- If
R
is a top type thenV0
is a top type, otherwiseV0
is a proper subtype ofObject?
. - If
R
is a non-nullable type thenV0
is a non-nullable type. V0
is a subtype of each ofV1 .. Vk
(and a proper subtype unlessV0
is a top type).*- At run time, the type
V0
is identical to the typeR
. In particular,o is V0
ando as V0
have the same dynamic semantics aso is R
respectivelyo as R
, andt1 == t2
evaluates to true ift1
is aType
that reifiesV0
andt2
reifiesR
, and the equality also holds ift1
andt2
reify types whereV0
andR
occur as subterms (e.g.,List<V0>
is equal toList<R>
).
This section mentions a few topics that have given rise to discussions.
In the current proposal there is a subtype relationship between every
view and each of its superviews. So if we have
view V(...) extends V1, V2 ...
then V <: V1
and V <: V2
.
In some cases it might be preferable to omit the subtype relationship,
even though there is a code reuse element (because V1
is a superview
of V
, we just don't need or want V <: V1
).
A possible workaround would be to write forwarding methods manually:
view class V1(R it) {
void foo() {...}
}
// `V` can reuse code from `V1` by using `implements`. Note that
// `S <: R`, because otherwise it is a compile-time error.
view class V(S it) implements V1 {}
// Alternatively, we can write a forwarder, in order to avoid
// having the subtype relationship `V <: V1`.
view class V(S it) {
void foo() => V1(it).foo();
}