Ana səhifə

Pil programming in Lua

Yüklə 86 Kb.
ölçüsü86 Kb.
Classlib v. 2.03

A multiple-inheritance class library for Lua
This library is based on the general guidelines on OOP found in the PIL (Programming in Lua by Roberto Ierusalimschy) and is also inspired by the class library in, which was taken as a starting point.
It offers a full-featured class system in C++ style, including multiple inheritance of stateful classes (not just interfaces), shared (like C++ virtual) derivation and the correct handling of inheritance and ambiguities.

Class creation
Classes are created by calling class() with the optional list of classes from which the new class derives.
A = class(P, Q)

B = class(P, Q)

C = class(A, B)
Objects or instances are created by calling the class with arguments to the constructor. This is the structure of the generated object:
Cobj = C(...)

P obj

Q obj

Q obj

P obj

B obj

A obj







C obj

An object of type C directly contains two base objects of type A and B, and indirectly two each of types P and Q which are not directly visible from C. The A and B base objects can be accessed from the C object as:
Cobj[A], Cobj[B]
The P and Q objects are not directly accessible from the C object since their references are ambiguous. They can be accessed by their full paths:
Cobj[A][P], Cobj[A][Q], Cobj[B][P], Cobj[B][Q]

Shared derivation
Shared derivation is roughly equivalent to C++ virtual derivation:
X = class(P, shared(Q))

Y = class(P, shared(Q))

Z = class(X, Y)
Zobj = Z(...)

P obj

Q obj

P obj

Y obj

X obj

Z obj








The shared derivation of X and Y from Q results in a single Q base object. This object is visible from the Z object, since its reference is unambiguous. It can be accessed directly or following any of its full paths:
Zobj[Q] == Zobj[X][Q] == Zobj[Y][Q]

Populating the class
When a new class is created by the class() function, it includes attributes (methods and properties) inherited from the base classes according to inheritance rules that will be explained later. After that, the class may be populated by adding new attributes or redefining inherited ones:
Account = class()
function Account:__init(initial)

self.balance = initial or 0

function Account:deposit(amount)

self.balance = self.balance + amount

function Account:withdraw(amount)

self.balance = self.balance - amount

Method __init() is the constructor; it is called when an instance is created and passed all arguments given to the __call() operator of the class. For example,
myAccount = Account(10.00)
creates an account with an initial balance of $10.00.
If __init() is defined, it may call the constructors of the base objects explicitly in order to initialize them:
NamedAccount = class(Account)
function NamedAccount:__init(name, initial)

self[Account]:__init(initial) = name or 'anonymous'

myNamedAccount = NamedAccount('John', 10.00)

The library modifies the user-supplied __init() by wrapping it in such a way that:
1. It will run exactly once per object. This is specially necessary in the case of multiple shared inheritance, when a shared base object may be accessed following more than one path, in order to protect the object against multiple initialization. In our example of shared inheritance given before, the Q object might otherwise be initialized up to three times.
2. Base objects not explicitly initialized by the user-supplied constructor are initialized by default with the same arguments received by the constructor of the derived object. If this is not adequate, base objects should be initialized explicitly.

Named classes and dot scoping
Accessing the base objects by indexing the object with the base class is simple and intuitive. This is the mechanism used by unnamed classes.
The library also supports named classes. Using named classes, a base object is accessed by indexing the derived object not with the base class, but with the name of the base class (a string). This makes the base object appear as a field of the derived object, the field name being the name of the base class. Here is our previous example revisited, where Account is now a named class whose name is precisely 'Account':
function NamedAccount:__init(name, initial)

self.Account:__init(initial) = name or 'anonymous'

myNamedAccount = NamedAccount('John', 10.00)

Note that self[Account] has been replaced by self.Account. This scoping syntax is more in line with the conventions used by other OO languages
There are several ways of giving a name to a class. The first one is to simply assign its __name attribute:
Account = class()

Account.__name = 'Account'

Another way is to pass the class name as a first string argument to class():
Account = class('Account')

NamedAccount = class('NamedAccount', Account)

Here the classes were named after the variables holding them. If these variables are global, there is a shorter way of doing the same thing:
class.Account() -- same as Account = class('Account', ...)


This technique is limited to global variables. For classes living in locals and upvalues, use explicit naming. In the rest of this document we shall assume that classes are named.

Object and class attributes
Object attributes live in their respective objects. The constructor of the Account class, for example, creates a property called balance in the Account object. The constructor of NamedAccount creates a property called name in the NamedAccount object. Object properties created by methods of a certain class never migrate to a derived object; a NamedAccount object does not have a balance in itself, but in its base Account object. In the example above: == 'John'

myNamedAccount.balance == nil

myNamedAccount.Account.balance == 10.00
This scheme avoids name clashes between attributes generated by different classes, keeps classes independent of each other and allows one to derive from classes whose name spaces overlap. It departs from the C++ scheme in the sense that base object attributes are not inherited by the derived objects (in C++ properties of base objects are inherited by the derived object if they are not ambiguous) but there is no other reasonable way given the dynamic nature of variables in Lua: object properties are not defined statically, they come into existence and disappear during program execution.
Following the guidelines of Lua OOP (and also departing from C++), this library makes class attributes default values for object attributes. This feature comes from the fact that classes are metatables of their objects, and helps get rid of constructors whose sole purpose is assigning initial default values. For example, if an Account were to be created always with an initial zero balance, instead of defining a constructor one could simply declare:
Account.balance = 0
myAccount = Account()
Before the balance is assigned to for the first time, myAccount.balance refers to the class property and reads zero. On the first deposit, however, the Account:deposit() method creates a balance property in the Account object (it does not modify the class property) and thereafter myAccount.balance refers to the object property. If sometime later the user assigns myAccount.balance = nil, the object property disappears and myAccount.balance refers to the class property again. This behavior must be taken into account if class properties are used as default values: assigning nil to an object property defaulted by a class property seems to actually assign it the default value.
Class properties can also be used as class global variables (as in C++) and assigned to, but in that case they must be accessed directly and not through the objects. For example, by making the direct assignment
Account.balance = 4.50
all accounts created thereafter will have an initial balance of $4.50.
Class functions (i.e. those functions that are not methods) should also be called directly, never through the objects. This is another departure from C++, which accepts both ways of calling a static or class method as equivalent. See the following section for an explanation why this is not so with this library.

Taking the NamedAccount example, we can make a deposit by calling
But the deposit() method is inherited by NamedAccount from Account, so we can simply say
and we get exactly the same result.
Inheritance rules are simple: the first rule says that an attribute is inherited from a base class if there is no ambiguity. Attributes that have the same name in different bases are not inherited. Multiple inheritances of the same attribute from the same base are ambiguous unless they originate in a shared base, the reason being that unshared base objects of the same class have their own separate states and methods accessing their states must be called with a full path to make sure that they receive the correct self.
For example:


class.CombinedAccount(SavingsAccount, CurrentAccount)
myCombinedAccount = CombinedAccount()

myCombinedAccount:deposit(2.00) ← Error, deposit is nil

The deposit()method of the Account class is inherited by both SavingsAccount and CurrentAccount, but not by CombinedAccount since it would be ambiguous as the Account base is not shared. These are correct:


Both will increment myCombinedAccount.CurrentAccount.Account.balance by $2.00.
There would exist a CombinedAccount:deposit() method if SavingsAccount and CurrentAccount were derived from shared(Account). That would create a single Account base object and a single balance and would avoid the ambiguity, although it would certainly not make any sense in this particular application.
The second inheritance rule says that when methods are inherited, they are not merely copied from the base class to the derived class, but modified (wrapped) so that they call the original methods with self values corresponding to the base objects of their class.
Recall that these two operations were reported to produce identical results:
myNamedAccount.Account:deposit(2.00) (1)

myNamedAccount:deposit(2.00) (2)

The original deposit()belongs to the Account class. In (1) the original method is called with myNamedAccount.Account (the Account object) as self. In (2) the inherited version is called with myNamedAccount (the NamedAccount object) as self. This inherited version in turn calls the original one passing it self.Account as self. The runtime cost of this wrapping mechanism is indexing on self and calling a function, no matter how many levels of derivation are involved.
When a class is created, the derivation process wraps all inherited functions assuming that they are methods, since there is no way to tell. The user is expected to avoid calling functions that are not methods as if they were; class functions should be called only directly, never through the objects. The only cost of wrapping class functions, then, is to generate some (small) useless code.

Metamethods and indexing
Classes may freely define metamethods like __call(), __add(), __mul(), etc. They behave like any other method and are inherited if they are not ambiguous.
Indexing, however, is special. The methametods controlling indexing (__index() and __newindex()) cannot be defined by the user directly, since that would dismantle the whole class machinery.
The class library supports indexing control by letting users define two special methods: __get() and __set(). These methods are not meant to be called directly (they are internally renamed) but if they are defined they are called when indexing operations (square bracket or dot-field accesses) are performed on an instance of a class.
Classes are metatables of their instances. Normally, a class C has:
__index == C

__newindex == nil

When __get() is defined, an __index() handler is installed. This handler is called every time an attempt is made to read a missing object attribute. The handler first looks for the attribute in the class. If it finds it there, it just returns it. If the attribute is not found in the class, the handler calls __get() with the name of the attribute, and returns whatever __get(name) returns.
When __set() is defined, a __newindex() handler is installed. This handler is called every time an attempt is made to assign a value to a missing object attribute. The handler starts by calling __set() with the name and value of the attribute. If __set(name, value) returns true, this indicates that __set() has handled the assignment and there is nothing else to do. Otherwise, the assignment was not handled and the handler creates the attribute directly in the main body of the object.
This mechanism is useful for defining classes that behave like arrays or tables and keep the elements separate from the main body of the object. Since the elements are not found directly in the object, __get() and __set() are called on every access. One can also think of applications where objects are proxies and the elements are stored remotely and accessed through a communications interface.
As an example, consider this excerpt from a Tuple class that behaves like a contiguous array indexed by integers from 1 to the current size plus one. It has two fields: a table elem for holding the elements and the current size n. Element [n+1] is a sentinel, reading it returns nil and assigning it increases the size by one.
function Tuple:__get(i)

if type(i) ~= 'number' then return nil end

assert(i >= 1 and i <= self.n + 1, 'Invalid index')

return self.elem[i]

function Tuple:__set(i, v)

if type(i) ~= 'number' then return false end

assert(i >= 1 and i <= self.n + 1, 'Invalid index')

if v == nil then --remove

if i <= self.n then

table.remove(self.elem, i)

self.n = self.n - 1


return true


if i == self.n + 1 then self.n = i end --append

self.elem[i] = v

return true

Note how __get() and __set() carefully reject non-numeric indexes by returning nil and false respectively. These methods must be written with great care so that they do not re-enter indefinitely. Use rawget() and rawset() when necessary.

This class can be used as follows:
t = Tuple(11, 22, 33)

= t --> (11, 22, 33)

= t[2] --> 22

t[2] = nil

= t --> (11, 33)

t[3] = 333

= t --> (11, 33, 333)

t[5] = 55 --> Error, invalid index

The indexing hooks have a cost in terms of performance, but it is only paid by classes that define the __get() and __set() methods. Other classes are not affected. If a class derives from an indexing base but does not need the indexing operations, it may just define __get = nil and __set = nil; this will remove the indexing hooks.

The library is implemented in a single file, classlib.lua. Public interface:
class([name,][bases]) Creates a new class that derives from zero or more classes. If name exists it creates a named class, otherwise an unnamed class; bases is a list of classes or values returned by shared().[bases]) Creates a new named class and assigns it to a global variable with the same name: is the same as name = class('name', bases) where name is global.
shared(class) Wraps a class so that it can be passed as an argument to class() for shared derivation.
There is a version of the library that only supports unnamed classes and square-bracket scoping, unclasslib.lua. If named classes are not used this version should be a bit more efficient, since it eliminates one level of indexing when accessing the base objects. Classes are created by class([bases]). No name argument is allowed, and assigning the __name attribute of a class has no effect.
There is also a group of utility functions for checking types and interface support:
typeof(v) Returns 'object' if v is an object, 'class' if it is a class, type(v) otherwise.
classof(v) Returns the class of v if v is an object or a class, nil otherwise.
classname(v) Returns the name of the class of v if v is an object or a class and the class is named, nil otherwise.
implements(v, class) True if v is an object or a class that implements the interface of class.
is_a(v, class) True if v is an object or a class of class class or derives from class and implements its interface.
When a class is created, it has a few initial methods:
__call(...) Calling the class creates an instance. Arguments are passed to the constructor.
__init(...) Default constructor.
implements(class) Method to determine if an object or class implements the interface of a target class.
is_a(class) Method to determine if an object or class is of the type of a target class.
The is_a() and implements() methods are somehow special since they can be applied to both classes and objects.
The implements() method checks that an object or class supports the interface of the target class. The interface of a class is the set of its callable attributes (functions and objects with a __call() operator). The method merely checks that these attributes exist and are callable; certainly this does not guarantee that they are semantically correct.
The is_a() method checks whether the object or class is of the same class as the target class or derives from the target and implements it. For example,
myNamedAccount:is_a(Account) == true

myCombinedAccount:is_a(Account) == false

The last one fails because although CombinedAccount derives from Account, it does not support its deposit() and withdraw() methods since they are ambiguous. This means that a CombinedAccount cannot be passed to a function expecting an Account, since that function could blindly attempt to call those methods.

Reserved names
The implementation of the class library reserves some attribute names for internal use:
__index __newindex __type

__class __bases __shared __inherited __from __user_init __initialized __user_get __user_set

Please leave these alone or just avoid using names starting with two underscores, with the exception of __name, __init(), __get(), __set() and the metamethods like __call(), __add(), etc. which may be safely defined.

Verilənlər bazası müəlliflik hüququ ilə müdafiə olunur © 2016
rəhbərliyinə müraciət