Basic Concepts
Creating objects in Lua is very simple and clean. Take for example the code bellow that creates an object implementing a stack:
Stack = { top = 0 } function Stack:push(value) local pos = self.top+1 self[pos] = value self.top = pos end function Stack:pop() local pos = self.top if pos > 0 then local value = self[pos] self[pos] = nil self.top = pos-1 return value end end . . . Stack:push("Hello") print(Stack:pop())
However, if we want to create more than one stack, we will need a factory of stacks. There are different ways to create a factory in Lua. For example, you could create a function that uses the code above to create and return a new stack. A more usual solution however is to create objects that borrow the implementation from the original stack object, which works as a prototype or class for the new objects. LOOP modules are designed to aid the later approach.
Cloning Objects
LOOP provides a module for cloning objects called loop.proto
.
This module basically provides the clone
function.
This function receives the object that will provide the members (methods and attributes) and an optional table that will become the cloned object.
The clone
function is equivalent to:
function clone(proto, clone) return setmetatable(clone or {}, {__index=proto}) end
Cloning an object with clone
is different from copying it (see copy
) because the cloned object does not have a copy the members of the original object.
Instead, the cloned object references the original object and consults it to get its members whenever necessary.
So, if the original object changes, this change will reflect on its clones.
As an example we could make our stack object a prototype and clone it to create more stacks, like in the example below:
oo = require "loop.proto" otherStack = oo.clone(Stack) otherStack:push("Hello") print(otherStack:pop()) --> Hello
It is worth noticing that when we set a field in the clone, this field is not set in the prototype.
So when we call myStack:push("Hello")
the line self.top = pos
sets a value for attribute top
in the clone, not the prototype.
This way, next time someone consults the value of top
in the cloned object, it will not consult the prototype to get its value.
The clone now have a value for top
of its own.
One problem with cloning is that we have to avoid changing the prototype in a way that breaks the behavior of the clones.
For example, suppose we call an operation on the prototype, like Stack:push()
.
This call would change the value of attribute top
of the prototype to 1
.
All clones without a value for top
would automatically inherit the value 1
from the prototype.
This way, all such clones would appear as if they have the same values of the prototype.
This might not be what we want.
For this reason, a better approach in many scenarios is to treat the prototype object in a special way, not as an ordinary object, but exclusively as a template or class.
Creating Classes
LOOP provides different modules that support OOP based on the concept of classes. A LOOP class is basicaly a metatable. Moreover the instances of a class are tables that have the class as their metatable.
LOOP classes have three minor extensions in relation to ordinary metatables.
First, the default value of metamethod __index
is the class itself.
So all fields of the class are shared with all its instances.
In fact the class works as a prototype for all its instances.
Secondly, a class can be used as a function to create instances of that class thus working as a factory.
Finally, a class can optionally define a special metamethod called __new
to redefine how instances of that class are created.
Class-based moduels provides function class
to create classes, which is equivalent to:
function new(class, ...) local new = class.__new if new ~= nil then return new(class, ...) end return setmetatable(... or {}, class) end function class(metatable) if metatable == nil then metatable = {} end if metatable.__index == nil then metatable.__index = metatable end return setmetatable(metatable, {__call=new}) end
By default, classes are like prototypes but, unlike prototypes, classes can redefine some behavior of its instances through metamethods. Moreover, since the metamethods defined in the class only applies to the instances, the class itself might not behave like its instances. For this reason, classes usually are used only as a template or mold of a group of object and not as working objects. As an example, we could rewrite our stack implementation using a class, like in the example below:
oo = require "loop.base" StackClass = oo.class{ top = 0 } function StackClass:push(value) local pos = self.top+1 self[pos] = value self.top = pos end function StackClass:pop() local pos = self.top if pos > 0 then local value = self[pos] self[pos] = nil self.top = pos-1 return value end end function StackClass:__tostring() local strings = {} for i, value in ipairs(self) do strings[i] = tostring(value) end return "{"..table.concat(strings, ",").."}" end . . . myStack = StackClass() myStack:push("Bottom") myStack:push("Middle") myStack:push("Top") print(myStack) --> {Bottom,Middle,Top} print(myStack:pop()) --> Top print(myStack:pop()) --> Middle print(myStack:pop()) --> Bottom
The __new
Metamethod
By default, when a class is used as a factory to create an instance of that class, it takes the first argument and turns it into an instance of the class. This allows for an easy way to initialize the new instance attributes when necessary. For example, we could create a stack with three strings inside it by providing a table with proper contents, like in the example below:
myStack = StackClass{ "Bottom", "Middle", "Top", top = 3 } print(myStack:pop()) --> Top print(myStack:pop()) --> Middle print(myStack:pop()) --> Bottom
However, the way instances are created can be redefined with a metamethod defined by field __new
of the class.
This metamethod is called to create instances of a class and receives the class and all the values provided to the factory.
For example, suppose that we want to change the class StackClass
so it can be instantiated from a sequence of values instead of a table containing its internal values.
Then we can use the code provided below:
oo = require "loop.base" StackClass = oo.class{ top = 0 } function StackClass:__new(...) local count = select("#", ...) return oo.rawnew(self, { top = count, ... }) end . . . myStack = StackClass("Bottom", "Middle", "Top") print(myStack:pop()) --> Top print(myStack:pop()) --> Middle print(myStack:pop()) --> Bottom
The function rawnew
used in the code above turns a table into an instance of a class without calling the __new
metamethod.
One common use for the __new
metamethod is to initialize a table so it can be turned into an instance of a class.
For example, suppose we want to store the elements of our stack in a separate table stored in a field called elements
.
This way, we would have to create a new storage table for each instance of the stack class.
We could use the __new
metamethod to create such table at each instantiation, like in the example below:
oo = require "loop.base" StackClass = oo.class() function StackClass:__new(newStack) newStack = newStack or {} if newStack.elements == nil then newStack.elements = {} end newStack.top = #newStack.elements return oo.rawnew(self, newStack) end function StackClass:push(value) local pos = self.top+1 self.elements[pos] = value self.top = pos end function StackClass:pop() local pos = self.top if pos > 0 then local value = self.elements[pos] self.elements[pos] = nil self.top = pos-1 return value end end
Library Organization
LOOP is organized as a set of modules. These module are designed to allow the programmer to load only the code that provides the functionality he needs. For most applications only a few of these modules are actualy required. We can separate LOOP modules in tree categories, described below:
Basic Modules
The first group of modules are designed to provide support for different styles of OOP in Lua.
To use prototyping, you might requilre only the loop.proto
module.
However, to use classes you might requilre one of the class-based modules listed below.
These class-based modules are organized as incremental modules that provides the same functionality of the previous one but introducing some new funcionality.
This way, your application can load only the module that provides the funcionality it requires.
Class-based Module | Introduced feature |
---|---|
loop.base |
The __new metamethod. |
loop.simple |
Simple inheritance. |
loop.multiple |
Multiple inheritance. |
loop.cached |
Metamethod inheritance. |
loop.scoped |
Member access control. |
Complementary Modules
The second group contains modules with auxiliary functions that complement the functionality of the basic modules but are not necessary in most applications.
Utility Modules
Finally, LOOP provides the loop.table
module with some basic operations on tables.