Celedev object framework for Lua

Celedev Object Framework provides a complete object-oriented architecture for Lua. While using the simple and standard Lua object syntax, Celedev Object Framework is fully interoperable with the Objective-C runtime and objects model and with the highly dynamic nature of the responsive Programming System.

Overview

Celedev Object Framework implements an object model that closely matches the Objective-C model for outstanding integration with iOS and OS X applications. It adds to this model the flexibility and simplicity of Lua.

This object model is classically based on classes and instances. A class inherits its methods and properties from its parent class (no multiple inheritance) and can define additional methods and properties or override methods and properties from its parent.

A Lua class can be defined as a subclass of another Lua class, or it can defined as a subclass of an Objective-C class. From the developer's perspective, this is transparent: there is no difference between Lua-only classes and classes derived from Objective-C, as both kinds of classes behave exactly the same.

A Lua object - instance or class - can transparently be used from the application's Objective-C code, and vice versa: any Objective-C object can be used from the Lua code.

A Lua class can be modified at any time after its creation by adding, modifying or removing methods or properties. This is a key feature that makes Celedev Live Programming system truly dynamic.

Declaring a new class

A class is declared by calling the function class.createClass

class.createClass (className, [superclass, [classEnv]])

Parameters:

  • className: the name of the created class (string)
  • superclass: the parent class of the new class; if not set, the root class LuaObject is used as the superclass
  • classEnv: the environment of the created class (Lua table or class); this is the namespace in which the class name is unique; if not set, the global table class is used

A class declared with class.createClass can be a subclass of a Lua class created with Celedev Object Framework. Or it can be a subclass of an Objective-C class (see Interacting with Objective-C). This makes absolutely no difference: all methods of the superclass are available in the derived class and any of these methods can be overridden if needed. The same is true for properties of the superclass that are available in the subclasses and can be overridden at will.

class.createClass returns the class object corresponding to the class named className in the class environment classEnv. If the class did not exist, a new class is created; if a class with this name did exist, the existing class object is returned. The returned class object is always a cleared class, so after calling class.createClass you have to (re)define every methods or properties of this class, except thoses declared in class extensions (see below).

If you simply want to add or update a method in a Lua class without affecting the other methods of the class, you can just get the class from its environment using classEnv.className and define a new method on the class.

Examples:

-- Create a class named "MyBaseClass" subclassing the root class LuaObject
local MyBaseClass = class.createClass ("MyBaseClass")

-- Create a class named "MyClass" subclassing MyBaseClass
local MyClass = class.createClass ("MyClass", MyBaseClass)

-- Define methods
-- ...

return MyClass
-- Get the class named "MyClass" in the default class environment
local MyClass = class.MyClass

-- Use or modify MyClass
-- ...

Warning: you can not change the superclass of a class after it has been created, so if you call class.createClass multiple times, be sure to always pass the same superclass.

Extending an existing class

A class extension can be defined by calling the function class.extendClass

class.extendClass (extendedClass, [extensionName])

Parameters:

  • extendedClass: the class in which the extension is defined
  • extensionName: the name of the defined extension (string). If no extension name is provided, the empty string is used as extension name.

Class extensions are a way to split the definition of a Lua class across multiple modules in the program. In this perpective, they are similar to Objective-C categories, but with the additional flexibility of live-coding provided by the Celedev system.

A class extension can be used to extend a Lua class created with class.createClass somewhere in the current program. It can also be used to extend in Lua an Objective-C class in order to define additional methods for this class or to re-define in Lua existing methods defined in Objective-C.

For example, let's suppose that you have defined in Objective-C a UIViewController subclass named MyViewController. You can write your view controller methods in Lua with the folowing code:

-- Declare a class extension of the Objective-C class "MyViewController"
local MyViewController = class.extendClass (objc.MyViewController)

-- Define the viewDidLoad method
function MyViewController:viewDidLoad()
    -- Add your code here
end

When you call class.extendClass (aClass, extensionName) an extension for the class aClass named extensionName is created if it didn't exit yet, or cleared from all its existing methods if an extension with the same name was already present.

Methods are associated to a class extension corresponding to the last call to class.extendClass in the module where a method is defined; if class.extendClass has not been called in the current module, methods defined in this module are not considered to be part of any class extension. Basically this means that, when you call class.extendClass, all methods defined later in the same module will belong to this extension.

If for some reason you want to end the declaration scope of an extension, you can call class.endExendClass. Methods declared after this call will not be part of any extension.

class.endExtendClass (extendedClass)

Parameters:

  • extendedClass: the class for which the extension is ended

Warning: You shall not define a Lua method in more than one extension, nor shall you define in an extension a method already defined in a Lua class. Doing so would lead to an undefined behavior regarding which method would be used when the method is called.

However, redefining in Lua a method already defined in objective C is perfectly correct: doing so will simply mask the Objective-C method, that will be replaced by the corresponding Lua method, when called from either Lua or Objective-C. And if you undefine the Lua method by setting it to nil or by calling class.extendClass again, the Objective-C method will be restored!

Declaring methods

Declaring an instance or class method for a Lua class is done by using the standard Lua method definition syntax function aClass:methodName (parameters) ... end, where aClassis a variable containing the class as returned by class.createClass or class.extendClass.

Instance method definition

Instance methods are simply defined on a class by using the : Lua method operator.

For example, this code defines an instance method setBackgroundColor for the class CellClass:

function CellClass:setBackgroundColor (cellIndex, cellCount)
    -- self refers to the instance
    local contentView = self.contentView
    local cellHue = (cellIndex / cellCount) % 1.0
    contentView.backgroundColor = UIColor:colorWithHue_saturation_brightness_alpha (cellHue, 0.3, 0.8, 1)
end

Instance methods are called on instances of this class (or of one of its subclasses).

local aCell = someWayToGetACell() -- contains an instance of CellClass
aCell:setBackgroundColor(3, 20)

Class method definition

To differentiate it from an instance methods, a class methods is declared as a method of a special class field classMethod.

For example, this code defines a class method configure for the class CellClass:

function CellClass.classMethod:configure (scale, orientation)
    -- self refers to the class
    self.configuration = { scale = scale, orientation = orientation }
end

And it is called as methods of the class in which it was declared, or of any of its subclasses.

-- Call the configure method
CellClass:configure (0.2, math.pi / 2)

Calling a superclass implementation of a method

In a truly dynamic language, the superclass implementation of a method cannot be computed at runtime and therefore there can be no such thing as a super keyword. For this reason, the Celedev Object Framework defines a logical and easy-to-use syntax to invoke on a target object the implementation of a method provided by a different class (generally a superclass of this object).

Calling a superclass implementation of a method on an object is done by class-indexing this object by the class providing the method that we want to call, like this:

anObject[targetClass]:method (parameters…)

Where:

  • anObject: the target object
  • targetClass: the class in which the method implementation is provided
  • parameters…: the method parameters

Example:

local MyClass = class.createClass ("MyClass", MyBaseClass) 

function MyClass:doSomeStuff (a, b, c)
    -- Call the superclass version of doSomeStuff
    self[MyBaseClass]:doSomeStuff (a, b, c)

    -- Do specific stuff for MyClass
    -- ...
end

Note: although it is possible to use the class-indexing syntax with any target class, it is a good practice to use it only with an ancestor class of the current object.

Warning! Never use the superclass of the target object as target class of the class-indexing syntax, as this can cause an infinite loop. For example:

local MyClass = class.createClass ("MyClass", MyBaseClass)

function MyClass:doSomeStuff (a, b, c)
     self[self.superclass]:doSomeStuff (a, b, c) -- WRONG!
     -- Do specific stuff for MyClass
     -- ...
end

local MySubclass = class.createClass ("MySubclass", MyClass)

local anInstance = MySubclass:new()

-- This will cause an infinite loop in method doSomeStuff since here self.superclass is MyClass !!!
anInstance:doSomeStuff (1, 2, 3)

Initializers

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

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 anInstance = MyClass:newWithRect (someRect) creates an instance of class MyClass and initialize it by calling method initWithRect(someRect) on the new MyClass instance.

And like any other Lua method, an initializer can also be directly called from Objective-C, as part of a alloc-init sequence.

Initializers in Celedev Object Framework can follow two design patterns: the Objective-C initializer pattern or the simple initializer pattern. You can choose one or the other for your class depending on its behavior.

Objective-C-like initializer

Objective-C-like initializers follow the same pattern as in Objective-C: an initializer method takes a hidden self parameter, like every other method, but it can change the value of self (or set it to nil), and it shall return this - possibly different - self value as its single result.

Typically, the initializer calls a superclass initializer, stores the result in self, performs its specific processing if self is non-nil, and return self at the end.

Example:

local MyClass = class.createClass ("MyClass", MyBaseClass) 

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

    return self;
end

local anInstance = MyClass:newWithRect (someRect)

In this example, MyClass declares an init method initWIthRect. The implementation of the initWithRect method first calls the superclass implementation and store the result in self; then it does any necessary class specific initialization work and finally it returns self. Then the program creates an instance of MyClass by calling newWithRect which is the instance creation method corresponding to the init method initWithRect.

Simple initializer

A simple initializer is a simplified way of writing an initializer that doesn't change the value of self. In this case, the initializer just doesn't return any value. This leads to a simpler and more concise initializer code.

If we know that MyBaseClass:initWithRect does not change self, the initializer above can be rewritten:

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

Simple initializers are fully interchangeable with objective-C-like initializers that do not modify self, meaning that they can also seamlessly be call from Objective-C as init methods.

Finalizers

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

Example:

local MyClass = class.createClass ("MyClass", MyBaseClass) 

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

Likewise it is possible to define a class finalizer as a class method finalize. A class finalizer is called when the owner Lua Context of the class is terminated (i.e. the Lua Context in which the class was previously created). Although rarely used, class finalizer can be needed for releasing application resources reserved at the class level.

Declaring properties

In Celedev Object Framework, a property is similar to a Lua table field and is accessed with the. syntax object.property. Properties do not need to be declared: a property is automatically created when a value is assigned to it in the code. Properties can be defined at the instance or class level.

Example:

local MyClass = class.createClass ("MyClass", MyBaseClass) 

function MyClass:initWithRect (rect)
    -- Call the superclass init method
    self = self:doAsClass ("init", MyBaseClass)

    if self then
        -- Store rect in an instance property
        self.rect = rect
    end

    return self;
end

-- set a class property
MyClass.defaultAngle = math.pi / 3

local anInstance = MyClass:newWithRect (someRect)

print (anInstance.rect.origin.x) -- read the rect property

-- set a specific "innerText" property for this instance
anInstance.innerText = "angle is " .. tostring(self.class.defaultAngle)

Getters and Setters

Getters and Setters can be declared for instance properties. Getters (resp. Setters) are defined using the class method declareGetters (resp. declareSetters).

Getters

declareGetters is defined as:

LuaObject:declareGetters (gettersTable)

Parameters:

  • gettersTable: a Lua table of property getters to be added to the current class. This table contains associations between property names (keys) and getter methods (values). Getter methods are instance methods without parameter; they can be defined as anonymous functions or as method names (i.e. strings).

Example:

-- PinchFlowLayout is a class

function PinchFlowLayout:getRotationAngle ()
    return self._rotationAngle
end

PinchFlowLayout:declareGetters {
    rotationAngle = "getRotationAngle", 
    pinchedCellScale = function (self) return self._pinchedCellScale end,
}

Here the property getter for rotationAngle is a reference to the method getRotationAngle, and the getter for pinchedCellScale is an anonymous function. Note that for anonymous functions, you shall explicitly declare the hidden self parameter!

Setters

Likewise declareSetters is defined as:

LuaObject:declareSetters (settersTable)

Parameters:

  • settersTable: a Lua table of property setters to be added to the current class. This table contains associations between property names (keys) and setter methods (values). Setter methods are instance methods with a single value parameter; they can be defined as anonymous functions or as method names (i.e. strings).

Example:

-- PinchFlowLayout is a class

function PinchFlowLayout:setRotationAngle (angle)
    self._rotationAngle = angle
    -- redraw the layout when the rotation angle is changed
    self:invalidateLayout ()
end

PinchFlowLayout:declareSetters { 
    pinchedCellScale = function (self, scale)
                            self._pinchedCellScale = scale
                            -- redraw the layout when the scale is changed
                            self:invalidateLayout ()
                       end,
    rotationAngle = "setRotationAngle",
}

Here the property setter for rotationAngle is a reference to the method setRotationAngle, and the setter for pinchedCellScale is an anonymous function. Note that for anonymous functions, you shall explicitly declare the hidden self parameter!

Using classes and instances

This section contains additional information about the usage of object instances and classes.

An instance has direct access to the properties and method of its class, as long as no instance property or method is defined with the same name. This can be used for defining default values for instance properties.

Example:

local MyClass = class.createClass ("MyClass", MyBaseClass) 

-- set a class property
MyClass.rotationAngle = math.pi / 3

local anInstance = MyClass:newWithRect (someRect)

print (anInstance.rotationAngle) -- prints the class default value π/3

anInstance.rotationAngle = 1.57

print (anInstance.rotationAngle) -- prints the instance value 1.57

Class properties are inherited by subclasses. Instance getters and setters are inherited by subclasses as well.

Post a Comment