Lua for App Development

Bounces Lua Object Framework

This document presents the Bounces Lua object framework, a significant addition that Bounces brings to Lua. The Lua object framework provides a powerful and easy-to-use object model integrated right into Lua. The object framework plays a key role in Bounces, as it enables dynamic code update and native objects bridging.

To make possible the development in Lua of iOS, tvOS, and macOS applications, Bounces adds a few new types and frameworks to those already provided by the Language. However, Bounces doesn't make any significant change to the Lua language itself. All Bounces additions are implemented using the flexible extension mechanisms build-in in Lua.

This article supposes that you already know Lua, or at least that you feel comfortable reading basic Lua code. If not, you can read the first article in this series, Get started with Lua - The Lua language, that gives a simple introduction to the Lua language.

This article includes many code examples introduced by short explanation texts. Comments in the code bring additional information, so they are probably worth reading. All code examples are valid tested code that you can execute in Bounces, and you can use the debugger to get an in-depth understanding of their behavior if you want.

There are several ways to use this article: you can read it sequentially, or you can use it as a quick reference of Bounces additions, by jumping directly to a specific topic via the table of content (on the left).

Bounces Lua object framework

Bounces comes with an object framework that adds object-oriented programming to Lua. This framework implements a familiar class-based object model with single-inheritance, similar to those found in Swift, Objective-C, or Java.

The Bounces object model is very easy to use, just like Lua tables; it is fully compatible with the native object models of Bounces target platforms, as we will see in the Lua Native bridge section; and, last but not least, it is fast and fully integrated with the Bounces debugger.

Defining a Class

You create a new class by calling the global function class.create() that returns a reference to the created class. Stored in a variable, this reference is then used to declare methods, fields and properties for the corresponding class.

Let's start by creating a simple counter class:

-- Create a class named "Counter" 
local Counter = class.create ("Counter")  -- the created class is stored in a local variable

-- You can also use the shorter equivalent syntax: local Counter = class("Counter")

-- Variable 'Counter' contains a reference to the class
print (Counter)             --> Class Counter
print (Counter.superclass)  --> Class LuaObject   (LuaObject is the root class)

-- define instance methods

function Counter:increment (delta)  -- this defines an instance method named 'increment'
    delta = delta or self.step  -- self.step is the default counter step
    self.count = self.count + delta
end

function Counter:doOneStep ()  -- this defines an instance method named 'doOneStep'
    self:increment ()
end

-- a class can have one or more initializer; initializer names shall start with 'init' (e.g. 'init', 'initXyz', 'initWithSomeStuff'...)
-- IMPORTANT: you never call directly an initializer. Instead, you create an instance by calling a 'new'-prefixed method (e.g. 'new', 'newXyz', 'newWithSomeStuff'...)

function Counter:initWithCountAndStep(initialCount, defaultStep)
    self.count = initialCount
    self.step  = defaultStep
end

return Counter -- return the created class as the chunk's result

A good practice is to put a class definition in its own Lua module, and this is what we have done here. By convention, this module returns the class object, Counter in this example.

The Counter class defines a single initializer method initWithCountAndStep(). To learn more about initializers, read the Object Initalizers and Finalizers section later in this document.

Instance creation and basic use

Here is how you create and use an instance of the Counter class defined above:

local Counter = require "Counter"  -- load the Counter class, stored in the "Counter" module

-- creating an instance
local c1 = Counter:newWithCountAndStep (10, 1) -- create a Counter instance using the initializer 'initWithCountAndStep'

print (c1)  --> [Counter 0x600000014b20]    count: 10    step: 1

-- Calling instance methods
c1:doOneStep()    -- c1.count --> 11
c1:increment(5)   -- c1.count --> 16

-- you can add a field to an object instance anywhere in the code
c1.role = 'Counting stuff'
print (c1)      --> [Counter 0x600000014b20]    count: 16    step: 1    role: Counting stuff

print (c1.count, c1.role)  --> 16   Counting stuff    (instance fields can be directly accessed)

Class methods and fields

Class fields

Just like instances, classes can have fields. Class fields have a double role:

  1. provide true class variables,
  2. provide default values for unset instance fields (the class being considered as the instance prototype).

Class fields are fully dynamic —like other Lua entities— and can be set anywhere in the code without having to be declared first.

local Counter = require "Counter"  -- get the counter class (load the counter module if needed)

-- creating an instance with the default initializer
local c2 = Counter:new()  -- create a Counter instance using the default initializer 'init', not defined for this class 
print (c2)                 --> [Counter 0x610000015040]
print (c2.count, c2.step)  --> nil    nil

-- because fields 'count' and 'step' in c2 are not set, calling method 'increment' cause an error (comment out the following line to continue execution)
c2:increment()  --> [Counter, 11] Error: attempt to perform arithmetic on field 'count' (a nil value)

-- you set class fields using the dot-notation, just like instances fields
Counter.count = 0  -- a class field can have the same name as an existing instance field
Counter.step = 1

-- class fields act as default values for unset instance fields with the same name
print (c2)                 --> [Counter 0x610000015040]
print (c2.count, c2.step)  --> 0    1
c2:increment()             --  No more error!
print (c2)                 --> [Counter 0x600000014b20]    count: 1

-- class fields can be changed anywhere in the code
Counter.step = 4

c2:increment()  -- c2.step still defaults to the class value
print (c2)      --> [Counter 0x600000014b20]    count: 5

Class methods

As we have seen above in the Counter class example, to define an instance method, you declare this method directly on the class variable.
For example, function Counter:doOneStep() ... end defines an instance method doOneStep, shared by all instances of the Counter class.

To define a class method, you declare it as a method of the class field of the target class.
For example, function Counter.class:aClassMethod (...) ... end defines a class method of the Counter class, named aClassMethod.

local Counter = require "Counter"  -- get the counter class (load the counter module if needed)

-- defining a class method: you do this by defining a method of Counter.class
function Counter.class:createCountDown(initialValue)   -- notice the '.class' specifying that createCountDown is a class method
    return self:newWithCountAndStep (initialValue, -1) -- create a new instance  using the initalizer 'initWithCountAndStep'
end

-- calling a class method
local c3 = Counter:createCountDown(100)  -- class method `createCountDown` is called on the Counter class
print (c3)        --> [Counter 0x608000017f20]    step: -1    count: 100
c3:doOneStep()    -- c3.count --> 99   -- instance method `doOneStep` is called on an instance (c3)
c3:doOneStep()    -- c3.count --> 98

Note that the definition of class method createCountDown above, that have been put here for clarity, would probably be better located if in the "Counter" module.

Subclassing

To define a subclass of a given class, you call the createSubclass method of the parent class, like parentClass:createSubclass(subclassName).

Alternatively, you can call class.create and pass the parent class as the second parameter like class.create(subclassName, parentClass).

local Counter = require "Counter" -- get the counter class (load the counter module if needed)

-- create a subclass of Counter
local EvenCounter = Counter:createSubclass ("EvenCounter") -- parameter is the subclass name

print (EvenCounter.superclass) --> Class Counter

EvenCounter.count = 0
EvenCounter.step = 2

-- define methods for this class

function EvenCounter:increment ()  -- override method 'increment' defined by the superclass
    -- call the superclass method
    self[Counter]:increment (self.step) -- read this as "call the increment method of class Counter on self"
                                        -- Equivalent to: self[EvenCounter.superclass]:increment(self.step)
end

function EvenCounter:reset()  -- define a new method
    self.count = nil -- revert to the class field value
end

return EvenCounter

Using the EvenCounter class is straightforward:

local EvenCounter = require "EvenCounter" -- load the EvenCounter class

local ec = EvenCounter:new() -- create an instance of class EvenCounter

ec:increment()
ec:increment()
print (ec)        --> [EvenCounter 0x6180000180f0]    count: 4

ec:reset()
print (ec)        --> [EvenCounter 0x6180000180f0]
print (ec.count)  --> 0

Note in the above code how a superclass method is called: by indexing an object —usually self— by a known class reference —like here self[Counter]—, you specify that any method called on this entity shall use the corresponding method defined by the indexing class, and not the current method defined for the indexed object. This replaces the super compile-time-defined keyword found in most static languages, but you can rewrite the above code like this if you prefer:

local super = self[Counter]
  super:increment()
  

Warning: You should never call a superclass method by directly getting the superclass of a dynamic object instance, otherwise your code could enter an infinite recursion and crash:

-- This will cause infinite recursion if called on an EvenCounter subclass instance
  function EvenCounter:increment ()
      self[self.superclass]:increment(self.step) -- !! Do not write this !!
  end
  

Class extensions

Class extension provide a robust mechanism permitting to split the definition of a class between several modules, or to add new functionalities to an existing class defined elsewhere. Class extensions in the Bounces object framework are similar to Swift Extensions or Objective-C Categories.

You can define a class extension of a Lua class (i.e. a class created by class.create or :createSubclass); you can also create a class extension of a native class, as we will see in the Lua Native bridge section.

You create a class extension by calling method addClassExtension on the extended class with an optional extension name parameter.

local Counter = require "Counter" -- load the Counter class

Counter: addClassExtension ("TestLoop") -- create an extension of the Counter class named "TestLoop" and returns the extended class (here Counter)

-- Following déclarations are part of the class extension

Counter.defaultIterationsCount = 100000

function Counter:testLotsOfIncrements(n)
    print ("Before test: ", self)

    for i = 1, n or self.defaultIterationsCount do
        self:increment()
    end

    print ("After test: ", self)
end

return Counter

Before using methods, fields or properties defined in a class extension, you have to load the module defining it.

local Counter = require "Counter" -- get the counter class (load the counter module if needed)

local c = Counter:newWithCountAndStep (10, -1) -- create a Counter instance

print (c.testLotsOfIncrements)  --> nil  (method "testLotsOfIncrements" is not defined yet)

-- You can also test if a class extension is loaded with class method "hasClassExtension"
print (Counter:hasClassExtension "TestLoop")  --> false

-- To load a class extension, you load the module defining it (named here "Counter-TestLoop")
require "Counter-TestLoop"
print (Counter:hasClassExtension "TestLoop")  --> true

-- Now we can call method "testLotsOfIncrements" on instance c 
-- (Note that having created c before the extension was loaded is not an issue)
c:testLotsOfIncrements() --> Before test:   [Counter 0x610000017fc0]    step: -1    count: 10
                         --> After test:    [Counter 0x610000017fc0]    step: -1    count: -99990

Warnings: You can not use a class extension to override a Lua method defined in another module or class extension; if a same method is defined in more than one class extension, which one will be called is undefined. Additionally, if you define several extensions of a given class, be sure to give a different name to each of them, or odd things may happen.

Object properties

In the Bounces object framework, a property is an object field associated with a getter and a setter methods. Reading the field actually gets the value returned by the getter; writing the field actually pass the new value to the setter.

Bounces object properties are similar to (and compatible with) Swift computed properties or Objective-C properties. They are easy to use and make the code more readable.

You define a property by calling the global function property and assigning the result to a class field, like in: Class.field = property ().

require "Counter" -- ensure the Counter class is loaded

local Counter = class.Counter:addClassExtension ("Properties") -- start a class extension; note that the Counter class can be accessed as class.Counter

-- declare the field 'step' (used by the increment method) as a property
Counter.step = property { default = 1 }  -- define a property 'step', with a getter method `step()`, a setter method `setStep(stepValue)` and an internal storage key `_step`
                                         -- Additionally here, we specify a default value (1) for the property

-- we can define a specific property setter or/and getter method anywhere, before or after the property definition
function Counter:setStep(stepValue)
    -- This setter builds a step history stack, so that setting a step value can be undone
    self.stepHistory = self.stepHistory or {} -- create the step history table if needed
    self.stepHistory[#self.stepHistory + 1] = self._step -- append the current step value to the history table

    self._step = stepValue -- store the new step value (notice the leading '_' in the property storage key)
end

-- define a class field (not a property)
Counter.count = 0 -- set a default value for the count field in Counter instances

-- a property definition can include attributes and getter / setter definitions
Counter.previousStep = property { kind = 'readonly',  -- attribute 'kind' specifies a predefined behavior for this property; supported kinds are: 'readonly', 'copy', and 'weak'
                                  get = function (self)  -- attribute 'get' is used to define a getter method (don't forget the self parameter!)
                                            if self.stepHistory then
                                                return self.stepHistory[#self.stepHistory] -- the previous step value is the last value in the history table
                                            end
                                            -- if the stepHistory is nil, this getter doesn't return any value (equivalent to returning nil)
                                        end
                                  -- for a non-readonly property, we could also define a setter as: set = function(self, value) ... end
                                }

-- define an undo method
function Counter:undoSetStep ()
    if self.stepHistory then
        local historyLength = #self.stepHistory

        -- restore the last step value
        self._step = self.stepHistory[historyLength] -- remember: table indexes start at 1

        -- remove the last step from the history table
        if historyLength == 1 then
            self.stepHistory = nil  -- empty history
        else
            self.stepHistory[historyLength] = nil
        end
    end
end

return Counter

Using properties is straightforward:

local Counter = require "Counter-Properties" -- load the Counter class, and the "Properties" class extension

local c = Counter:new()
c:increment()  -- c.count --> 1

c.step = 10 -- this calls the property setter defined in the "Properties" class extension above
c:increment()  -- c.count --> 11
print (c.step, c.previousStep) --> 10    1    (this calls the property getters of 'step' and 'previousStep')

c.step = -4
print (c.step, c.previousStep) --> -4    10
c:increment()  -- c.count --> 7
c:undoSetStep()
print (c.step, c.previousStep) --> 10    1
c:increment()  -- c.count --> 17

c.previousStep = 42  --> Error: Cannot set read-only property previousStep.

Object Initializers and Finalizers

This section goes into more details about object initializer methods —briefly mentioned in section Defining a Class above— and shows how to perform specific actions when an object is deallocated by defining a finalizer method.

Initializers

Initializers are special methods that initialize a newly created object instance. In Bounces Object Framework, by convention, the name of an initializer method shall begin with the string "init", eventually followed by more capitalized words.

Your Lua code never directly calls an initializer method. Instead, it calls a new-prefixed method on a class: this method creates (i.e. allocates) the new instance and then calls the corresponding init-prefixed initializer on the newly created instance, i.e. the corresponding initializer method name is calculated by replacing the prefix new in the class method name by init.

For example, local object = MyClass:newWithRect (someRect) creates an instance of class MyClass and initialize it by calling method initWithRect(someRect) on the new MyClass instance.

Initializers can follow two patterns: the simple initializer pattern or the transforming initializer pattern. You can choose one or the other for your class depending on its behavior.

Simple initializer

A simple initializer doesn't change the value of self and doesn't return any value. This leads to simple and concise initializer code.

function MyClass:initWithRect (rect)
    self[MyClass.superclass]:initWithRect(rect) -- call the superclass initializer
    -- ... specific init for MyClass
end

Transforming initializer

Transforming initializers can change the value of self and shall return this new value. This is generally used to indicate that the initializer has failed, by returning nil. This may also be used to force the use of a singleton instance in a class, or to implement a class factory or class cluster.

For example, assuming the MyClass' superclass init may fail, we would rewrite MyClass:initWithRect like this:

function MyClass:initWithRect (rect)
    self = self[MyClass.superclass]:initWithRect(rect) -- call the superclass initializer
    if self ~= nil then
        -- ... specific init for MyClass
    end
    return self
end

Typically, a transforming initializer calls a superclass initializer, stores the result in self, performs its specific inits if self is non-nil, and finally return self.

If you are familiar with Objective-C, you have probably noticed that transforming initializers are very similar to ObjC init methods. This is by design and make things easier when briding Lua code with iOS or macOS native code.

Note that a simple initializer is fully equivalent with a transforming initializers that returns an unmodified self, so which one you choose is mostly a matter of personal preference.

Object finalizers

Optionally, if an object need to perform some action before being deallocated, you can define a finalize instance method for this object's class. A finalize method has no parameter and isn't supposed to return any value.

Example:

function MyClass:finalize()
    self:closeMyOpenFiles()
end

Remember that Lua uses a Garbage Collector mechanism to release its memory. Therefore the finalizer method for a given object is called when this object is GC-ed, which may occur some time after the point where this object is not needed anymore by the program.

Class finalizers

Although class-level finalizers are rarely needed, you can define one by declaring a class method named finalize. A class finalizer is called when the current Lua Execution Context is terminated. Typically, a class finalizer may be needed for releasing application resources reserved at the class level.

Dynamic Update of Lua Classes

Bounces object framework supports dynamic code update by design. This means that changing the code of a class and reloading the corresponding module(s) just works as expected: the new class code replaces the old one and existing instances of the class or of its subclasses continue to live their own life undisturbed but using the new class code for their methods, properties and fields.

Creating or extending a class multiple times

It is perfectly safe to call class creation and extension functions / methods multiple times: if the created class or class extension already exists, the call simply returns the existing class.

So, when a Lua module calling class.create(), :createSubclass() or :addClassExtension() is reloaded, the module code behave just as expected and you don't end up with multiple class or extension version in the target application.

Updating methods and properties

When a Lua module is reloaded, all classes defined or extended in this module get updated to reflect the set of methods or properties defined in the new module version.

This means that you can dynamically add, remove or update Lua methods or properties of a class by simply modifying and reloading the Lua module in which those methods or properties are defined.

When a method is removed, if a superclass version of this method exists, the superclass method becomes visible to instances of the current class or its subclasses. Otherwise, calling the removed method may cause an exception.

Note: a method deleted (or commented out) in a reloaded Lua module will actually be removed from its class only if the method was declared in a context of class creation or class extension in the current Lua module (i.e. after a call to class.create(), :createSubclass() or :addClassExtension() done in the same Lua module).

Therefore, although a method can technically be defined anywhere in your Lua code, it is highly recommended that you always define methods in the context of a class creation or class extension.

Where to go from here?

The Bounces object framework is great, but it takes its whole dimension when using or extending the target native platform. The Lua Native bridge overview document is worth reading before you start writing part of your next iOS, tvOS or macOS application in Lua.

Post a Comment