A class in MoonScript is declared with class. The new property is the
constructor, methods are written with the fat arrow =>, and @ refers to the
instance. Calling the class like a function creates an instance. Classes
compile to plain Lua tables and metatables, so they work anywhere Lua does and
can be used from Lua code.
MoonScript
|
Quick reference
| MoonScript | Meaning |
|---|---|
class Name | Declare a class, stored in a local named Name |
new: (a, b) => | The constructor |
new: (@name) => | Assign an argument directly to self.name |
method: => | A method; @ is the instance |
Name!, Name a, b | Create an instance |
obj\method! | Call a method |
@count: 0 | A class variable, stored on the class object |
@@count | Read a class variable from a method (self.__class.count) |
class B extends A | Inherit from A |
super ... | Call the parent's version of the current method |
obj.__class | The class an instance was made from |
What a class compiles to
A class is made of two tables. The base holds the methods and properties listed in the class body, and is the metatable of every instance. The class object holds the constructor, and calling it creates a new instance:
MoonScript
|
Lua
|
new becomes __init on the class object. Everything else in the body goes
in _base_0, which is stored on the class as __base. An instance is an
empty table with _base_0 as its metatable, so looking up p.length finds
the method in the base.
The class is assigned to a local variable with the class’s name. If the class is the last statement in a file, the file returns it, which is how a module usually exports a class.
Put instance fields in new
Every property in the class body is stored in the base, which all instances share. That is what you want for methods. For tables it means every instance reads and modifies the same table:
MoonScript
|
b never added anything, but it sees the sword because @items refers to
the one table in the base. Create each instance’s own state in the
constructor:
MoonScript
|
Numbers, strings, and booleans in the class body are safe to use as defaults.
Assigning @health -= 10 creates a field on the instance and leaves the base
alone. Only values that are modified in place, like tables, are a problem.
Class variables and private values
A property prefixed with @ in the class body is stored on the class object
instead of the base. Methods reach the class object with @@, which compiles
to self.__class:
MoonScript
|
@spawn is a class method: inside it, @ is the class, and @ x, y creates
an instance. Instances don’t see class variables directly, so e.count is
nil.
An assignment in the class body creates a local variable that only the class’s methods can see. Use it for private constants and helper functions:
MoonScript
|
Inheritance with extends and super
extends makes a class inherit the parent’s methods and properties. A
subclass without its own new uses the parent’s constructor:
MoonScript
|
Inside a method, calling super calls the parent’s method of the same name,
passing self automatically. A subclass that defines new must call super
itself, because the parent’s constructor no longer runs on its own:
MoonScript
|
Forgetting super in the constructor leaves the parent’s fields unset. The
error shows up later, wherever the missing field is used:
MoonScript
|
lua5.4: player.lua:62: attempt to perform arithmetic on a nil value (field 'x')
On Lua 5.1 and LuaJIT:
luajit: player.lua:62: attempt to perform arithmetic on field 'x' (a nil value)
A common pattern is to take the parent’s result and add to it:
MoonScript
|
super works the same way in class methods. Here @ is Player, so the
parent’s @create builds a Player:
MoonScript
|
Used without calling it, super is a value referring to the parent class.
super.method gets the parent’s version of a method as a plain function.
Reacting to subclasses with __inherited
If a class defines the class method @__inherited, it’s called each time
another class extends it, with the parent as @ and the new class as the
argument:
MoonScript
|
__inherited is itself inherited, so it also runs when a subclass is
extended. Its main use is copying metamethods, shown
below.
Sharing methods between classes
A class can only extend one parent. To share methods between unrelated classes, write them in a class of their own and copy its methods into each class’s base:
MoonScript
|
include @, Serializable runs in the class body, where @ is the class
being declared. Skipping names that start with __ leaves __index,
__class, and the target’s own metamethods alone.
A copied method keeps the super it was compiled with. If the mixin class
extends another class, super inside its methods calls that class, not the
parent of the class it was copied into.
Lapis provides @include for widgets and
applications, which mixes in another class’s methods the same way.
Inspecting classes and instances
Every class and instance carries a few fields that describe it:
obj.__classis the class an instance was created from.Name.__nameis the class’s name as a string.Name.__baseis the base table. Adding a function to it adds a method to every instance, including ones that already exist.Name.__parentis the parent class, if the class extends one.
To check what an instance is, compare its __class, or use switch to
branch on it:
MoonScript
|
These checks match the exact class only. A Player instance isn’t
== Entity, even though Player extends Entity.
Because methods live in __base, tests can replace a method for every
instance at once. With busted,
stub User.__base, "save" stubs the method, and reverting the stub restores
it.
The moon module that comes with MoonScript has functions for checking
types, including ones that follow the inheritance chain:
MoonScript
|
moon.type works like Lua’s type, but returns the class for an instance
and "class" for a class object.
Checking types with tableshape
tableshape is a separate library for
validating values, installed with luarocks install tableshape. Its
tableshape.moonscript module has type checkers for classes. Each one is a
function that returns true, or nil and an error message:
MoonScript
|
instance_of(cls)matches instances ofclsor any of its subclasses.subclass_of(cls)matches classes that extendcls, but notclsitself unless you passallow_same: trueas the second argument.- Both accept a class name as a string instead of the class object, which avoids requiring a module just to check against its class.
class_typeandinstance_typematch any class or any instance.
The checkers combine with the rest of tableshape, so you can validate a table of options that contains objects, such as a constructor’s argument:
MoonScript
|
The error names the field that failed:
lua5.4: team.lua:56: field "leader": table is not instance of Player
Metamethods
Properties with metamethod names like __tostring, __eq, __add, __lt,
and __call go in the base, which is the instances' metatable, so they work
as operators on instances:
MoonScript
|
Metamethods aren’t inherited
A subclass of Vec prints as table: 0x..., not (1, 2). Lua looks up
metamethods directly in an object’s metatable without following __index,
and the subclass’s base doesn’t contain __tostring itself. To pass
metamethods down, copy them into each subclass from __inherited:
MoonScript
|
The rawget check keeps anything the subclass defines itself, including its
own __index and __class.
Custom __index
An __index property in the class body has no effect, because the class
sets the base’s __index to the base itself so instances can find their
methods. To add a fallback for missing keys, replace it from the class body,
checking the base first:
MoonScript
|
Using a class from Lua
A compiled class is a table that Lua code calls to create instances, and
methods are called with : as usual:
local Player = require("player")
local p = Player("leafo")
p:greet()
To add a method from Lua, put it in Player.__base. Assigning a function to
Player itself only makes it a class-level value, invisible to instances.
Common mistakes
Using -> instead of =>
A function written with -> gets no self argument, so any use of @
inside it refers to a global self that doesn’t exist:
MoonScript
|
lua5.4: player.lua:8: attempt to index a nil value (global 'self')
On Lua 5.1 and LuaJIT:
luajit: player.lua:8: attempt to index global 'self' (a nil value)
Without @, the mistake is quieter. The instance arrives in the first
parameter, so name holds a table and every argument is off by one. Use =>
for new and every method. How to Call Methods
covers the difference in more detail.
Using @ in a property value
Property values in the class body are evaluated once, when the class is
created, and there’s no instance at that point. This fails with the same
global 'self' error as above:
MoonScript
|
Compute values that depend on other fields in new, or in a method:
MoonScript
|
Expecting :name to declare a field
In a class body, :name is the same shorthand as in a table literal. It means
name: name, and copies the value of a variable called name from the
surrounding scope:
MoonScript
|
There’s no syntax for declaring a field without a value. Assign fields in
new, or give them a default in the class body.
Calling methods with : or .
Methods are called with obj\method!. A Lua-style
obj:method! creates a table and calls a global function named method,
and obj.method!
calls the method without self. See
How to Call Methods for the error messages each one
produces.