Mixins in Dart
Written by Gilad Bracha
December 2012
This document describes mixins in Dart.
The semantics are deliberately restricted in several ways,
so as to reduce disruption to our existing implementations,
while allowing future evolution toward a full-fledged mixin implementation.
This restricted version already provides considerable value.
The intent is to incorporate mixins into Dart in M3.
Contents
- Basic concepts
- Syntax and semantics
- Possible issues
- Privacy
- Statics
- Types
- Extensions
- Spec changes
- 7. Classes
- 7.9 Superclasses
- 9. Mixins
- 14. Libraries and Scripts
- 15.3.1 Typedef
- 15.4 Interface Types
- 16.1.1 Reserved Words
Basic concepts
If you are familiar with the academic literature on mixins
you can probably skip this section.
Otherwise, please do read it,
as it defines important concepts and notation.
Those wishing to delve deeply into the topic can start with this paper:
Mixins in Strongtalk.
In a language supporting classes and inheritance,
a class implicitly defines a mixin.
The mixin is usually implicit—it is defined by the class body,
and constitutes the delta between the class and its superclass.
The class is in fact a mixin application—the
result of applying its implicitly defined mixin to its superclass.
The term mixin application comes from
a close analogy with function application.
Mathematically, a mixin M can be seen as a function
from superclass to subclass:
feed M a superclass S, and a new subclass of S is returned.
This is often written as M |> S in the research literature.
Based on the notion of function application,
one can define function composition.
The concept carries through to mixin composition;
we define the composition of mixins
M1 and M2,
written M1 * M2, as:
(M1 * M2) |> S =
M1 |> (M2 |> S).
Functions are useful because
they can be applied to different arguments.
Likewise mixins.
The mixin implicitly defined by a class
is usually applied only once,
to the superclass given in the class declaration.
To allow mixins to be applied to different superclasses,
we need to be able to either
declare mixins independently of any particular superclass,
or alternately,
to extricate the implicit mixin of a class
and reuse it outside its original declaration.
That is what we propose to do below.
Syntax and semantics
Mixins are implicitly defined via ordinary class declarations.
In principle, every class defines a mixin that can be extracted from it.
However, in this proposal,
a mixin may only be extracted from a class
that obeys the following restrictions:
- The class has no declared constructors.
- The class’ superclass is Object.
- The class contains no
super calls.
Restriction (1) avoids complications that arise
due to the need to pass constructor parameters up the inheritance chain.
Under those circumstances,
restriction (2) encourages mixins to be declared explicitly.
Restriction (3) means that implementations can continue
to statically bind super calls rather than either
rebinding them upon mixin application,
or binding them dynamically.
Example 1:
abstract class Collection<E> {
Collection<E> newInstance();
Collection<E> map((f) {
var result = newInstance();
forEach((E e) { result.add(f(e)); });
return result;
}
}
typedef DOMElementList<E> = abstract DOMList with Collection<E>;
typedef DOMElementSet<E> = abstract DOMSet with Collection<E>;
// ... 28 more variants
Here, Collection<E> is a normal class that is used to declare a mixin.
Both the classes DOMElementList and DOMElementSet are mixin applications.
They are defined by the typedef declaration that gives them a name
and declares them equal to an application of
a mixin to a superclass, given via a with clause.
The class is abstract
because it does not implement the abstract method
newInstance() declared in Collection.
In the above, DOMElementList is effectively
Collection mixin |> DOMList,
while DOMElementSet is Collection mixin |> DOMSet.
The benefit here is that the code in class Collection
can be shared in multiple class hierarchies.
We list two such hierarchies above—one rooted in DOMList
and one rooted in DOMSet.
One need not repeat/copy the code in Collection,
and every change made to Collection will propagate to both hierarchies,
greatly easing maintenance of the code.
This particular example is loosely based on a
real and very acute case in the Dart libraries.
The above examples illustrate one form of mixin application,
where the mixin application specifies a mixin and a superclass
to which it applies,
and provides the application with a name.
In an alternative form, mixin applications appear in
the with clause of a class declaration
as a comma-separated list of identifiers.
All the identifiers must denote classes.
In this form, multiple mixins are composed and applied
to the superclass named in the extends clause,
producing an anonymous superclass.
Taking the same examples again, we would have:
class DOMElementList<E> extends DOMList with Collection<E> {
DOMElementList<E> newInstance() => new DOMElementList<E>();
}
class DOMElementSet<E> extends DOMSet with Collection<E> {
DOMElementSet<E> newInstance() => new DOMElementSet<E>();
}
Here, DOMElementList is not the application Collection mixin |> DOMList.
Instead, it is a new class whose superclass is such an application.
The situation with respect to DOMElementSet is analogous.
Note that in each case,
the abstract method newInstance() is overridden with an implementation,
so these classes can be instantiated directly.
Consider what happens if DOMList has a non-trivial constructor:
class DOMElementList<E> extends DOMList with Collection<E> {
DOMElementList<E> newInstance() => new DOMElementList<E>(0);
DOMElementList(size): super(size);
}
Each mixin has its own constructor called independently,
and so does the superclass.
Since a mixin constructor cannot be declared,
the call to it can be elided in the syntax;
in the underlying implementation,
the call can always be placed at the start of the initialization list.
The constructor would set the values for any fields
and for the generic type parameters.
This rule ensures that these examples run smoothly
and also generalize cleanly once one lifts restriction (1).
The second form is a convenient sugar
that allows multiple mixins to be mixed into a class
without the need to introduce multiple intermediate declarations.
For example:
class Person {
String name;
Person(this.name);
}
class Maestro extends Person with Musical, Aggressive, Demented {
Maestro(name):super(name);
}
Here, the superclass is the mixin application:
Demented mixin |> Aggressive mixin |> Musical mixin |> Person
We assume that only Person has a constructor with arguments.
Hence Musical mixin |> Person inherits Person’s constructors,
and so on until the actual superclass of Maestro,
which is formed by a series of mixin applications.
In reality in this example we’d expect that
Demented, Aggressive, and Musical
actually have interesting properties that are likely to require state.
Possible issues
Having illustrated the proposal,
we now discuss several areas where we can anticipate issues:
Privacy
A mixin application may well be declared outside the library
that declared the original class.
This should not have any effect on
who can access members of a mixin application instance.
Access to members is determined based on
the library where they were originally declared,
exactly as with ordinary inheritance.
Strictly speaking, I need not even bring this up,
as it follows from the semantics of mixin application,
which are determined by the semantics of inheritance
in the underlying language.
Statics
Can one use the statics of the original class
via the mixin application or not?
Again, the answer (No) follows from the semantics of inheritance.
Statics are not inherited in Dart.
Types
What is the type of a mixin application instance?
In general, it is a subtype of its superclass,
and also supports the methods defined on the mixin.
The mixin name itself, however,
denotes the type of the original class,
which has its own superclass
and may not be compatible with a particular mixin application.
What about the interfaces a class supports?
Does its mixin support them?
In general, no, since interface support
may rely on inherited functionality.
This implies that a mixin application must declare
what interfaces it implements explicitly.
We may safely ignore this issue for the time being.
Because of restriction (2),
the type of a mixin does not include
additional members beyond those
declared by the mixin or shared by all objects.
Even if the mixin implements interfaces,
the mixin itself must implement
the methods of the interfaces,
and so it is safe to assume that anyone mixing in the mixin
is a subtype of the full type denoted by the mixin’s name.
However, when restriction (2) is lifted, the problem will arise.
This would argue for defining mixins as distinct constructs,
so that the mixin name would denote a stable type.
However, this requires pre-planning.
Instead, we might choose to denote
the type of the mixin of a class C
by a special type expression.
Generics are also an issue.
If a class has type parameters,
its mixin necessarily has identical type parameters.
Extensions
A key question is whether this proposal can be
cleanly extended when we relax its restrictions.
The implications of lifting restriction (2) have already been discussed.
Restriction (3) requires more sophistication in the implementation:
super calls must appear to bind dynamically to the actual superclass.
This can be achieved either by copying methods that use super,
or by making super calls late bound.
See section 6 of the paper
Mixins in Strongtalk
for a discussion of relevant implementation techniques.
Restriction (1) is more complex.
Because of Dart syntax,
there is no way to pass the constructor arguments
up the inheritance chain as part of the with clause
(as there is in, say, Scala).
A comprehensive approach to addressing the issue
is illustrated below via a variation on our mad maestro example.
class Musical {
final Instrument instrument;
Musical(this.instrument);
}
class Aggressive {
final String aggressionLevel;
Aggressive(this.aggressionLevel);
}
class Demented {
final disorder;
Demented(this.disorder);
}
class Maestro extends Person with Musical, Aggressive, Demented {
Maestro(name, disorder, degree, instrument) :
Demented(disorder), Aggressive(degree), Musical(instrument), super(name);
}
The constructor for Maestro explicitly channels the various parameters
to the various mixins that are used to define its superclasses.
The rules given in the restricted proposal still apply:
each mixin has its constructor called independently,
as does the superclass.
Only the part of the constructor
that operates on the mixin itself is called.
If the mixin had a superclass,
that superconstructor is not run.
If calls to mixin constructors are absent,
a default call of the form M(),
where M is the name of the mixin,
should be inserted by the implementation.
This will ensure calling of its default constructor if it exists.
Of course, these calls may be optimized away
if the mixin has no fields or constructors.
Hence, both the behavior and performance
of the restricted proposal are preserved.
Spec changes
The rest of this document describes and shows
how mixins change the language specification.
Changes to existing sections are
highlighted in yellow.
7. Classes
classDefinition:
metadata abstract? class identifier typeParameters? (superclass mixins?)? interfaces?
‘{‘ (metadata classMemberDefinition)* ‘}’
;
mixins:
with typeList
;
…
An abstract class is a class that is explicitly declared with the
abstract modifier,
either by means of a class declaration or via a
type alias for a mixin application.
…
7.9 Superclasses
The superclass of a class C
that has a with clause with M1, …,
Mk and an extends clause extends S
is the application of Mk * .. *
M1 to S.
If no with clause is specified then
the extends clause
of a class C specifies its superclass.
If no extends clause is specified, then either:
- C is Object, which has no superclass. OR
- Class C is deemed to have
an extends clause of the form extends Object,
and the rules above apply.
It is a compile-time error to specify an extends clause for class Object.
superclass:
extends type
;
It is a compile-time error if the extends clause of a class C
includes a type expression that does not denote a class available
in the lexical scope of C.
A class S is a superclass of a class C iff either:
- S is the superclass of C, or
- S is a superclass of a class S’ and S’ is a superclass of C.
It is a compile-time error if a class C is a superclass of itself.
9. Mixins
Caveat 1: Mixins are not implemented at this time.
Caveat 2: This section of the spec is work in progress.
A mixin describes the difference between a class and its superclass. A mixin may
be declared directly or derived from an existing class declaration.
It is a compile-time error if a declared or derived mixin refers to super.
It is a compile-time error if a declared or derived mixin explicitly declares a
constructor. It is a compile-time error if a mixin is derived from a class whose
superclass is not Object.
These restrictions are temporary.
We expect to remove them in later versions of Dart.
The restriction on the use of super
avoids the problem of rebinding
super when the mixin is bound to difference superclasses.
The restriction on constructors simplifies the construction of mixin
applications because the process of creating instances is simpler.
The restriction on the superclass means that the type of a class
from which a mixin is derived is always implemented
by any class that mixes it in.
This allows us to defer the question of whether and how
to express the type of the mixin
independently of its superclass and superinterface types.
Reasonable answers exist for all these issues,
but their implementation is non-trivial.
9.1 Mixin Application
A mixin may be applied to a superclass,
yielding a new class.
Mixin application may occur when a mixin is
mixed into a class declaration via its with clause,
or it may occur in the context of a type alias.
mixinApplication:
qualified_mixins interfaces?’;’
;
A mixin application of the form S with M
defines a class C with superclass S.
A mixin application of the form
S with M1, …, Mk
defines a class C whose superclass is
the application of the mixin composition
Mk * … * M1 to S.
In both cases above, C declares the same instance members as M.
If any of the instance fields of M have initializers,
they are executed in the scope of M
to initialize the corresponding fields of C.
The class C has an implicitly declared nullary generative constructor
with no initializer list and no body.
It is a compile-time error if S does not denote a class
available in the immediately enclosing scope.
It is a compile-time error if M
(respectively, any of M1, …, Mk)
does (respectively, do) not denote
a class or mixin available in the immediately enclosing scope.
It is a compile-time error if a well-formed mixin
cannot be derived from M
(respectively, from each of M1, …, Mk).
Let K be a class declaration with the same superclass and interfaces as C,
and the instance members declared by M
(respectively M1, …, Mk).
It is a static warning if the declaration of K
would cause a static warning.
It is a compile-time error if the declaration of K
would cause a compile-time error.
9.2 Mixin Composition
Dart does not directly support mixin composition, but the concept is useful
when defining how the superclass of a class with a mixin clause is created.
The composition of two mixins,
M1<T1 … TkM1> and
M2<U1 … UkM2>,
written M1<T1 … TkM1>
* M2<U1 … UkM2>,
defines an anonymous mixin such that for any class
S<V1 … VkS>,
the application of
M1<T1 … TkM1> to
S<V1 … VkS>
is equivalent to
typedef Id1<T1 …
TkM1, U1 … UkM2, V1 … VkS> =
abstract Id2<U1 … UkM2, V1 … VkS> with
M1 <T1 … TkM1>;
where Id2 denotes
typedef Id2<U1 … UkM2,
V1 … VkS> =
abstract S<V1 … VkS> with
M2<U1 … UkM2>;
and Id1 and Id2 are unique identifiers
that do not exist anywhere in the program.
The classes produced by mixin composition are regarded as abstract
because they cannot be instantiated independently.
They are only introduced as anonymous superclasses
of ordinary class declarations and mixin applications.
Consequently, no warning is given if a mixin composition
includes abstract members, or incompletely implements an interface.
Mixin composition is associative.
14. Libraries and Scripts
A Dart program consists of one or more libraries,
and may be built out of one or more compilation units.
A compilation unit may be a library or a part.
A library consists of (a possibly empty) set of imports, and a set of top
level declarations. A top level declaration is either a class,
a type alias declaration,
a function,
or a variable declaration.
topLevelDefinition:
classDefinition
| typeAlias
| external functionSignature
| external getterSignature
| external setterSignature
| functionSignature functionBody
| returnType? getOrSet identifier formalParameterList functionBody
| (final | const) type? staticFinalDeclarationList ‘;’
| variableDeclaration ‘;’
| ;
15.3.1 Typedef
A type alias declares a name for a type expression
or mixin application.
typeAlias:
metadata typedef typeAliasBody
;
typeAliasBody:
identifier typeParameters? `=’ abstract? mixinApplication
| functionTypeAlias
;
functionTypeAlias:
functionPrefix typeParameters? formalParameterList ‘;’
;
functionPrefix:
returnType? identifier
;
The effect of a type alias of the form
typedef T id (T1 p1, …,
Tn pn,
[Tn+1 pn+1, …, Tn+k pn+k])
declared in a library L
is to introduce the name id into the scope of L,
bound to the function type (T1, …, Tn,
[Tn+1 pn+1, …, Tn+k pn+k]) → T.
The effect of a type alias of the form
typedef T id (T1 p1, …,
Tn pn,
{Tn+1 pn+1,
…, Tn+k pn+k})
declared in a library L is to introduce the name id
into the scope of L,
bound to the function type (T1, …, Tn,
{Tn+1 pn+1, …, Tn+k pn+k]}) → T.
In either case, if no return type is specified,
it is taken to be dynamic.
Likewise, if a type annotation is omitted on a formal parameter,
it is taken to be dynamic.
The effect of a type alias of the form typedef C = M; or the form
typedef C<T1, …, Tn> = M; is to introduce the name C into the
scope of L, bound to the class defined by the mixin application M. The name
of the class is also set to C. Iff the type alias body includes the built-in
identifier abstract, the class being defined is an abstract class.
It is a compile-time error if any default values are specified in the signature
of a function type alias. It is a compile-time error if a typedef refers to
itself via a chain of references that does not include a class type.
15.4 Interface Types
The implicit interface of class I is a direct supertype of the implicit
interface of class J iff:
- I is Object, and J has no extends clause.
- I is listed in the extends clause of J.
- I is listed in the implements clause of J.
- I is listed in the with clause of J.
- J is a mixin application of the mixin of I.
16.1.1 Reserved Words
A reserved word may not be used as an identifier; it is a compile-time error
if a reserved word is used where an identifier is expected.
assert, break, case, catch, class, const, continue, default, do, else,
extends, false, final, finally, for, if, in, is, new, null, return, super,
switch, this, throw, true, try, var, void, while,
with.