Get started with Lua (2/3)

CodeFlow additions to Lua

This second article of the Get started with Lua series presents a few significant additions that CodeFlow brings to Lua. The main of these additions is the CodeFlow object framework, a nice and powerful object model integrated right into Lua. The CodeFlow object framework has a key role in CodeFlow, as it enables dynamic code update and native objects bridging. And, as you will learn in this article, it is also very easy to use!

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

This article includes many code examples introduced by short explanation texts. Comments in the code bring explanations and additional information, so they are probably worth reading. All code examples are valid tested code that you can execute in CodeFlow, 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 CodeFlow additions, by jumping directly to a specific topic via the table of content (on the left).

Part 2 - CodeFlow additions to Lua

To make possible the development in Lua of iOS, tvOS, and MacOS applications, CodeFlow adds a few new types and frameworks to those already provided by the Language.

Note that CodeFlow doesn't make any change to the Lua language itself. All CodeFlow additions are implemented using the flexible extension mechanisms build-in in Lua.

CodeFlow object framework

CodeFlow 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 CodeFlow object model is very easy to use, just like Lua tables; it is fully compatible with the native object models of CodeFlow target platforms, as we will see in the ObjC bridge section below; and, last but not least, it is fast and fully integrated with the CodeFlow debugger.

Defining a Class

You access to the CodeFlow object framework via the global variable class. class is a table containing two main functions: class.createClass() used to create a new class, and class.extendClass() used to define a class extension of an existing class.

Let's start by the begining, with class creation:

-- Create a class named "Counter"
local Counter = class.createClass ("Counter")  -- class.createClass returns the created class, so you can store it in a variable

-- 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' 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.

Instance creation and basic use

Here is how you create 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 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).
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 the following line to continue execution)
c2:increment()  --> [Counter, 11] Error: attempt to perform arithmetic on field 'count' (a nil value)

-- classes can have fields, just like instances
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 be better located if in the "Counter" module.

Subclassing

To define a subclass of a given class, you simply pass this class as the second parameter of class.createClass

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

-- create a subclass of Counter
local EvenCounter = class.createClass ("EvenCounter", Counter) -- first parameter is the new class name, second parameter is the superclass

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:

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

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 CodeFlow object framework are analog to Swift Extensions or Objective-C Categories.

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

You create a class extension by calling class.extendClass with two parameters: the class to extend and an optional extension name.

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

class.extendClass (Counter, "TestLoop") -- create an extension of the Counter class named "TestLoop" and returns the extended class (here Counter)

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, the behavior in undefined. Additionally, if you define several extensions of a given class, be sure to give a different name to each of them, or strange things may happen.

Object properties

In the CodeFlow 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.

CodeFlow 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.extendClass (class.Counter, "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.

Lua modules in CodeFlow

In CodeFlow, you load Lua code almost exclusively via the require function, as illustrated in the above code samples. Actually CodeFlow considers every code file in your project as a Lua module, including individual components of the bindings libraries used to provide interfaces to the native platform sdk.

For various reasons, and in particular to enable dynamic module update from the IDE, CodeFlow provides a custom implementation of the require function. This custom implementation has a few differences compared to the standard Lua version:

  • Lua modules in CodeFlow can return multiple values, like any Lua function. Therefore require returns transparently all results returned by the module's chunk, and returns nil if the module's chunk does not return anything.

    -- A module named 'Controller' in your project
    local function createController (controllerName) 
      return { name = controllerName } 
    end
    local createdMessage = "Controller has been created"
    
    return createController, createdMessage
    
    -- The caller module gets all results via the require function
    local createFunction, message = require "Controller"
    
    local firstController = createFunction ("controller-1") -- firstController --> { name = "controller-1" }
    local secondController = createFunction ("controller-2") -- secondController --> { name = "controller-2" }
    
    print(message) --> Controller has been created
    
  • Module names are references to files in the CodeFlow project source tree. A module name is expressed as a .-separated path indicating the location of the module the project source tree. The provided path can be absolute or relative to the caller module's location in the project source file tree. You usually use relative modules paths inside Lua module groups that you intend to reuse in different projects.

    -- Suppose the 'Controller' module above has been placed in a group or folder named 'Utils'
    local createFunction, message = require "Utils.Controller" -- if the current module is also in the 'Utils' folder, we could use the relative path "Controller" to specify the required module
    
    -- loading modules in bindings libraries follows the same pattern
    local UIView = require "UIKit.UIView" -- loads the module named 'UIView' in library 'UIKit'
    

    Actually CodeFlow's module search policy is optimized for apps development and remote modules loading from the IDE, making it quite different from the search policy implemented in standard Lua. As a consequence, the Lua package library is not relevant, and therefore not available, when developing with CodeFlow.

  • Lua modules may be re-executed if they change. In CodeFlow, require has the same overall behavior as in the standard Lua implementation: if you call require on an already-loaded module, the module is not recompiled nor re-executed, and require simply returns the stored results of the module chunk's previous execution.

    However CodeFlow's dynamic code update feature means that module chunks may be re-compiled and the chunk's function re-executed when the corresponding module is changed in the IDE. Generally, this won't have any unwanted impact on your program, especially if you are using the CodeFlow object framework, but you should keep this in mind when writing code at the top level of a Lua module.

Where to go from here?

The CodeFlow object framework is great, but it takes its whole dimension when using or extending the target native platform. The third and last part of this series, Get started with Lua - CodeFlow native bridge explains all you need to know to start writing your next iOS, tvOS or MacOS application in Lua.

Post a Comment