Yet Another Class Implementation |
|
Everyone is invited to contribute. Don't hesitate to fork the repo, post issues and suggest changes!
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.
The code can be found here: Files:wiki_insecure/users/jpatte/YaciCode.lua. I'll now describe what you can do with it. Any comments and suggestions are welcome!
This code "exports" only 2 things: the base class 'Object', and a function 'newclass()'.
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
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.
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
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)
Mary = Human("Mary", 20, "New York") Albert = Dog("Albert", 5, Mary)
Besides 'subclass()' and 'new()', each class owns several other methods:
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"
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).
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
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
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
Couple of comments for Lua and/or YaciCode? beginners.
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
dynamicityself.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
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:
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?
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
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 ;-)
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
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
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
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