Pedro Gimeno's gists (pastes with history), each in an independent (orphan) branch

Pedro Gimeno 2d335996dc OOP in Lua + Love 3 years ago
README.md 2d335996dc OOP in Lua + Love 3 years ago

README.md

Lua OOP and Löve

Classes and instances

Conceptually, you can think of a class as a type, and of an instance as a value of that type. There are some differences, but that helps understanding the mutual relationship between them and why they should not be confused. Just like a value is of a certain type, an instance is of a certain class. The class defines what you can do with the object it represents; the instance holds the data specific to that object.

For example, you can have a Rectangle class to represent rectangle objects, and you would define in it what operations can be performed on rectangle objects (e.g. intersect it with another rectangle object, find if a point is inside it, change its width or height, etc.); then you would have instances of that class, which contain the data that refers to specific rectangles rather than to the abstract type.

The key here is that when you call a method (i.e. a function) of an instance, it should operate on that instance specifically, not in any other, despite the function being defined in the class. For that purpose, the instance should always be passed as a parameter to the class.

This can be accomplished in Lua by means of the colon syntax. This syntax is just a shortcut that automatically passes the instance to the method as a first parameter called self.

Some methods do not need to operate on anything, they are just independent; it's just more practical to have them inside a class because they are intimately related to it. In many cases, the constructor (the function that creates an instance) falls under such category. When that's the case, the colon syntax is not really needed, and using it may create confusion. In C++, these are called static methods, so we'll use that name.

The Löve API uses that kind of constructors and instance methods, except that the constructors are outside the class. For example, the constructor of instances of the Image class is love.graphics.newImage. Notice no colon there. However, when we operate on a specific image, we use colon syntax to call its methods, e.g. img:getWidth().

In order for instances to act as if they contain the methods, we set the metatable of each instance to a table whose "__index" key is the class. This way, when we call a method in the instance, that method will be looked up in the class.

Here's an example of a class with a static constructor and an instance method:

-- Create the class
local Rectangle = {}
local RectangleInstanceMT = {__index = Rectangle}

-- This is the constructor. It does not operate on instances or classes, it's independent,
-- so it's static, and therefore we use the dot syntax for it.
function Rectangle.new(x, y, w, h)
  local result = {x = x, y = y, w = w, h = h}
  return setmetatable(result, RectangleInstanceMT)
end

-- This is a method that operates on a specific instance. It needs the instance as a
-- parameter, so we use the colon syntax which adds the instance parameter for us.
function Rectangle:containsPoint(x, y)
  return self.x < x and self.x + self.w > x and self.y < y and self.y + self.h > y
end

-- Note that these two lines are exactly equivalent:
-- function Rectangle:containsPoint(x, y)
-- function Rectangle.containsPoint(self, x, y)

-- Create two instances of the class Rectangle. We use dot syntax because the
-- constructor does not need to operate on an instance (in fact, none exists at the
-- beginning).
local rect1 = Rectangle.new(0, 0, 20, 30)
local rect2 = Rectangle.new(400, 300, 60, 90)

-- Check if the point (450, 350) is inside the first rectangle.
print(rect1:containsPoint(450, 350)) -- prints false
print(rect2:containsPoint(450, 350)) -- prints true

Note that when we call the static method using dot syntax, we're using the class (Rectangle); however, when we call the instances, we're using the variables that contain the instances. This is a good rule of thumb: in principle, there should be no reason to use dot syntax with instance variables. If you're using the class to invoke a method, you probably want dot syntax. Note, however, that it's possible to have class methods, that is, methods that operate on classes, and if you have them, then using colon syntax on classes would also make sense. In these cases, you need to be aware of whether the method you're calling requires dot or colon syntax. To avoid worrying about that, some authors prefer to ditch dot syntax and always use colon syntax even for static methods, even though they don't use the implicit self parameter.

Class inheritance

This is a basic concept in OOP. You create base classes that contain generic behaviour, and then make other classes that extend these base classes in some way. In turn, other classes may then extend these new ones.

For example, we may have a base class called Person, that refers to any person in general; then we can extend it to create two other classes like Employee and Customer. The Employee class could then be extended to create the class Programmer, used for the programmers of the staff of a certain company.

In our example, the only basic functionality of the class Person would be to display the name of the person. The Employee class would also display the salary, and the Programmer class would display the project that they're involved with. The Customer class would be able to contain and display the goods that they buy.

Some kind of mechanism is necessary in order to take advantage of the functionality of the parent class. In our example, this mechanism is implemented with the ability to call a method of the parent class, but applying it to the current instance. This means that the current instance must be a strict superset of the parent class, that is, it must have everything that the parent class has, plus some more stuff.

Here's an example of defining the class hierarchy and creating two programmers called John and Jane Smith, both earning 100 cr. a month and working on the Yoyodine project, and a customer called John Doe that buys oranges, then we display all three:

local Person = {}
local PersonInstanceMT = {__index = Person}

local Customer = setmetatable({}, {__index = Person})
local CustomerInstanceMT = {__index = Customer}

local Employee = setmetatable({}, {__index = Person})
local EmployeeInstanceMT = {__index = Employee}

local Programmer = setmetatable({}, {__index = Employee})
local ProgrammerInstanceMT = {__index = Programmer}

function Person.new(firstName, lastName)
  local result = {firstName = firstName, lastName = lastName}
  return setmetatable(result, PersonInstanceMT)
end

function Person:display()
  print("Name: " .. self.lastName .. ", " .. self.firstName) 
end

function Customer.new(firstName, lastName, goods)
  local result = Person.new(firstName, lastName)
  result.goods = goods
  return setmetatable(result, CustomerInstanceMT)
end

function Customer:display()
  -- Note how we don't use colon syntax here, because we don't want to pass the class (Person)
  -- but the instance (self).
  Person.display(self)
  print("Goods bough: " .. self.goods)
end

function Employee.new(firstName, lastName, salary)
  local result = Person.new(firstName, lastName)
  result.salary = salary
  return setmetatable(result, EmployeeInstanceMT)
end

function Employee:display()
  Person.display(self)
  print("Salary: " .. self.salary .. "cr")
end

function Programmer.new(firstName, lastName, salary, project)
  local result = Employee.new(firstName, lastName, salary)
  result.project = project
  return setmetatable(result, ProgrammerInstanceMT)
end

function Programmer:display()
  Employee.display(self)
  print("Project: " .. self.project)
end

local prog1 = Programmer.new("John", "Smith", 100, "Yoyodine")
local prog2 = Programmer.new("Jane", "Smith", 100, "Yoyodine")

local cust1 = Customer.new("John", "Doe", "Oranges")

prog1:display()
print()
prog2:display()
print()
cust1:display()

Note that with the technique described above, whenever you call a method that is only implemented in the base class, Lua needs to look up the metatable of every class in the hierarchy. This can make the program slow. To avoid this, a simple optimization is to copy the methods of the base class to the new class or instance.

It is not difficult to generalize this to a class library. Using class methods for the constructors instead of static methods helps with the automatic determination of the parent of a class, by getting the metatable of the self parameter. Additionally, using the class table as a metatable of itself saves a Lua table.

-- Object will be the base class
local Object = {}
local Metas = {}

-- Call a function from the parent of this object (call only on classes)
function Object:inherited(method, ...)
  local parent = self:parent()
  if not mt then
    error("Attempting to call an inherited method of the base Object")
  end
  mt.__index[method](...)
end

function Object:new(data)
  data = data or {}
  data.class = self
  
  -- Copy the methods of the class to the child
  for k, v in next, self do
    if data[k] == nil then
      data[k] = v
    end
  end

  if not Metas[self] then
    Metas[self] = {__index = self}
  end
  return setmetatable(data, Metas[self])
end

function Object:extend(methods)
  local newclass = methods or {}
  newclass.parent = self

  for k, v in next, self do
    newclass[k] = v
  end
  return setmetatable(data, {__index = self})
end