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:
- provide true class variables,
- 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 likeclass.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 hereself[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 thesuper
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