Author: lrn@google.com
Version: 0.7 (2018-06-21)
Status: Mostly designed, ready for external comments.
This proposal introduces a new syntax for declaring mixins, separate from deriving a mixin from a class declaration. It expects to deprecate and remove the ability to derive a mixin from a class declaration, but doesn't require it.
Dart 1 mixins have the following features:
There are a number of problems with this approach, especially the super-class constraints.
super.foo()
) are not statically guaranteed to hit a matching method. There is no specified static check of a mixin application that ensures that any mixed-in methods containing a super-call will actually hit an existing method. If the superclass is abstract, the super-call may fail dynamically.extends
clause which only allows a single type. There is no way to specify two requirements, and users trying to do so ends up with code that doesn't work like they expect.To avoid some of the problems mentioned above, we introduce a mixin declaration syntax separate from class declarations:
mixinDeclaration : metadata? ‘mixin’ identifier typeParameters?
(‘on’ types)? (‘implements’ types)? ‘{’ mixinMember* ‘}’
The mixinMember
production allows the same instance or static members that a class would allow, but no constructors (for now).
The mixin
word will have to be at least a built-in identifier to avoid parsing ambiguities. It does not need to be a reserved word.
It might be possible to just use mixin
as a contextual keyword, but it would require some look-ahead to determine whether an occurrence is a type named mixin
or a mixin declaration, and we would like to discourage the former anyway.
A mixin declaration introduces a mixin and an interface, but not a class. The mixin introduced by a mixin declaration contains all the non-static members declared by the mixin, just as the mixin derived from a class declaration currently does.
In a mixin declaration like mixin A on B, C implements D, E { body }
the on
clause declares the interfaces B
and C
as super-class constraints of the mixin. Having a super-class constaint allows the mixin declaration instance members to perform super-invocations (like super.foo()
) if they are allowed by a class implementing both B
and C
. The mixin introduced by A
can then only be applied to classes that implement both B
and C
.
Further, the interfaces B
and C
must be compatible. The on
clause introduces a synthetic interface combining B
and C
, call it A$super
, which is equivalent to the interface of a class declaration of the form:
abstract class A$super implements B, C {}
It is a compile-time error for the mixin declaration if the class declaration above would not be valid. This ensures that if more than one super-constraint interface declares a member with the same name, at least one of those members is more specific than the rest, and this is the unique signature that super-invocations are allowed to invoke.
A mixin declaration defines an interface. The interface for this mixin declaration is equivalent to the interface of the class declared as:
abstract class A implements A$super implements D, E { body' }
where body'
contains abstract declarations corresponding to the instance members of body
of the mixin A
.
It is a compile time error for the mixin declaration if this class declarations would not be valid.
An omitted on
clause is equivalent to on Object
.
It's a static warning (strong-mode error) if an instance method in a mixin body has a super-access (super.foo
, super.foo()
, super + bar
, etc.) which would not be a valid invocation if super
was replaced with an expression with static type A$super
.
A mixin cannot be marked as abstract
. All mixins are effectively abstract because they don‘t need to implement the members of the required superclass types. We could say that a mixin must implement all other members than the ones declared by the required superclass types, and then allow the declaration to be marked as abstract
if it doesn’t. It would still require mixin applications to be marked independently, so there is no large advantage to marking the mixin itself as non-abstract.
Mixin application syntax is unchanged.
Mixin application semantics is mostly unchanged, except that it‘s a compile-time error to apply a mixin to a class that doesn’t implement all the on
type requirements of the mixin declaration, or apply a mixin containing super-invocations to a class that doesn't have a concrete implementation of the super-invoked members compatible with the super-constraint interfaces.
Forwarding constructors are introduced in the same way as they currently are.
The Dart 1 specification doesn't warn at compile-time if a super
-invocation targets an abstract method. This allows declaring a mixin that extends an abstract interface, but it also means that mistakes are only runtime-errors. We want to fix that.
super
-invocations in mixin applications are valid. The mixin declaration only allows super
-invocations declared by their on
constraints and the mixin application requires the superclass to satisfy those constraints, and by also being non-abstract, there must be an actual implementation of the superclass method. That's probably too restrictive in practice, though (e.g., class UnmodifiableListBase<T> = ListBase<T> with UnmodifiableListMixin<T>;
is reasonable even if ListBase
is abstract).A$super
. This requires a mixin application to check whether the superclass has a concrete implementation of any member of A$super
which is used by a super-invocation. Obviously, if the superclass is not abstract, this check won’t be necessary.The latter option is the more permissive one, but that also comes with a cost of maintainability and usability. If a mixin adds a new super-invocation, then it may break existing mixin applications. It's not possible to see the actual requirements of the mixin from its type signature alone.
If the requirement is just that the superclass is non-abstract (first option), there are no hidden or fragile constraints in the relation between the mixin and the superclass. However it's likely too restrictive in practice, and there is no work-around if you do want to apply a mixin to an abstract class (short of adding throwing implementations of the missing methods, which is not something to encourage).
In either case, this requirement is new. The Dart 1 specification doesn‘t have it, instead it just silently accepts a mixin application on an abstract superclass that doesn’t actually implement the super-member, and the call will fail at runtime.
Current Dart classes can be used as superclasses, mixins and interfaces. A mixin declaration does not introduce a class, but we can, and probably should, allow extending the mixin as a shorthand for extending Object
with the mixin applied.
That is:
mixin M { String toString() => "Magnificent!"; } class C extends M { ... }
would be equivalent to:
mixin M { String toString() => "Magnificent!"; } class C extends Object with M { ... }
as long as M
has no on
clause requiring a class different from Object
.
This allows easier migration from existing classes that are used as both superclass and mixin.
In a future version of Dart, we'll remove the ability to derive a mixin from a class declaration.
This requires existing code to be rewritten. The rewrite is simple:
If the class is only used as a mixin,
class FooMixin extends S implements I { members; }
becomes
mixin FooMixin on S implements I { members; }
If the class is actually used as both a class and a mixin, and S
is not Object
, the mixin needs to be extracted:
class Foo extends S implements I { // Used as mixin *and* class members; }
becomes
class Foo extends S with FooMixin { static members } mixin FooMixin on S implements I { instance members (references to statics prefixed with "Foo.") } // All uses of "with Foo" changed to "with FooMixin".
Apart from static members (which are rare) this is basically a two line rewrite locally, and then finding the uses of the class as a mixin. Any missed use of Foo
as a mixin will be a compile-time error, so the uses are easy to find.
Private static members can be placed in either class, and mayb fit better in the mixin class if they are only used by instance members. Putting them in Foo
ensures that uses outside of the class, but still in the same library, do not need to be changed.
We may want to allow omitting extends Object
from class C extends Object with Mixin {}
, writing it simply as class C with Mixin {}
. The extends Object
is not necessary when declaring a class with no mixin, and the syntax is still easy to parse since with
cannot occur in that position with any other meaning.
This is not a necessary change, but it make some existing code which extends
a mixin-class slightly easier to port.
With separate syntax for mixins, we are open to adding more capabilities without needing it to also work for classes.
Options are:
extend
another mixin, application applies both).v0.5 (2017-06-12) Initial version
v0.6 (2017-06-14) Say mixin
must be built-in identifier.
v0.7 (2018-06-21) Change required
to on
and remove Dart 1 specific things.