Class Features

For all LOOP modules, classes are fundamentally metatables. For this reason, you can use ordinary tables as classes in all LOOP modules. For example, you can create a class that inherits from a module:

Array = oo.class({}, require "table")
function Array:length()
  return #self
end

a = Array{ "proto", "base", "simple", "multiple", "cached", "scoped" }
print(a:length())    --> 6
print(a:sort())
print(a:concat(" ")) --> base cached multiple proto scoped simple

Moreover, classes created with LOOP modules behave as ordinary metatables, so you can index them to get or set metamethods. However, each module implements classes differently to support additional features, like superclasses or access scopes. Each module introduces new functions to manipulate these additional class features, like iterating over the superclasses of a class.The downside of these functions is that they only manipulate classes created with that module. Therefore, if we use functions from module loop.simple to inspect classes created with module loop.multiple then things might not work as expected because the each module implements classes differently.

To minimize this problem when using different LOOP modules, all LOOP modules adopts a basic class model that allow us to manipulate them in a general way. This section describes such model, as well as some useful operations over classes and caveats of mixing classes from different models.

General Manipulation

Most of the time it is not a problem to use different LOOP modules in a application because, unlike objects, classes created in one portion of the code usually are not exported to other portions of the code that might use a different module. Libraries generally only export objects but not classes.

Anyhow, all LOOP modules adopts a basic class model where the metatable of the class provides the implementation of all operations that can be performed on that particular class. Therefore, to get the superclass of any class you can do:

local getsuper = getmetatable(class).getsuper
if getsuper ~= nil then
  super = getsuper(class)
end

LOOP provides the special module loop.classops to manipulate classes created with any of its modules using this general model. So, instead of the code above, you can do:

local oo = require "loop.classops"
super = oo.getsuper(class)

Inheritance Interoperability

There are some drawbacks when mixing classes from different LOOP modules in a single class hierarchy. In particular, classes from modules other than the loop.scoped module only inherits public members from classes of module loop.scoped. Other limitation is that fields of classes from loop.cached and loop.scoped modules always override the fields of classes from other modules, no matter their order in the list of super-classes. For example, consider the following code.

local BaseSuper   = loop.base.class  { attribute = "simple" }
local CachedSuper = loop.cached.class{ attribute = "cached" }
local MultipClass = loop.multiple.class({}, BaseSuper, CachedSuper)
local CachedClass = loop.cached.class  ({}, BaseSuper, CachedSuper)

local nocache = MultipClass()
local cached  = CachedClass()

print(nocache.attribute) --> simple
print(cached.attribute)  --> cached

The nocache object does not use a cache of fields, so it looks up the class hierarchy to find the value of field attribute and finds it in the BaseSuper class. However, the loop.cached uses a cache of inherited fields that only contains fields from cached classes (i.e. loop.cached and loop.scoped modules) therefore it finds the value of field attribute provided by class CachedSuper even though it is after the BaseSuper class in the list of super-classes of CachedClass.

Super-Class Access

LOOP modules provide functions for introspection of the class hierarchy, allowing the application to get the super-class of a particular class. However, the access to super-class in method implementations can be cumbersome in some situations. One main problem is that a class method is usually a plain Lua function and therefore is not bounded to a particular class. This way, there is no simple way to determine the class of a method in order to figure out its super-class and the inherited method implementation. The most straightforward solution is to place super-classes in local variables and access the inherited methods using the class stored in that variable, like in the example of the following code.

local Square = oo.class()
function Square:draw()
  self.canvas:setcolor(self.color)
  self.canvas:rect(self.xpos - self.width /2,
                   self.ypos - self.height/2,
                   self.xpos + self.width /2,
                   self.ypos + self.height/2)
end

local Button = oo.class({}, Square)
function Button:draw()
  Square.draw(self) -- calling inherited method
  self.canvas:setcolor(self.labelcolor)
  self.canvas:setfontsize(self.labelsize)
  local tw, th = self.canvas:textdim(self.label)
  self.canvas:text(self.xpos - tw/2,
                   self.ypos - th/2,
                   self.label      )
end

The first line of method Button:draw() calls the method Square:draw() over the object instance using the value stored in upvalue Square to retrieve the implementation of operation draw available in class Square. However, this explicit reference to the super-class Square may be troublesome in the scenario of maintaining the code in face of changes on the class hierarchy. To avoid such problem the acquisition of the super-class can be done through the Button class stored in variable Button, like in the code below.

local Button = oo.class({}, Square)
function Button:draw()
  oo.superclass(Button).draw(self) -- calling inherited method
  self.canvas:setcolor(self.labelcolor)
  self.canvas:setfontsize(self.labelsize)
  local tw, th = self.canvas:textdim(self.label)
  self.canvas:text(self.xpos - tw/2,
                   self.ypos - th/2,
                   self.label      )
end

Hierarchical Initialization

By default, LOOP classes work as Lua-like constructors, which are functions that receives a table containing named values to be used to create the new object, like in the example below:

Date = oo.class()
function Date:__tostring()
  return string.format("%02d/%02d/%d", self.day, self.month, self.year)
end

print(Date{day=30, month=5, year=2013}) --> 30/05/2013

However, we can use the __new metamethod to implement other styles of constructors. For example, the class can create an instance from a sequence of argument values instead of the values from a table, like in the example below:

Date = oo.class()
function Date:__new(day, month, year)
  local obj = {
    day = day,
    month = month,
    year = year,
  }
  return oo.rawnew(self, obj)
end
function Date:__tostring()
  return string.format("%02d/%02d/%d", self.day, self.month, self.year)
end

print(Date(30, 5, 2013)) --> 30/05/2013

The __new metamethod is a very basic mechanism and might not be easy to use in complex scenarios like the initialization of a class hierarchy. In such case, the class being instatiated might inherit from some classes that define define their own __new metamethod and also from others that do not. So the implementor of the subclass must know which __new must be called and which not, and in the proper order.

To make this easy, LOOP provides module loop.hierarchy with implementations of the __new designed to work with class hierarchies. Such implementations use the introspection mechanisms provided by LOOP (see General Manipulation) to inspect the class hierarchy and invoke a special initialization metamethod of each class of the hierarchy in order to initialize the instance properly.

Two implementations are provided. Function mutator is designed to work as Lua-like constructors. This function basically iterates through the class hierarchy from the superclasses down to the actual class, calling the method __init of each class that provides such method. This way, each class that requires the initialization of an attribute must provide the method __init that initializes the attribute correctly. For example, we could rewrite the example presented in here using this feature as illustrated below:

oo = require "loop.multiple"
hierarchy = require "loop.hierarchy"

Object = oo.class{ __new = hierarchy.mutator }

Contained = oo.class({}, Object)
function Contained:__init()
  assert(self.name, "no name for object")
  assert(self.container, "no container for object")
  self.container:add(self.name, self)
end

Container = oo.class({}, Object)
function Container:__init()
  self.members = self.members or {}
end
function Container:add(name, object)
  self.members[name] = object
end
function Container:search(path)
  local container, newpath = string.match(path, "(.-)/(.+)$")
  if container then
    container = self.members[container]
    if container and container.search then
      return container:search(newpath)
    end
  else
    return self.members[path]
  end
end

ContainedContainer = oo.class({}, Contained, Container)
.
.
.
Root = Container{}
Folder = ContainedContainer{
  container = Root,
  name = "my_folder",
}
File = Contained{
  container = Folder,
  name = "my_file.txt",
  data = "Hello, I'm a file"
}
print(Root:search("my_folder/my_file.txt").data) --> Hello, I'm a file

The other implementation provided is function creator, which is designed to resemble class constructors of Java or C++. This function basically creates an empty instance of the class and then calls the method __init of this instance if such method exists. The __init method receives all the parameters provided to the constructor.

In this case only a single __init method is called. So each class should implement a __init method that receives the constructor parameters for that class. Moreover, it must also call the __init function of each one of its superclasses providing the parameters expected by each superclass constructor, as illustrated in the example below:

oo = require "loop.multiple"
hierarchy = require "loop.hierarchy"

Object = oo.class{ __new = hierarchy.creator }

Contained = oo.class({}, Object)
function Contained:__init(container, name)
  assert(name, "no name for object")
  assert(container, "no container for object")
  container:add(name, self)
  self.name = name
  self.container = container
end

Container = oo.class({}, Object)
function Container:__init()
  self.members = {}
end
function Container:add(name, object)
  self.members[name] = object
end
function Container:search(path)
  local container, newpath = string.match(path, "(.-)/(.+)$")
  if container then
    container = self.members[container]
    if container and container.search then
      return container:search(newpath)
    end
  else
    return self.members[path]
  end
end

ContainedContainer = oo.class({}, Contained, Container)
function ContainedContainer:__init(container, name)
  Contained.__init(self, container, name)
  Container.__init(self)
end
.
.
.
Root = Container()

Folder = ContainedContainer(Root, "my_folder")

File = Contained(Folder, "my_file.txt")
File.data = "Hello, I'm a file"

print(Root:search("my_folder/my_file.txt").data) --> Hello, I'm a file

Copyright (C) 2004-2018 Renato Maia

This project was originally developed in Tecgraf at PUC-Rio.