Yet Another Class Implementation

lua-users home
wiki

The code can be found here: https://github.com/jpatte/yaci.lua.

Everyone is invited to contribute. Don't hesitate to fork the repo, post issues and suggest changes!

Introduction

This isn't certainly quite original, but I thought it could be useful for some other Lua users or fans. I've seen several implementations for classes which suggest how to use metatables to simulate object oriented aspects like intanciation or inheritance etc (see for example ObjectOrientationTutorial, LuaClassesWithMetatable, InheritanceTutorial, ClassesAndMethodsExample and SimpleLuaClasses), but I thought it should be possible to go even further than that, by adding some additional features and facilities. This is why I suggest here yet another implementation, which is mainly based on the other ones, but with some additional stuff in it. I don't pretend it to be the best way, but I think it could be useful for some other persons than me, thus I wanted to share it here ;)

Note that this code has been designed to be as comfortable as possible to use; therefore this is certainly not the fastest way of doing things. It is rather complicated, it makes intensive use of metatables, upvalues, proxies... I tried to optimize it a lot but I'm not an expert thus maybe there were some shortcuts that I didn't know yet.

I'll now describe what you can do with it. Any comments and suggestions are welcome!

Features

Content

This code "exports" only 2 things: the base class 'Object', and a function 'newclass()'.

Class Definition

There are basically 2 ways of defining a new class: by calling 'newclass()' or by using 'Object:subclass()'. Those functions return the new class.

When creating a class you should specify a name for it. This is not absolutely required, but it could be helpful (for debugging purposes etc). If you don't give any name the class will be called "Unnamed". Having several unnamed classes is not a problem.

When you use 'Object:subclass()', the new class will be a direct subclass of 'Object'. However 'newclass()' accepts a second argument, which can be another superclass than 'Object' (If you don't give any class, the class 'Object' will be chosen; that means that all classes are subclasses of 'Object'). Note that each class has the 'subclass()' method, thus you could use it too.

Let's take an example to illustrate this:

-- 'LivingBeing' is a subclass of 'Object'
LivingBeing = newclass("LivingBeing")

-- 'Animal' is a subclass of 'LivingBeing'
Animal = newclass("Animal", LivingBeing)

-- 'Vegetable' is another subclass of 'LivingBeing'
Vegetable = LivingBeing:subclass("Vegetable")

Dog = newclass("Dog", Animal)   -- create some other classes...
Cat = Animal:subclass("Cat")
Human = Animal:subclass("Human")

Tree = newclass("Tree", Vegetable)

Note that the exact code of 'newclass()' is

function newClass(name, baseClass)
 baseClass = baseClass or Object
 return baseClass:subClass(name)
end
It was just added for convenience.

Methods Definition

Methods are created in a rather natural way:

function Animal:eat()
  print "An animal is eating..."
end

function Animal:speak()
  print "An animal is speaking..."
end

function Dog:eat()
  print "A dog is eating..."
end

function Dog:speak()
  print "Wah, wah!"
end

function Cat:speak()
  print "Meoow!"
end

function Human:speak()
  print "Hello!"
end 

The method 'init()' is considered as a constructor. Thus:

function Animal:init(name, age)
  self.name = name
  self.age = age
end

function Dog:init(name, age, master)
  self.super:init(name, age)   -- notice call to superclass's constructor
  self.master = master
end

function Cat:init(name, age)
  self.super:init(name, age)
end

function Human:init(name, age, city)
  self.super:init(name, age)
  self.city = city
end

Subclasses may call the constructor of their superclass through the field 'super' (See below). Note that 'Object:init()' exists but does nothing, so it is not required to call it.

Events Definition

You may also define events for the class instances, exactly in the same way as for the methods:

function Animal:__tostring()
  return "An animal called " .. self.name .. " and aged " .. self.age 
end

function Human:__tostring()
  return "A human called " .. self.name .. " and aged " .. self.age .. ",
         living at " .. self.city
end
Any events could be used, excepted '__index' and '__newindex' which are needed for OO implementation. You can use this feature to define operators like '__add', '__eq' etc. '__tostring' is a really useful event here, therefore the class 'Object' implements a default version for it, which simply returns a string "a xxx" where 'xxx' is the name of the instance's class.

Instance Creation

Each class has a method 'new()', used for instanciation. All arguments are forwarded to the instance's constructor.

Robert = Human:new("Robert", 35, "London")
Garfield = Cat:new("Garfield",  18)
The result is the same if you "call" the classes directly:
Mary = Human("Mary", 20, "New York")
Albert = Dog("Albert", 5, Mary)

Classes services

Besides 'subclass()' and 'new()', each class owns several other methods:

Instances services

Every instances permit access to the variables defined in the constructor of their class (and of their superclasses). They also have a 'class()' method returning their class, and a field 'super' used to access the superclass's members if you overrode it. For example:

A = newclass("A")
function A:test() print(self.a) end
function A:init(a) self.a = a end
B = newclass("B", A)
function B:test() print(self.a .. "+" .. self.b) end
function B:init(b) self.super:init(5) self.b = b end

b = B:new(3)
b:test()   -- prints "5+3"
b.super:test()   -- prints "5"
print(b.a)   -- prints "5"
print(b.super.a)   -- prints "5"
The superclass's members are created (and initialized) when the "self.super:init()" method is called. You should generally call this method at the beginning of the constructor to initialize them.

Note that as 'b' is an instance of 'B', 'b.super' is simply an instance of 'A' (So be careful, here 'super' is dynamic, not static).

Static variables

Each time you define a new method for a class, it goes in a "static" table (this way we cannot mix class methods with class services). This table is accessible through a 'static' field. This is mainly done to permit access to static variables in classes. Example:

A = newclass("A")
function A:init(a) self.a = a end
A.test = 5   -- a static variable in A

a = A(3)
prints(a.a)   -- prints 3
prints(a.test)   -- prints 5
prints(A.test)   -- prints nil (!)
prints(A.static.test)   -- prints 5

The end

Whew - I think that's all. :) Again, any remarks and comments will be appreciated. But this is my first submission here, so don't smash me too hard :D -- Julien Patte, 19 Jan 2006 (julien.patte AT gmail DOT com)

Last minute note: I just discovered SimpleLuaClasses, which I didn't see before. I was amazed (and happy) that there were so many resemblances between our implementations, at least in the way people would use it. However, here only events must be copied for inheritance, each instance holds an instance of its superclass, and there are some other additional details.

Im still not fully satisfied by this code. Even instances variables (other than functions) are "virtual" here, and that's a huge problem, since it could bring some weird bugs if superclasses and subclasses use some variables with the same name. But I guess it can't be easily helped :/ -- Julien Patte

Bugfix

I've found a subtle but confusing bug: instance variables are usually virtual ('protected' in C++-speak) except for some particular situations involving calling overridden superclass functions from inside subclass functions, for example when B:Update() calls self.super:Update() (ie A:Update()) internally. This can leave multiple variables of the same name but with different values scattered across several levels of inheritance, which tends to break things, be confusing, and generally make one unhappy.

The fix for this is to add the following:

function c_istuff.__newindex(inst,key,value)
  if inst.super[key] ~= nil then inst.super[key] = value;
  else rawset(inst,key,value); end
end

just after function c_istuff.__index(inst,key) ... end inside the subClass function on Julien's otherwise very clever code. And now it should all work. Be careful though, as fixing this might break things that rely on it being broken -- specifically, situations where superclass variable names are inadvertently reused on subclasses.

-- Damian Stewart (damian AT frey DOT co DOT nz), 6 Oct 2006

Beginner's notes

Couple of comments for Lua and/or YaciCode? beginners.

Instance variables

You must define all instance variables of a class in the init() method, using false for anything you'd like to be nil. This has to do with how Lua manages tables and the method used by YaciCode? to provide inheritance, and is related to the bug above as well. If you fail to do this, you'll have surprises calling overridden superclass functions from inside subclass functions.

A = newclass("A")

function A:init(a_data)
  self.a_data = a_data
  self.foo = nil
end

function A:setFoo(foo)
  self.foo = foo
end

function A:free()
  if self.foo then self.foo = nil end
end

B = A:subclass("B")

function B:init(a_data, b_data)
  self.super:init(a_data)
  self.b_data = b_data
  self.b_table = {'some', 'values', 'here'}
end

function B:free()
  self.b_table = nil
  self.super:free()
  if self.foo then print("self.foo still exists!!!") end
end

-- and now some calls
myA = A:new("a_data")
myB = B:new("a_data2", "b_data")

myB:setFoo({'some', 'more', 'values'})

myB:free()
-- will print "self.foo still exists" !!!

myB:setFoo() calls A.setFoo(myB) (i.e., self is myB). myB.foo does not exist in myB or higher in the hierarchy, so the foo key is added to the myB table. When freeing, B.free(myB) is called (self is myB). self:super:free() calls A.free(self.super), i.e. A.free is not called with myB, but with myB.super, a pseudo-object of class A maintained by YaciCode?, which has no foo instance variable! The thing is, self.foo = nil had no side effect whatsoever in A:init(). It did not create a foo instance variable.

If you do self.foo=false, however, it does create a foo instance variable and when myB:setFoo() calls A.setFoo(myB), myB.foo does not exist but it does higher in the hierarchy (with value false) and in this case, it ends up replaced by the foo function parameter. The nice thing with false is that is makes test like if self.foo then work the same if self.foo is nil or false.

self.super dynamicity

The text above mentions self.super is dynamic. What it means in practice is that you should probably define functions at each level of the hierarchy if the function calls its superclass. Consider:

A = newclass("A")

function A;init(...) ... end

function A:free()
  print("A:free()")
end

B = A:subclass("B")

function B:init(...) ... end

function B:free()
  self.super:free()
  print("B:free()")
end

C = B:subclass("C")

function C:init(...) ... end
-- Note C has no "free" method

-- code
myC = C:new()

myC:free()
-- prints:
B:free()
B:free()
A:free()
-- i.e. B:free is called **twice**

What happens is that myC:free() is C.free(myC). Since C has no free method, but B has one, what ends up called is B.free(myC). In this function we do self.super:free(), which is really myC.super.free(myC.super). And it turns out myC.super.free is (again) B.free so what is called is really B.free(myC.super) and B.free ends up being called twice, once with the original object as a parameter, and then once with the "pseudo" superclass object YaciCode? maintains behind the scenes.

This could have unwanted side effects, so it's probably best to define explicitly C.free(), if only just to do self.super:free()...

Note the same happens with init() but since all classes define it, there is no such side effect.

-- Frederic Thomas (fred AT thomascorner DOT com), 22 Feb 2007

New version 1.2

Well, Frederic has pointed out a very annoying problem...

Basically one would like to say this: "Currently, if B is a subclass of A, and myB is an instance of B, myB:foo() is equivalent to A.foo(myB) if foo() is defined in A and not in B; altough what we need is an equivalent form A.foo(myB.super) instead". This sounds right, because the two bugs Frederic's just mentioned are a direct consequence of that fact. This change isn't really difficult to make, and the two sample codes above can work correctly just by replacing the instances' __index metamethod by something a little more sophisticated.

However... What about virtual? What if one wants to call a virtual method bar() defined in B from the method foo() defined in A? If we applied this change, this kind of things wouldn't be possible anymore because there wouldn't be any access to the virtual fields in B from the methods defined in A -- as it is the case now because foo() receives myB as argument instead of myB.super, and thus it can access the methods defined at B's level.

Here is an example to illustrate this:

A = newclass("A")

function A:whoami()
  return "A"
end

function A:test()
  print(self:whoami())
end

B = newclass("B", A)

function B:whoami()		-- is it a virtual function?
  return "B"
end

myB = B()
myB:test() -- what should be printed here? "A" or "B"?

Java users e.g would like to see "B" (because methods are virtual by default in Java), although C++ users would prefer to see "A" because whoami() isn't declared as "virtual". And the problem is: there is no keyword "virtual" nor "final" in Lua. So what would be the best behaviour?

While writing the code I thought that all methods should be virtual by default, and this is why I organized things in this way. But the bugs Frederic's reported are too important to be acceptable, in my opinion.

Thus I wrote a new version of "YaciCode?" where these bugs are fixed, where default virtual is disabled, and with some new class functions to provide virtual methods and casting functionalities. The new code can be found here: Files:wiki_insecure/users/jpatte/YaciCode12.lua . If possible I'd like to have people approbation before editing my notes above to add explanations about the new functionalities. Every tests I did ran fine, but I could have missed something; and again, any comments are appreciated if you see a "more suitable way" of doing things :-)

Here is a couple of notes about the major changes in this version:

Internal reorganization

In order to manage castings and explicit virtual I had to add several things to the code, and in particular a weak table metaObj that associates an instance object with its meta-information (which is not visible by the user). These informations concern the object's class, its "superobject", its "lowerobject", etc. This was mainly needed for casting implementation: casting back myB.super into a B instance (i.e, myB itself) is now possible because there is a link from myB.super to myB in the meta-informations. This table could be used to store any other information about each instance in future versions.

The __index metamethod for class instances is a little more complex than before, in order to transform myB:foo(myB) into A.foo(myB.super) instead of A.foo(myB) if foo() is defined at A's level; this "simple" change fixes the two bugs that Frederic mentioned above.

The classes may have some meta-informations too, and in particular they maintain a list of their virtual methods. Every time an instance is created, the "virtual table" is directly copied into the instance (and into all its "superinstances"). That means that virtual methods have a higher priority than the simple methods declared by the class (and this is exactly what we want: if A and B define the virtual method foo(), B.foo must have a higher priority than A.foo() at each level of the hierarchy).

By the way, as there is a Object class, I'm considering the introduction of a Class class. Any classes would be instances of Class; e.g. one should write A = Class:new("A") or A = Class("A") instead of A = newclass("A"). This isn't difficult to implement and would bring more "homogeneity" to the code. What's your opinion about it?

Virtual

Here we are. As I said, virtual is now disabled by default (this is due to the new __index metamethod). In the example code I gave about the whoami() function, the current implementation would print "A", because A:test() receives myB.super as self instead of myB. But what if we want to make whoami() virtual? In other terms, how could we override A:whoami() with B:whoami(), even at A's level (and only for B's instances)? Well, you just have to write A:virtual("whoami") to explicitly declare whoami() as virtual. This must be written outside any method, and after the method definition. Thus:

A = newclass("A")

function A:whoami()
  return "A"
end
A:virtual("whoami") -- whoami() is declared virtual

function A:test()
  print(self:whoami())
end

B = newclass("B", A)

function B:whoami() -- now yes, whoami() is virtual
  return "B"
end
					-- no need to declare it again
myB = B()
myB:test() -- will print "B"

It is also possible to declare some methods as abstract (i.e. pure virtual methods); you just have to call A:virtual() with the name of the abstract method without defining it. An error will be raised if you try to call it without having defined it lower in the hierarchy. Here is an example:

A = newclass("A")

A:virtual("whoami") -- whoami() is abstract

function A:test()
  print(self:whoami())
end

B = newclass("B", A)

function B:whoami() -- define whoami() here
  return "B"
end
					
myB = B()
myB:test() -- will print "B"

myA = A()  -- no error here! 
myA:test() -- but will raise an error here

"Protected" and "private" attributes

Damian wrote here: "This can leave multiple variables of the same name but with different values scattered across several levels of inheritance, which tends to break things, be confusing, and generally make one unhappy."

Well, personally I tend to think the opposite. IMHO the encapsulation principle should also be applied between a class and its subclasses; that means that an instance of a subclass should have no knowledge of the attributes declared in its superclasses. It may have access to some methods and services provided by the superclasses, but it should not know how these services are implemented. This is the parent's business, not the child's business. In practice, I would say that every attributes in a class should be declared as "private": if a class and its subclass use an attribute of the same name for their respective business, there should be no interference between them. And if the implementation of the superclass' services has to change, there must only be a minimal impact on the subclasses, and this is mainly possible because the subclasses do not know what are the exact attributes used at the higher levels.

These are two opposite opinions, and it's really difficult (impossible?) to tell who's right and who's wrong. So the best thing we could say is probably "Let the user decide what he wants to do" :-)

It is now possible to define "protected" and "private" attributes in a class, depending on the order these attributes are initialized. Note that "protected" and "private" aren't the best terms here (because there is no real protection mechanism), we should rather talk about "shared" and "non shared" attributes between a class and its subclasses. You will also note that this distinction is made by the subclass itself (and not by the superclass), which can decide (in its constructor) if certain attributes of the superclass should be shared or overridden.

Consider the following example:

A = newclass("A")

function A:init(x)
  self.x = x
  self.y = 1  -- attribute 'y' is for internal use only
end

function A:setY_A(y)
  self.y = y
end

function A:setX(x)
  self.x = x
end

function A:doYourJob()
  self.x = 0   -- change attributes values
  self.y = 0
  -- do something here...
end


B = A:subclass("B")

function B:init(x,y)
  self.y = y              -- B wants to have its own 'y' attribute (independant from A.y)
  self.super:init(x)      -- initialise A.x (and A.y)
                          -- x is shared between A and B
end

function B:setY(y)
  self.y = y
end

function B:setY_B(y)
  self.y = y
end

function B:doYourJob()
  self.x = 5
  self.y = 5
  self.super:doYourJob()  -- look at A:doYourJob
  print(self.x)           -- prints "0": B.x has been modified by A
  print(self.y)           -- prints "5": B.y remains (safely) unchanged
end


myB = B(3,4)
print(myB.x, myB.y, myB.super.x, myB.super.y) -- prints "3 4 3 1"

myB:setX(5)
myB:setY(6)
print(myB.x, myB.y, myB.super.x, myB.super.y) -- prints "5 6 5 1"

myB:setY_A(7)
myB:setY_B(8)
print(myB.x, myB.y, myB.super.x, myB.super.y) -- prints "5 8 5 7"

myB:doYourJob()

You can see that the different behaviours of the attributes 'x' and 'y' come from the order of initialisation in the constructor. The "first" class that defines an attribute will get possession of that attribute, even if some superclasses declare an attribute with the same name "later" in the initialisation process. I personnaly suggest to initialise all "non shared" attributes at the beginning of the constructor, then call the superclass' constructor, then eventually use some of the superclass' methods. On the contrary if you want to access an attribute defined by a superclass, you may not set its value before the superclass' constructor has done it.

I hope this solution will be suitable for everyone ;-)

Castings

Now comes another problem: by transforming myB:foo() into A.foo(myB.super), a part of information about myB is "lost". foo() is at A's level; but what if we want to access from foo() some specific (non virtual) methods/attributes defined at B's level? The answer is: we should be able to cast myB.super "back" to myB.

This can be done with two new class methods: cast() and trycast(). A simple example is...

A = newclass("A")

function A:foo()
  print(self.b) -- prints "nil"! There is no field 'b' at A's level
  aB = B:cast(self)  -- explicit casting to a B
  print(aB.b)  -- prints "5"
end

B = newclass("B",A)

function B:init(b) 
	self.b = b
end

myB = B(5)
myB:foo()

C:cast(x) tries to find the "sub-objet" or "super-object" in 'x' corresponding to the class C, by searching higher and lower in the hierarchy. Intuitively, we will have myB.super == A:cast(myB) and myB == B:cast(myB.super). Of course this works with more than 2 levels of inheritance. If the casting fails, an error will be raised.

C:trycast(x) does exactly the same, except that it simply returns nil when casting is impossible instead of raising an error. C:made(x), which was already existing, has been modified and returns now true if C:trycast(x) does not return nil i.e, if casting is possible.

Let's take another example:

A = newclass("A")
function A:asA() return self end

B = newclass("B",A)
function B:asB() return self end

C = newclass("C",B)

D = newclass("D",A) -- subclass of A

a, b, c, d = A(), B(), C(), D()

b_asA = b:asA()
c_asA = c:asA()
c_asB = c:asB()

print( A:made(c) ) -- true
print( A:made(d) ) -- true

print( B:made(a) ) -- false
print( B:made(c) ) -- true
print( B:made(d) ) -- false

print( C:made(b) ) -- false
print( C:made(c) ) -- true
print( C:made(d) ) -- false

print( D:made(d) ) -- true
print( D:made(a) ) -- false

print( b_asA:class() , B:made(b_asA) ) -- class A, true

print( c_asA:class() , C:made(c_asA) ) -- class A, true
print( c_asB:class() , C:made(c_asB) ) -- class B, true

print( c:asA() == c.super.super ) -- true
print( C:cast( c:asA() ) == c ) -- true

And a last one (it isn't really a good practice to write things like that, but it's still a good example for casting operations):

A = newclass("A")

function A:printAttr() 
  local s
  if B:made(self) then s = B:cast(self) print(s.b)
  elseif C:made(self) then s = C:cast(self) print(s.c)
  elseif D:made(self) then s = D:cast(self) print(s.d)
  end
end 

B = newclass("B",A) 
function B:init() self.b = 2 end

C = newclass("C",A) 
function C:init() self.c = 3 end

D = newclass("D",A) 
function D:init() self.d = 4 end

manyA = { C(), B(), D(), B(), D(), C(), C(), B(), D() }

for _, a in ipairs(manyA) do
  a:printAttr()
end

Comments

Here was a description of the changes introduced by the new version 1.2; I hope these improvements will be helpful. Please don't hesitate to give a feedback, if you think one could do even better or if you find a bug somewhere ;-)

The new version is available at Files:wiki_insecure/users/jpatte/YaciCode12.lua , if possible I'd like to receive some comments on this version before updating the whole page. Thanks a lot for your interest!

-- Julien Patte (julien.patte AT gmail DOT com), 25 Feb 2007

Bugfix

Peter Bohac reported a bug in version 1.2 about the class() method. As a side effect the default __tostring metamethod (which uses this method) raises an error when an instance is "printed". The bugfix is rather simple:

1 - at line 149:

function inst_stuff.class() return theclass end
it should be "theClass" instead of "theclass"...

2 - after line 202, there should be a definition of the Object:class() method:

obj_inst_stuff.__newindex = obj_newitem
function obj_inst_stuff.class() return Object end

This bug has been fixed in the file YaciCode12.lua.

-- Julien Patte (julien.patte AT gmail DOT com), 19 Mar 2007

See Also


RecentChanges · preferences
edit · history
Last edited June 14, 2023 6:57 pm GMT (diff)