Objects in Lua
Defining a class
Making an instance
Defining methods
Initializers
Static fields
Inheritance
Access to superclasses
Notes
Objects in Lua
Lua is not an object-oriented language in itself, but it is flexible
enough to be used like one when desired. There are different approaches
to object-orientedness in Lua, usually implying metamethods.
The approach I used in gpeddler 2 is very basic and does not use
metamethods; it could be called "Cheap Lua Object System", but
unfortunately that acronym is already taken ;-)
I do not know if this approach has already been proposed (I confess I
do not read as much as I should do), in any case it is more than enough
for my simple needs. It uses a single helper function: Utils.ShallowCopy(), defined in utils.lua
Not being an OO worshipper, I only used objects where I considered them
to be useful in this program, maybe overdoing this a bit for the sake of experiment
(as in the MainFrame 'class'). The two
'classes' that may be worth a look are Route
in route.lua and especially Solver in solver.lua;
the latter demonstrates inheritance and polymorphism.
The rest of this page describes my approach to objects in Lua.
Defining a class
Lua being a dynamic language, there is no need for painstakingly
detailed declarations: an object is just a table including data
fields (data members) and function fields (methods). Furthermore, an
object is created by simply making a copy of a master object,
i.e. the 'class'.
For example, this is a class (a master object table):
--
-- class Square
--
Square = {
side = nil -- see note below
}
Note that the expression inside the table has no effect, it is
just there to document a field that will be defined later. So, in fact,
the simplest class is just an empty table, although it could contain
initializers and static fields, as shown later.
The 'class' table will also contain functions (methods), but
they will be added implicitly as they are defined (and, hopefully,
documented) in the source code, so there is no need to write anything
here.
Making an instance
An object is created by calling its constructor function
(method), conventionally named Create(),
passing all the required arguments:
sq = Square.Create(5)
Here is the constructor that produces instances of the class Square:
--
-- constructor for Square
--
function Square.Create(side)
-- first make a copy of the master object
local self = Utils.ShallowCopy(Square)
-- then set fields
self.side = side
return self
end
(note that side is the received
argument, while self.side it the object
data field)
The first line makes a copy of the master object table (i.e. of
the 'class' fields) into the local variable conventionally named self; it is a good idea to keep this name to
avoid confusion, as it will be used by Lua syntax in methods (as we'll
see shortly).
The Utils.ShallowCopy() function, defined
in utils.lua, if called with a single
argument (a table) just makes a first-level copy of the argument
and returns the copy. In this case the table is empty, so Utils.ShallowCopy() just returns a new empty
table.
The second line assigns a value to a data member (self.side) of the new object just instantiated (self), in fact creating the data member in this
case, because it previous value was nil (i.e. non-existent).
The last line returns the newly-created object.
Defining methods
A function field (method) could be defined and/or documented
inside the 'class' (master object table) as shown in the notes at the
end of this page, but I find it cleaner to define it separatedly,
usually later in the same file:
--
-- compute the area of a Square object
--
function Square:GetArea()
return self.side * self.side
end
The colon (:) instead of the dot (.) is a Lua shortcut for:
function Square.GetArea(self)
That also implies that this function will probably be called with the
colon syntax, and that a hidden self
argument will be passed first, referring to the object given in the
function call. Here is an example of use:
sq = Square.Create(5)
print("area:", sq:GetArea)
Inside a function field (method), the self
variable refers to the object used to call the method: sq in the above example.
Unless 'getter' and 'setter' functions (e.g. GetSide(), SetSide()) are explicitely defined, the data
fields of an object can be accessed from outside methods by use of
the dot syntax:
sq.side = 12
print("area:", sq:GetArea)
As always in Lua, barriers are not enforced: the programmer is
responsible to keep code clean by avoiding messing with data members
directly (the lack of automatic getters and setters is a limitation of
this object system, although an helper function could probably be
devised to create them - either directly or through a metamethod-based
mechanism - but I like simple things).
Initializers
To initialize a data field, just set its value in the class
(master object) definition:
--
-- class Square
--
Square = {
side = nil,
posx = 0, -- initialized data
posy = 0
}
Since these fields are copied to every new instance in the
constructor Square.Create(), they act as
initializers.
An initializer can also be used as default value, if none (or a
special value, e.g. false) is passed to
the constructor:
--
-- class Square
--
Square = {
side = 10 -- default side length
}
--
-- constructor
--
function Square.Create(side)
local self = Utils.ShallowCopy(Square)
-- use default if arg not given
if side then self.side = side end
return self
end
(as seen before, side is the received
argument, while self.side it the object
data field)
Static fields
A data field in the class (master object) can be accessed independently
from the existence of instantiated objects; it can be considered common
to all instances ('static' in C++ or Java parlance). For example, we
could use a list to to keep track of the objects created so far:
--
-- class Square
--
Square = {
-- instance data
side = nil,
--
-- static data
created = {} -- list of created Squares
}
The created field can be accessed from
anywhere as Square.created, for
example every time a Square is created from
the constructor:
--
-- constructor
--
function Square.Create(side)
local self = Utils.ShallowCopy(Square)
self.side = side
-- remove copy of static data from this object
self.created = nil
-- add the new Square to the static list
table.insert(Square.created, self)
return self
end
In the above example, Square.created is a static
field (common to all objects), while self.created
is its copy made by Utils.ShallowCopy()
along with all other fields. This copy is just a waste of space, so in
the constructor it is convenient to delete it from the newly-created
instance, by setting it to nil.
(note: the above code will keep every created Square
alive through the reference from Square.created;
this could lead to a memory leak if objects are often created and
destroyed, unless appropriate steps are taken; this is a common issue
with garbage-collected languages and has nothing to do with objects)
Static functions (methods) are defined using a dot instead of the
colon, i.e. they do not receive the hidden argument self:
--
-- clear list of created Squares
--
function Square.ResetCreated()
Square.created = {}
end
Static function are not called through an instance using colon syntax
(as methods are), but they are called directly using the dot syntax:
Square.ResetCreated()
Inheritance
Lua being a dynamic language, in many cases there is no need to define
subclasses: single objects (instances) can be modified at will by
adding, removing or changing data fields or function fields, to build
personalized objects.
A more structured approach can also be used, similar to the one
employed by many popular languages: derive a subclass from a
superclass (or from more than one, in case of multiple inheritance). It
can be done like this:
--
-- class ColoredSquare (derived from Square)
--
ColoredSquare = {
color = BLACK
}
The class definition is similar to that of a stand-alone class like Square; the actual derivation takes place in
the constructor:
function ColoredSquare.Create(side)
-- first create the superclass
local self = Square.Create(side)
-- then add subclass fields
self = Utils.ShallowCopy(ColoredSquare, self)
-- rest of initialization here
return self
end
The first line creates a self object of
class Square; the second line uses the Utils.ShallowCopy() helper function to add
a copy of the ColoredSquare class fields to
the new self object.
After this operation, the object will contain both the fields
from the superclass Square and those from
the derived class ColoredSquare:
csq = ColorSquare.Create(5)
csq.side = 10
csq.color = RED
Utils.ShallowCopy() adds fields in-place,
i.e. modifies the table passed as second argument (self
in the above example) instead of making a copy, for the sake of
efficiency in the creation of complex objects.
If a field from the derived class has the same name as a field
from the superclass, the field is replaced: the derived class has
precedence, as would be expected.
Function fields (methods) work exactly the same way as data
fields: no special operation is required to make inheritance work. The
above note about field substitution implies that in this situation:
function Square:Draw()
-- draw a square
end
function ColoredSquare:Draw()
-- draw a colored square
end
the appropriate (most specific) method will be called:
sq = Square.Create(5)
csq = ColoredSquare.Create(5)
sq:Draw() -- calls Square.Draw()
csq:Draw() -- calls ColoredSquare.Draw()
Multiple inheritance is possible, by copying fields from more
than one superclass:
--
-- ColoredFilledSquare (derived from FilledShape and Square)
--
function ColoredFilledSquare.Create(side)
-- create the first superclass
local self = Square.Create(side)
-- add fields from the second superclass
self = Utils.ShallowCopy(FilledShape.Create(), self)
-- add subclass fields
self = Utils.ShallowCopy(ColoredSquare, self)
-- rest of initialization here
return self
end
Note that the constructors for all the superclasses must be
called to have valid objects to derive from: it would not be a good idea
to simply use FilledShape instead of FilledShape.Create(), because constructor
initialization would be skipped.
Name conflict precedence in multiple inheritance is simple: the
last one used in the constructor wins.
Access to superclasses
It is sometime useful to access a function field (method) that has been replaced,
in the subclass, by a different function of the same name. For example,
if ColoredSquare is derived from Square, the following code calls ColoredSquare:Draw():
csq = ColoredSquare.Create(5)
csq:Draw() -- calls ColoredSquare.Draw()
The original Square:Draw() of the
superclass has been replaced in the constructor of the derived
class (ColoredSquare.Create()) and is
therefore not direcly available. However, it could be called in this way:
csq = ColoredSquare.Create(5)
Square.Draw(csq) -- calls Square.Draw()
There are two differences here from an usual method call: the use of
the dot syntax and the explicit passing of the object csq to act upon (it will become self inside the Draw()
function).
This is nothing new: in fact, normal method calls like:
csq:Draw()
are translated by Lua to:
csq.Draw(csq)
where Draw is a field of the table csq, and csq is
also passed as first argument to Draw().
So, calling a function from a superclass just means calling a function
that is not a field of the object.
Notes
- This object system has limitations. Apart from the lack
of getters/setters mentioned above, the main drawback is that every
object carries with it [references to] all its methods; this makes this
approach inefficient for small objects to be produced in large numbers.
- Utils.ShallowCopy() only copies first-level
data; this must be remembered when instances have complex data
structures. The required behaviour should be implemented in the
constructor.
- Instance creation for derived classes could be automated
by using a super field referring to the
superclass (or to a list of superclasses) and writing a helper function,
say MakeInstance(). I prefer things to
stay simple.
- For the benefit of C++ users, in Lua a colon (:) means
member access requiring an object (dot or arrow in C++), while a dot
(.) means static access (double colon in C++).
- An object is valid as soon as it is created during the
constructor, so its methods can be called even within the
constructor itself.
- It is interesting to note that function fields (methods)
are treated exactly as initializers; defining a function as:
function Square:GetArea()
return self.side * self.side
end
is equivalent to defining it inside the class declaration, with an
explicit self parameter:
Square = {
side = nil,
--
GetArea = function(self)
return self.side * self.side
end
}
- Last but not least, the use of objects should not (in my opinion)
be taken as a religion; many problems can be efficiently solved by combining
object-based and non-object-based techniques. This is applicable to many
languages, but it is especially true in the case of Lua.
Back to start of page