Enhancing Mixins with Decorator Functions

Last time, in "Real" Mixins with JavaScript Classes, we saw how we can create powerful ES6 mixins by using class expressions as subclass factories.

Now, let's look at some enhancements that make our mixins more powerful, in a way still requires no framework for mixin users, and is still easy to use for mixin authors.

First, we'll look at how subclass-factory-style mixins can be wrapped, or decorated, to add aditional features. Then we'll enable the caching of mixin applications, so that the same mixin definition applied to the same superclass multiple times reuses the same mixin application, and implement ES2015's @@hasInstance[1] method so that instanceof works with mixins!

The Technique and Some Groundwork

We're going to add these enhancements at the mixin definition site, so that only the mixin author needs to do anything to get them.

Recall a mixin definition from the last post:

const MyMixin = (superclass) => class extends superclass {
  /* ... */
};

Our new enhancements are built as functions that wrap the mixin function. Mixin declaration will now look like this:

const MyMixin = BaseMixin((superclass) => class extends superclass {
  /* ... */
});

BaseMixin wraps the author's subclass factory in a function that does a little extra work before calling the factory. This is very much like ES.next decorators, just without the special @BaseMixin syntax, so I'll call them decorator functions.

All kinds of behaviors can be added to mixins by adding more wrappers. Since we're going to implement caching and @@hasInstance support, our mixin definitions might end up looking like this:

const MyMixin = Cached(HasInstance(BaseMixin((superclass) => class extends superclass {
  /* ... */
})));

This should be fairly easy for authors to use, and mixin users don't need to do anything different. Mixin application still just works as function invocation:

class A extends MyMixin(MyBaseClass) {}

class B extends MyMixin(MyBaseClass) {}

Or we can use mix().with():

class A extends mix(MyBaseClass).with(MyMixin) {}

class B extends mix(MyBaseClass).with(MyMixin) {}

mix().with() is still just plain and optional sugar around function invocation.

If we implement Cached and HasInstance correctly, the mixins and classes suddenly have new powers:

let a = new A();
let b = new B();

a instanceof A; // true
b instanceof B; // true
Object.getPrototypeOf(a) === Object.prototypeOf(b); // true

We can use standard function composition to create a convenience decorator for the common behaviors:

const Mixin = (mixin) => Cached(HasInstance(BaseMixin(mixin)));

const MyMixin = Mixin((superclass) => class extends superclass {
  /* ... */
});

Wrapping with Care

We have to be a little more careful with wrapping mixin functions than usual because we want the final result of calling the decorators to be an object with certain properties, like @@hasInstance. We don't know what decorators might be applied to our mixin, and we'd like to avoid requiring a specific ordering of decorators, so we don't want to obscure properties added by one wrapper with a subsequent wrapper.

To solve this we'll create a wrap utility that sets the prototype of the wrappers, and stores a property pointing to the original mixin function so that we have a canonical identity for a mixin:

const _originalMixin = Symbol('_originalMixin');

const wrap = (mixin, wrapper) => {
  Object.setPrototypeOf(wrapper, mixin);
  if (!mixin[_originalMixin]) {
    mixin[_originalMixin] = mixin;
  }
  return wrapper;
};

This makes our function wrapping behave a lot more like inheritance.

Next, we'll create a base mixin decorator that applies the mixin and stores a reference from the mixin application back to the mixin definition for later use in caching:

const BaseMixin = (mixin) => wrap(mixin, (superclass) => {
  let application = mixin(superclass);
  application.prototype[_mixinRef] = mixin[_originalMixin];
  return application;
});

Notice the call to wrap. This sets things up so that other mixin decorators can add properties to the mixin and have them be visible after wrapping.

Caching Mixin Applications

The whole purpose of mixins is to apply them to different superclasses so that using a mixin doesn't require a specific inheritance hierarchy. However, mixins may still be repeatedly applied to the same superclass.

Consider two classes, B and C which both extend A and mixin M1 and M2:

class B extends mix(A).with(M1, M2) {}

class C extends mix(A).with(M1, M2) {}

The simplest mixin implementation will produce two identical A-with-M1 and two identical A-with-M1-with-M2 prototypes.

Instead we would like to share identical applications:

We can do this by caching mixin applications on classes:

const _mixinRef = Symbol('_mixinRef');

const Cached = (mixin) => wrap(mixin, (superclass) => {
  // Create a symbol used to reference a cached application from a superclass
  let applicationRef = mixin[_cachedApplicationRef];
  if (!applicationRef) {
    applicationRef = mixin[_cachedApplicationRef] = Symbol(mixin.name);
  }

  // Look up an cached application of `mixin` to `superclass`
  if (superclass.hasOwnProperty(applicationRef)) {
    return superclass[applicationRef];
  }

  // Apply the mixin
  let application = mixin(superclass);

  // Cache the mixin application on the superclass
  superclass[applicationRef] = application;

  return application;
});

Hopefully the comments are easy to follow. The approach is to create a unique symbol per mixin, then store mixin applications in a property keyed by that symbol on the superclass of a mixin application. That allows us to look up and reuse previous applications of a mixin to a superclass.

If this seems like overkill, consider cases where you must extend a certain class, yet libraries would like to offer help in customizing those subclasses.

I'm thinking of custom elements here, and specifically, libraries like Polymer. Custom elements need to extend one of the built-in elements - at the very least HTMLElement - but Polymer does a lot of work by being on the prototype chain. Mixins make this easy to express:

class MyElement extends mix(HTMLElement).with(Polymer) {
  // ...
}

But with naive mixins a new identical superclass of HTMLElement-With-Polymer is created for every custom element class, wasting time and memory. With mixin application caching this can be easy and efficient.

Adding instanceof Support

ES2015 has support for overriding the instanceof operator via the @@hasInstance method, which should be implemented by objects that appear of the righthand side of an instanceof operator[2].

Ideally an expression like o instanceof MyMixin would work as expected: if o is an instance of a class that has mixed in MyMixin, the expression should return true.

With mixins, the "type" is one of our subclass factory functions, so we need to patch the @@hasInstance method into the mixin. The prototype setting we did in the groundwork section will ensure that this method is available even after subsequent wrapping.

Out patch looks like this:

const HasInstance = (mixin) => {
  if (!Symbol.hasInstance) {
    return mixin;
  }
  mixin[Symbol.hasInstance] = function(o) {
    const originalMixin = this[_originalMixin];
    while (o != null) {
      if (o.hasOwnProperty(_mixinRef) && o[_mixinRef] === originalMixin) {
        return true;
      }
      o = Object.getPrototypeOf(o);
    }
    return false;
  }
  return mixin;
};

I can't find that this is actually implemented in any production VM yet, but it looks like Webkit has initial support, along with Babel under a flag.

Writing Your Own Mixin Decorators

mixwith.js includes the wrap utility and some common symbols that help with writing mixin decorators. You can use these to write decorators that play well with each other. What kind of decorators might you want to write? Maybe a de-duplication, so that a mixin applied twice to a prototype chain only appears once in the chain, or a traits-like system that disallows overriding.

There are probably two main types of mixin decorators: those that need to wrap the mixin function, and those that just need to patch it.

If you need to wrap the mixin function, make sure you call wrap and usually you want to invoke the mixin function. Sometimes you may want to conditionally invoke the mixin, like with de-duplication:

import { wrap } from 'mixwith';

const DeDupe = (mixin) => wrap(mixin, (superclass) => {
  if (mixinAlreadyApplied(superclass)) {
    return superclass;
  }
  return mixin(superclass);
});

If you only need to patch the mixin function, just patch and return it:

const Fooify = (mixin) => {
  mixin.foo = "Foo";
  return mixin;
};

Follow the conversation

Footnotes


  1. @@foo is shorthand for Symbol.foo in the JavaScript specification ↩︎

  2. This is fabulous for extensibility, since new types can accept an instance without having to modify the instance. You can implement any kind of type checking you want, say structural typing. It's terrible for static type checking, however. ↩︎

Show Comments