23
Dec 2013
On CoffeeScript Mixins
Tags |
On Computer Technology
CoffeeScript offers some rather nice improvements over JavaScript. One feature which I personally find very useful is the inclusion of classes. While JavaScript’s prototypal inheritance is powerful, I was exposed to more traditional OO languages prior to JavaScript, and I find that classes are a more intuitive way to organize code.
Chapter 3 of the Little Book on CoffeeScript offers a quick and dirty overview of classes in CoffeeScript. But that is not our main topic of the evening. Instead, we shall take a little look at Mixins, also explained in the same chapter.
Personally, I am rather new to the concept of Mixins. It was only through some Ruby work that I was first exposed to this idea. In short, Mixins are a way to mimick multiple inheritance. Now, before an angry mob of programmers kill me, I think we all agree that in general, multiple inheritance is a bad idea, but it does have its advantages. Mixins are a way of getting the best out of both worlds, gaining the power of multiple inheritance while keeping class hierarchies as a tree.
To understand Mixins, let us take a look at a rather contrived example in Ruby.
Suppose we have a ComputerMonitor
class, which is a subclass of the Display
class. However, we also want it to have the functionality of the BlackObject
class (yea my monitors usually have black frames; I know this example
is really contrived, but bear with me for a moment). How we will do it in
Ruby is, instead of having BlackObject
as a class, we organize it as a
Module. So the ComputerMonitor
class will extend the Display
class, and include the BlackObject
module, like this:
class ComputerMonitor < Display
include BlackObject
We can immediately see several advantages of this approach, which makes use of Mixins:
And while I’m pretty new to this, I think my last point should be particularly true for Mixins. The code should be sufficiently generic to permit reuse across several classes, otherwise, we might as well put the code inside the class making use of it in the first place.
Since the above example is very contrived, here’s a more real world example.
In Ruby, the Array
and Hash
classes are blueprints for the very
indispensable data structures. When we do an ri Array
, we see the following:
and when we do an ri Hash
, we see the following:
See any similarities? They both include Enumberable
, hence they share a lot
of methods. An ri Enumerable
confirms that yes, Enumerable
is a Mixin:
So I hope that is sufficiently real world, and an ok introduction to Mixins. Now, onto how they are implemented in CoffeeScript.
By default, CoffeeScript does not have Mixins as a built-in language feature. However, it is possible to implement them in CoffeeScript, as the Mixins section of Chapter 3 of the Little Book on CoffeeScript shows us.
That said, the full-fledged code for enabling Mixins in CoffeeScript is at the Extending classes section, which is right after the Mixins section. Here is the code:
moduleKeywords = ['extended', 'included']
class Module
@extend: (obj) ->
for key, value of obj when key not in moduleKeywords
@[key] = value
obj.extended?.apply(@)
this
@include: (obj) ->
for key, value of obj when key not in moduleKeywords
# Assign properties to the prototype
@::[key] = value
obj.included?.apply(@)
this
We will use include
for “instance level” Mixins, in other words, to mix in
methods or properties we want to appear in every instantiated object
of the class and its descendents.
We will use extend
for “class level” Mixins, which adds the methods or
properties of the given object onto the class itself, which makes this
kind of like static methods/attributes in C++ / Java.
The book provides an example usage:
classProperties =
find: (id) ->
create: (attrs) ->
instanceProperties =
save: ->
class User extends Module
@extend classProperties
@include instanceProperties
# Usage:
user = User.find(1)
user = new User
user.save()
Here, we see that the User
class inherits from Module
. All the magic
enabling Mixins is in the Module
class, so having a class inheriting the
Module
class is a prerequisite to enabling Mixins.
Here, we see that the User
class @extends
the classProperties
object.
So the find
and create
methods are available on the User
class itself
(and descendent classes), but not User
objects.
The User
class @includes
the instanceProperties
object. As such, the
save
method is available on User
objects, and objects which are
instances of classes that descend from User
.
From here, we can see that, for a class to use Mixins:
Module
(or a class with similar functionality)@extend
or @include
, and pass in an object to those
functions. That object will contain the methods / properties we want to
mix in to the class. We can write the methods as if they belong to a class
(meaning that we can use the @
notation)Now then, I skipped some details that I wish to go back to.
Let’s take a look at the code for the Module
class again:
moduleKeywords = ['extended', 'included']
class Module
@extend: (obj) ->
for key, value of obj when key not in moduleKeywords
@[key] = value
obj.extended?.apply(@)
this
@include: (obj) ->
for key, value of obj when key not in moduleKeywords
# Assign properties to the prototype
@::[key] = value
obj.included?.apply(@)
this
When I first looked at this, several questions came into mind:
obj.extended?.apply(@)
lines doing?@[key] = value
for @extend
, but @::[key] = value
for
@include
?All these questions can be answered by looking at the last example given in Chapter 3 of the Little Book on CoffeeScript:
ORM =
find: (id) ->
create: (attrs) ->
extended: ->
@include
save: ->
class User extends Module
@extend ORM
Essentially, this example is doing exactly the same thing as the example
where we had the User
class @extend
the classProperties
object and
@include
the instanceProperties
object (so the User
class will possess
the find
and create
class methods, and the save
instance method).
But how can that be, they look so different. Yet it is truly the case.
Let’s go into a bit more detail.
Let’s start from the definition of the User
class:
class User extends Module
@extend ORM
This should probably be familiar to us by now. It will call the @extend
method of the Module
class, with the ORM
object passed in.
Let’s look at how @extend
is defined inside the Module
class:
@extend: (obj) ->
for key, value of obj when key not in moduleKeywords
@[key] = value
obj.extended?.apply(@)
this
our ORM
object has the find
, create
and extended
properties.
There is something subtle with the name of the function. By writing it as
@extend
instead of extend
, we are actually creating extend
as a
class method. Use of the js2coffee
tool confirms this.
The @extend
function is compiled to the following JavaScript:
Module.extend = function(obj) {
var key, value, _ref;
for (key in obj) {
value = obj[key];
if (__indexOf.call(moduleKeywords, key) < 0) {
this[key] = value;
}
}
if ((_ref = obj.extended) != null) {
_ref.apply(this);
}
return this;
};
Changing @extend
to extend
causes the same snippet of code to be compiled
to:
Module.prototype.extend = function(obj) {
var key, value, _ref;
for (key in obj) {
value = obj[key];
if (__indexOf.call(moduleKeywords, key) < 0) {
this[key] = value;
}
}
if ((_ref = obj.extended) != null) {
_ref.apply(this);
}
return this;
};
Notice how everything stays the same, except that Module.extend
becomes
Module.prototype.extend
. As such, since the function is defined as
@extend
, the @
refers to the Module class. This is why passing the
an object to @extend
will cause the object’s properties to be available
to the target class.
In a similar spirit, for @include
:
@include: (obj) ->
for key, value of obj when key not in moduleKeywords
# Assign properties to the prototype
@::[key] = value
obj.included?.apply(@)
this
the code is compiled to the following JavaScript:
Module.include = function(obj) {
var key, value, _ref;
for (key in obj) {
value = obj[key];
if (__indexOf.call(moduleKeywords, key) < 0) {
this.prototype[key] = value;
}
}
if ((_ref = obj.included) != null) {
_ref.apply(this);
}
return this;
};
Notice how @::[key] = value
is compiled to this.prototype[key] = value;
.
This is what makes the properties of the object available to the instances
of the target class.
Going back to @extend
:
@extend: (obj) ->
for key, value of obj when key not in moduleKeywords
@[key] = value
obj.extended?.apply(@)
this
Recall that the ORM
object we passed in to @extend
has 3 properties:
find
, create
and extended
. So what the code here is doing is, for
any key that is not in the moduleKeywords
variable, make them available
to the class.
It happens that moduleKeywords
is defined as follows:
moduleKeywords = ['extended', 'included']
and we see that extended
is in moduleKeywords
, so that property is
not added to the User
class.
We are done with the for
loop in the @extend
method, so now we are at
this intimidating line:
obj.extended?.apply(@)
obj.extended?
checks if obj
has a property called extended
. If so,
then call its apply
method, with this
bounded to @
. Taking another
look at the ORM
object we passed in to @extend
:
ORM =
find: (id) ->
create: (attrs) ->
extended: ->
@include
save: ->
We see that the ORM
object has an extended
property, which happens to
be a function. In JavaScript (and hence CoffeeScript), Functions have the
apply
method (details here),
which takes in 1 argument, and simply calls the function with this
becoming
that supplied argument.
In other words, obj.extended?.apply(@)
will call the ORM.extended
function,
replacing this
with @
(here, @
refers to the User
class). Inside
ORM.extended
, we have an @include
function call, with the argument being
an object with the save
property. I think it should be clear what this is
doing - it is a roundabout way of @include
‘ing an object with the save
function to the User
class. So hopefully this paragraph in Chapter 3 of
The Little Book on CoffeeScript makes sense now, especially with regards to the
callbacks:
As you can see, we’ve added some static properties, find() and create() to the User class, as well as some instance properties, save(). Since we’ve got callbacks whenever modules are extended, we can shortcut the process of applying both static and instance properties:
Pretty neat, huh? I didn’t figure all that out by myself. All the credit goes to these 2 questions on Stack Overflow, and their answers:
This post is really just an organization of the wonderful chapter inside The Little Book of CoffeeScript, and knowledge gained from the 2 questions above. Hopefully there aren’t too many mistakes. Haha
Disclaimer: Opinions expressed on this blog are solely my own and do not express the views or opinions of my employer(s), past or present.