Tools & thanks gpeddler 2 wxLua Simple OO in Lua Code notes


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


Back to start of page