Friday, August 17, 2012

Dart's modest proposal for Mixins

Posted by Gilad Bracha

(Editor's note: mixins is a work in progress, and is not slated for the M1 release. You can track the bug for this feature, and star it to be notified of changes. Please direct discussion to the Dart mailing list.)


Mixins in Dart

This document a minimalist proposal for introducing mixins in Dart. It is deliberately restricted in several ways, so as to minimize disruption to our existing implementations, while allowing future evolution toward a full fledged mixin implementation. The hope is that this restricted version already provides considerable value.

The intent is that something very much like this would be incorporated into Dart at some time after M1.

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.


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.

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.

A Modest Proposal


We propose that mixins be 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:

  1. The class has no fields.
  2. The class has no declared constructors.
  3. The class’ superclass is Object.
  4. The class contains no super calls.


Restrictions (1) & (2) avoid complications that arise due to the need to pass constructor parameters up the inheritance chain.  Under those circumstances, restriction (3) encourages mixins to be declared explicitly. Restriction (4) means that implementations can continue to statically bind super calls rather than either rebinding them upon mixin application, or binding them dynamically.


Example:

class Collection<E> {
  abstract Collection<E> newInstance();
  Collection<E> map(f) {
    result = newInstance();
    this.forEach((E e){result.add(f(e));})
    return result;
 }
}

class DOMElementList<E> mixin Collection<E> extends DOMList {
  DOMElementList<E> newInstance() => new DOMElementList();
  ...
}

class DOMElementSet<E> mixin Collection<E> extends DOMSet {
  DOMElementSet<E> newInstance() => new DOMElementSet();
  ...
}


… 28 more variants


Here, Collection<E> is a normal class that is used to declare a mixin.  Both the classes DOMElementList and DOMElementSet inherit from mixin applications.

In the above, the superclass of DOMElementList is Collection mixin |> DOMList, while the superclass of DOMElementList 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.

How do the above examples work if DOMList or DOMSet have non-trivial constructors?

class DOMElementList<E> mixin Collection<E> extends DOMList {
  DOMElementList<E> newInstance() => new DOMElementList(0);
  DOMElementList(int size): super(size); // super size me
}


This looks good, but notice that:

  1. The superconstructor call invokes a non-trivial superconstructor.
  2. Collection has no non-trivial constructor.
  3. Constructors are not inherited.
  4. The superclass is the mixin application Collection |> DOMList, which by the above, has no non-trivial constructor, and so the code would appear to be erroneous.


To deal with this problem, I propose that each element of the mixin list has its constructor called independently. Each mixin has its own constructor called, and so does the superclass.  Since a mixin constructor does nothing at all, the call to it can be elided in both the syntax and the underlying implementation.  

Note however, that the generic type arguments must be set somehow. Usually, this would be set up by the constructors.

This rule ensures that these examples run smoothly and also generalize cleanly once one lifts restrictions (1) & (2).


A second example illustrates the limitations of the proposal, Suppose we want to create a Person who is Demented, Aggressive and Musical.

class Person {
 String name;
 Person(this.name);
}

class Maestro  mixin Demented, Aggressive, Musical extends Person {
 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.

However, in reality in this example we’d expect that Demented, Aggressive and Musical actually have interesting properties that are likely to require state. If they don’t, they are liable to be simply marker interfaces, in which case we did not need mixins at all.

Later on we will discuss how the restrictions might be lifted to accommodate such cases.

There remain many details to attend to.  This proposal is not yet at the stage of a full specification, but we can anticipate issues in the following areas:

  • Privacy
  • Statics
  • Types

these are discussed in the next few sections.

Privacy


A mixin application may well be declared outside the library that declared the original class. Should this have any effect on who can access members of a mixin application instance? I would hope not. Access to members would be 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 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.

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 such as C.mixin.  Since this precludes the use of the word mixin as a member, I suggest instead that we provide a special notation such as C mixin.

Examples: x is C mixin or List<C mixin>.

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.

Generics are also an issue. If a class has type parameters, its mixin necessarily has identical type parameters. The first example illustrated this point and it raises no syntactic issues, but it does raise constructor-related issues for the implementation, even under our current restrictions.



Extensions


A key question is whether this proposal can be cleanly extended when we relax its restrictions. Restriction (3) by itself is of minor significance and can be dropped easily.  Restriction (4) requires more sophistication in the implementation: super calls must appear to bind dynamically to the actual superclass.  

Restrictions (1) and (2) are more complex.  An mixin clause with multiple identifiers is analogous to Scala’s with clause, which has no such restrictions. However, because of Dart syntax, there is no way to pass the constructor arguments up the inheritance chain.

One approach to addressing the issue is  illustrated below via an 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  mixin  Demented, Aggressive, Musical extends Person {
 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 nulling out any of the mixin’s fields and 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.

Finally


There is one aspect of this proposal that is rather irregular:  mixin applications are defined via the mixin clause and always result in anonymous superclasses.

We could introduce a second form of mixin application written as a special kind of class declaration. It has its own name, type parameters, superclass and superinterfaces just like a normal class. Instead of a class body, it specifies a class whose mixin is used, followed by a comma- separated list of constructors, terminated with a semicolon.  

That form can be used to define a class that specifies constructors with appropriate plumbing - providing both the mixin and the superclass with the desired parameters.  We could support this in a simple form with a single class specifying the mixin.

Originally, the constructors were given inside curly braces, but this confused people who thought it was a regular class body.

This syntax closely matches the semantic structure. A mixin application creates a new class, and so is written very much like a class declaration, including its type parameters (if any) extends and implements clauses. The difference is only in the mixin of the new class. In a normal class the class body implicitly defines the class’ mixin. In a mixin application, the class body is replaced by an alternative way of specifying a mixin. The new syntax specifies the class whose mixin will be applied in this case. If necessary, its specifies what type arguments should be passed to the mixin. Optionally, a list of constructors for the mixin application is given.

Here is a variant of the malign musician example that illustrates the above

class Musician  mixin Musical extends Person:
 Musician(name, instrument): Musical(instrument), super(name) ;

class ViolentViolinist extends Musician mixin Aggressive:
 ViolentViolinist(name, instrument): Aggressive(‘very’), super(name, new Violin());


It is not yet clear that this additional form is useful enough to be justified.