An in-depth look into the MoonScript class implementation
MoonScript’s class system is great balance of functionality and brevity. It’s simple to get started with, doesn’t impose many restrictions, and is incredibly flexible when you need to do advanced things or bend the rules.
Even if you have no intention of using MoonScript, understanding the class system implementation is a good exercise for understanding some of the more complicated parts of Lua.
A simple example
Lets start with a typical class in MoonScript:
class Player
new: (@x, @y) =>
say_hello: =>
print "Greetings! I'm at #{@x}, #{@y}"
And take a look at the generated Lua: (Warning: there’s a lot going on, scroll past for analysis of each component)
local Player
do
local _base_0 = {
say_hello = function(self)
return print("Greetings! I'm at " .. tostring(self.x) .. ", " .. tostring(self.y))
end
}
_base_0.__index = _base_0
local _class_0 = setmetatable({
__init = function(self, x, y)
self.x, self.y = x, y
end,
__base = _base_0,
__name = "Player"
}, {
__index = _base_0,
__call = function(cls, ...)
local _self_0 = setmetatable({}, _base_0)
cls.__init(_self_0, ...)
return _self_0
end
})
_base_0.__class = _class_0
Player = _class_0
end
Lets go from the outside in. The result of the class expression is a new local
variable called Player
. Nothing else is made available on the calling scope.
The class’s internal objects are created inside of a Lua do end
block, this
ensures that they are scoped to just the class in question. The two internal
objects are _class_0
and _base_0
.
The resulting local, Player
is assigned _class_0
.
The numbers at the end of these variables are not fixed, they come from MoonScript’s local name generator. They will increment if you nest classes. You should never write code that depends on their names.
The class object
The class object, aka _class_0
in the generated code, is a Lua table that
represents the class. To create a new instance we call the class object as if
it were a function. We can see here that it’s not actually a function.
In order to make a Lua table callable it must implement the __call
metamethod.
Here’s the extracted class object’s creation:
local _class_0 = setmetatable({
__init = function(self, x, y)
self.x, self.y = x, y
end,
__base = _base_0,
__name = "Player"
}, {
__index = _base_0,
__call = function(cls, ...)
local _self_0 = setmetatable({}, _base_0)
cls.__init(_self_0, ...)
return _self_0
end
})
The Lua function setmetatable
sets the metatable of the first argument to the
second argument. It then returns the first argument. This means the value of
_class_0
is the modified version of the first table.
The table _class_0
is very basic. It has the constructor we created (with
new
) stored in __init
, the base object stored in __base
and the name of
the class stored in __name
.
Unlike the generated names, these names are unchanging and safe to use in your code. Because they are stored directly on the class object we can access them with dot syntax:
print(Player.__name) --> prints "Player"
Two metafields are provided on the class objects metatable: __index
and
__call
.
The __call
function is what is called when we create a new instance:
Player()
It’s responsible for creating a new table to be the instance,
providing it with a metatable, then calling the constructor.
You can can see how the _base_0
is used directly as the metatable of the
object.
Additionally, the class object has an __index
metafield set to the base. This
has a lot of implications. The most important is you can access any fields from
base directly on the class object, assuming they haven’t been shadowed by any
fields directly on the class object.
The base object
local _base_0 = {
say_hello = function(self)
return print("Greetings! I'm at " .. tostring(self.x) .. ", " .. tostring(self.y))
end
}
_base_0.__index = _base_0
_base_0.__class = _class_0
The base object, __base_0
is a regular Lua table. It holds all the instance
methods of the class. Our example from above implemented a say_hello
method
which is compiled directly into the base.
The base object has a circular reference to itself in the __index
field.
This lets us use the base object directly as the metatable of instances. The
__index
property is where instance methods are fetched from. Since it points
to itself, the instance methods can be pulled directly from the metatable
without any indirection.
Likewise, this also lets us implement other metamethods directly as instance methods of the class. I'll have an example below.
It’s a very cool concept, and definitely worth taking a moment to understand.
Lastly, a reference to the class placed on the base object with the name
__class
. This is how the @@
operator accesses the class object.
Classes with inheritance
Super invocation has changed a bit in MoonScript 0.4.0
Classes that inherit from other classes in MoonScript introduce a few more
ideas. The extends
keyword is used for inheritance:
class SizedPlayer extends Player
new: (@size, ...) =>
super ...
say_hello: =>
super!
print "I'm #{@size} tall"
Here’s the resulting Lua:
local SizedPlayer
do
local _parent_0 = Player
local _base_0 = {
say_hello = function(self)
_parent_0.say_hello(self)
return print("I'm " .. tostring(self.size) .. " tall")
end
}
_base_0.__index = _base_0
setmetatable(_base_0, _parent_0.__base)
local _class_0 = setmetatable({
__init = function(self, size, ...)
self.size = size
return _parent_0.__init(self, ...)
end,
__base = _base_0,
__name = "SizedPlayer",
__parent = _parent_0
}, {
__index = function(cls, name)
local val = rawget(_base_0, name)
if val == nil then
return _parent_0[name]
else
return val
end
end,
__call = function(cls, ...)
local _self_0 = setmetatable({}, _base_0)
cls.__init(_self_0, ...)
return _self_0
end
})
_base_0.__class = _class_0
if _parent_0.__inherited then
_parent_0.__inherited(_parent_0, _class_0)
end
SizedPlayer = _class_0
end
The majority of the generated code is the same as a regular class. Here are the differences:
local _parent_0 = Player
There’s a new local variable inside the do end
block called _parent_0
that
holds a reference to the parent class.
local _base_0 = {
-- ...
}
_base_0.__index = _base_0
setmetatable(_base_0, _parent_0.__base)
The metatable of the base is set to the base of the parent class. This
establishes the inheritance chain for instances. If a method can’t be found on
the class’s base, then the parent class’s base is automatically searched due
to how __index
works.
There’s a slight disadvantage to this. Metamethods are fetched with
rawget
, so metamethod inheritance does not work by default. We can work around this with the__inherited
callback discussed below.
local _class_0 = setmetatable({
-- ...
__parent = _parent_0
}, {
-- ...
}
The parent class is stored on the class object in a field called __parent
.
This gives you an easy way to reference the parent class object.
{
__index = function(cls, name)
local val = rawget(_base_0, name)
if val == nil then
return _parent_0[name]
else
return val
end
end,
-- ...
}
The __index
metafield on the class object is now a function, instead of a
reference to the base (which is a table). rawget
is used control the
precedence of the properties. If the field can’t be found directly on the base
then the parent class is searched.
Remember that class objects also pull fields from their bases, so this has the
effect of searching both the parent class object and the parent class’s base.
Even though we've used rawget
on the base, we can still get access to the
parent class’s base.
if _parent_0.__inherited then
_parent_0.__inherited(_parent_0, _class_0)
end
Lastly, we now have a class callback. When a subclass is created and the parent
class has a method __inherited
then it is called with the class object that
has just been created.
The
__inherited
method works directly with class objects, no instances are involved.
local _base_0 = {
say_hello = function(self)
_parent_0.say_hello(self)
return print("I'm " .. tostring(self.size) .. " tall")
end
}
In the example I included a method that calls super
. All MoonScript does is
provide sugar for calling the method of the same name on the parent class.
Class tips and tricks
Now that you have an understanding of how a class in MoonScript is implemented, it’s easy to see how we can work with the internals to accomplish new things.
Adding __tostring
and other metamethods
If you want your instances to have a string representation you can implement a
__tostring
method in the metatable.
As we saw above, the metatable has an __index
field set to itself, we just
need to implement metamethods as instance methods:
class Player
new: (@x, @y) =>
__tostring: =>
"Player(#{@x}, #{@y})"
print Player(2, 8) --> "Player(2, 8)"
All of Lua’s metamethods work (except __index
, see below). Here’s an example
of a vector class with overloaded operators:
class Vector
new: (@x, @y) =>
__tostring: =>
"Vector(#{@x}, #{@y})"
__add: (other) =>
Vector @x + other.x, @y + other.y
__sub: (other) =>
Vector @x - other.x, @y - other.y
__mul: (other) =>
if type(other) == "number"
-- scale
Vector @x * other, @y * other
else
-- dot product
Vector @x * other.x + @y * other.y
print Vector(1,2) * 5 + Vector(3,3) --> Vector(8, 13)
I mentioned above that metamethod inheritance does not work:
class Thing
__tostring: => "Thing"
class BetterThing extends Thing
print BetterThing! --> table: 0x1057290
We can work around this by using the __inherited
callback:
class Thing
__tostring: => "Thing"
__inherited: (cls) =>
cls.__base.__tostring = @__tostring
class BetterThing extends Thing
print BetterThing! --> Thing
Adding a new method to a class after declaration
Now that we know about __base
it’s easy to add new methods to classes that
don’t have them.
class Player
new: (@name) =>
-- add the new method
Player.__base.jump = =>
print "#{@name} is jumping!"
Player("Adam")\jump! --> Adam is jumping!
We can extend this concept even further to dynamically generate methods:
class Player
new: (@name) =>
for dir in *{"north", "west", "east", "south"}
@__base["go_#{dir}"]: =>
print "#{@name} is going #{dir}"
Player("Lee")\go_east! --> Lee is going east
Converting an existing table to an instance
Sometimes you might already have a table that you'd like to convert to an
instance of a class without having to copy it. Now that we know how the
__init
method works we can use setmetatable
to accomplish a similar result:
class Rect
area: => @w * @h
some_obj = { w: 15, h: 3 }
-- apply the metatable
setmetatable(some_obj, Rect.__base)
print some_obj\area! --> 45
This same method can be used to convert on object from type to another.
Adding __index
metafield to an instance
MoonScript uses the __index
metafield on class instances in order to allow
instance properties to be looked up. If we just replace __inde
with another
implementation without any consideration we would break the instance. We'll
have to chain our custom __index
with the old one.
Here’s how we might implement getter methods:
class Thing
getters: {
age: =>
os.time! - @created_at
}
new: =>
@created_at = os.time!
mt = getmetatable @
old_index = mt.__index
mt.__index = (name) =>
-- for completion we handle when index is function
if type(old_index) == "function"
if gs = old_index @, "getters"
if gs[name]
return gs[name] @
old_index @, name
else
if g = old_index.getters and old_index.getters[name]
g @
else
old_index[name]
t = Thing!
print t.age
Its’s important that you don’t try to access
self
(withoutrawget
) within the__index
metamethod, otherwise you'll cause an infinite loop.
Writing that massive implementation in the constructor isn’t ideal. Here’s a base class that automatically upgrades anyone who inherits with getter functionality:
class HasGetters
getters: {}
__inherited: (cls) =>
old_init = cls.__init
cls.__init = (...) =>
old_init @, ...
mt = getmetatable @
old_index = mt.__index
mt.__index = (name) =>
if getter = old_index.getters[name]
getter @
else
if type(old_index) == "function"
old_index @, name
else
old_index[name]
class BetterThing extends HasGetters
getters: {
age: =>
os.time! - @created_at
}
new: =>
@created_at = os.time!
t = BetterThing!
print t.age
The clever part here is replacing the __init
method on the base class with a
custom one that automatically injects support for getters.
Future improvements
The class system is far from perfect. Here are some future improvements that I'd like to add:
- There’s no way to determine which order methods are added to a class. If you're going to be triggering side effects from method creation then your options are limited.
- The MoonScript class meta-properties use double underscore just like Lua. If Lua ever decides to use any of the same names then there will be conflicts.
Closing
Not all of the functionality of MoonScript classes was covered in this guide. You can learn more on the Object Oriented Programming section of the MoonScript documentation.
Related projects
A programming language that compiles to Lua, inspired by CoffeeScript and written in MoonScript.