There is a new version: https://gist.github.com/lrhn/ab2b5d81c22d0fc0ad8926b5743a50ed
Author: lrn@google.com
Version: 0.9
<view-declaration> ::=
<metadata>
'view' <identifier> <typeParams>? <parameterList> ('is' typeList)? '{'
<member-declaration>*
'}'
Example:
view MyString(String _string) is String {
int operator[](int index) => _string.codeUnitAt(index);
}
The <argumentList>
must have precisely one parameter. Can be positional or named, and required or optional.
The type of that parameter is called the “representation type” of the view.
If the view is generic (has a <typeParams>
), the representation type can depend on the type parameters, as can the is
-clause’s type list.
- The
<member-declaration>
s must not contain any instance variable declarations or non-redirecting generative constructors. - The view must not declare an unnamed constructor. (The primary constructor syntax implicitly introduces an unnamed constructor.)
- The view must not declare a member with the same name as the primary constructor parameter.
- The view must not declare a concrete member with the same base name as an instance member of
Object
(==
,hashCode
,toString
,runtimeType
,noSuchMethod
).
The declaration
view I<X1 extends B1, ..., Xn extends Bn>(T<X1, ..., Xn> name) is S1<X1, ..., Xn>, ..., Sk<X1, ..., Xn> { … }
introduces a new type I
per type instantiation of X1
..Xn
. This is a view type with representation type T<X1,..,Xn>
.
The Si<X1,…,Xn>
represents some type expression which can be parameterized by any of X1
..Xn
. They must no be dynamic
or Never
.
- If
Si<X1,..,XN>
is a view type, with representation typeTi
, thenT<X1,..,Xn>
must be a subtype ofTi
. - Otherwise,
T<X1,..,Xn>
must be a subtype ofSi<X1,..,XN>
.
There must not be any cycles in the is
-clause types. The types must form a DAG (the same type can occur more than once, we allow diamond hiearchies).
The same generic type (view or non-view) must not occur multiple times in the supertype DAG with different type instantiations. (No view Foo(List<int> x) is List<num>, List<Object>
, not view C<X>(X x){}
and view D(int) is C<num>, C<Object>
, and not even if the types are only transitive is
-super-types.)
The type I<X1, …, Xn>
is then an immediate subtype of each Si<X1, …, Xn>
.
The instance members of I have three different forms: View members (refers to a concrete view member declaration), interface members (has a member signature which is a subtype of the signature of the same member on the representation type), and conflicted members (which cannot be invoked).
-
A synthetic unnamed constructor induced by the primary constructor syntax:
-
external const I(T<X1, ..., Xn> name);
which effectively does
name as I<X1, …, Xn>
, a cast which cannot fail for a valid argument. -
A synthetic getter named
name
of the formT<X1, …, Xn> get name => this as T<X1, …, Xn>;
. -
The members declared by
I
itself.- If the member declaration is not abstract (it’s a view member), the member must not be an instance variable declaration, and parameters cannot be declared
covariant
. Such members are view declarations referring to themselves. - If the member is abstract (no body, abstract instance variable), the introduced signatur(s, if non-final field) must be a supertype of the signature of the same-named member of the representation type. Such members are interface members with the same signature as the declaration.
- If the member declaration is not abstract (it’s a view member), the member must not be an instance variable declaration, and parameters cannot be declared
-
For each name n where any of
S1
, …,Sk
has a member of that name, andI
does not declare a member with the same name, and the name is notname
:- I has a declaration for n found as follows:
- Let M be the set of declarations of n of
S1
..Sk
.- If
Si
does not have a member of that name, there is no declaration forSi
. - If
Si
is a view type with a member named n, then M contains the declaration of that view member ofSi
(found recursively using this algorithm.) - If
Si
is a non-view type with a member named n, then M contains the member signature for n inSi
.
- If
- If M contains precisely one element (either because only one of
Si
..Sk
had an n member, or because multiple ones had a member, but they all referred to the same view member declaration, or all had the same member signature), the declaration of n inI
is that member. (It can be a view member declaration, and interface member signature, or a conflict). - If M contains a conflict, the declaration of n in
I
is a conflict. - If M contains two distinct view member declarations, the declaration of n in
I
is a conflict. - If M contains a view member declaration and an interface signature, the declaration of n in
I
is a conflict. - If M contains no view members or conflicts, only multiple distinct interface member signatures, use the algorithm used for resolving multiple interface member signatures in an interface. If that succeeds and finds a signature which is a valid override of all the signatures of M, then that is the declaration of n in
I
. If not, the declaration of n inI
is a conflict.
-
If
k
is zero, treat asis Object?
, which has only the interface members declared byObject
andNull
.
The type of a view
declaration is, statically, like any other nominal type. It’s a subtype of the types defined above.
An expression of the form o.member(args)
is type-inferenced as follows:
- Infer types for
o
and Let v bet its static type. - If v is not a view type, proceed as normal.
- Otherwise, if the declaration of
member
in v is:- A conflict, a compile-time error occurs.
- An interface member signature s, continue as an invocation of that interface member signature.
- A view member declaration m, continue as an invocation of that member’s interface signature.
- If there is no declaration of
member
inv
, continue as normal (extension methods may apply).
The view type is entirely erased at runtime, replaced by its representation type, as if it was a type alias.
Member invocations o.member(args)
on receivers, o
, with a view type as static type, T are evaluated as follows:
- Evaluate
o
to a valuev
. - Evaluate
args
to an argument list. - If the declaration of
member
on T is a view method declaration, invoke that method declaration withthis
bound tov
and the argument list bound to the parameters. - If the declaration of
member
on T is a non-view method signature, invoke.member(args)
onv
as a normal method invocation. - Otherwise proceed as normal (possible extension member invocation).
Any runtime subtype check of the form is ViewType
/as ViewType
/on ViewType
performs the same check using the representation type of ViewType
.
I’m going with lazy conflict errors. A conflict between super-type members only matters if not shadowed, and the member is actually invoked.
The alternative is to make it an early error, requiring the view
declaration to declare members which resolve the conflict.
“Forwarding” to instance members happens automatically using is InterfaceType
or an abstract int member();
. The semantics is not forwarding, but to call the normal instance member directly.
There is no show
/hide
option to show part of an interface. You have to use abstract members for that. The is InterfaceType
inherits every member which isn’t shadowed by another declaration.
You can restrict parameters in views, like
view SafeSet<T>(Set<T> _set) is Set<T> {
// Shadows `bool Set.contains(Object?)`.
bool contains(T value) => _set.contains(value);
}
or
view SafeSet<T>(Set<T> _) is Set<T> {
// "Forwards" to `bool Set.contains(Object?)`.
bool contains(T value);
}
In the latter case, the restricted type argument ensures that a valid invocation on SafeSet
is also a valid invocation on the representation type.
Making the primary constructor mandatory and public strongly signals that there is no restriction on which values of the representation type can be typed at the view type.
Adding other constructors, forwarding or factory, is still possible, but it’s not possible to pretend that view EvenInt(int x)
can only contain even integers.
You can still create a type and pretend that it is somehow a subset of the view type, and as long as people use it carefully, it will be true. There will just always be a prominent primary constructor showing that it cannot possibly be enforced.
Always making the primary constructor be the unnamed constructor can get in the way of some designs. On the other hand, it’s very predictable and readable. Invoking an unnamed view constructor can be read as being a simple injection in the view type, without having to look up the constructor.
We can allow the default constructor to be named, with syntax view Name.name(args)
, if we want to. I think we shouldn’t.
This is basically re-introducing the ability to unveil part of the representation type interface. I think we'll need to do that at some point anyway.
I'm not so convinced that the ability to have both superviews and representation-type-supertypes in the same clause (the
is
clause) is helpful: It doesn't communicate to the reader (or writer) of a view declaration that those two things are completely different (unveiled representation type members have OO dispatch, view members are resolved statically, representation type members will remain the same for dynamic invocations and for any typed invocation, but view members may change their behavior based on the static type, including the change from the current view type to one of the superviews).So I'd like to have the features, but I'd want to specify superviews and representation-type-unveiling types in two separate clauses.
PS: The type argument lists in the section 'Declaration' should not be
X1 .. Xn
, each ofT
andSj
, j in 1 .. k, should have their own type argument lists (orSj
andT
should be considered to be potentially parameterized types).