blog.poucet.org Rotating Header Image

A simple OO-system in Lua

Well, I recently (very recently) started to play around with Lua. I ordered the book before leaving to Colombia and found it when I was back. It took me just a day to read it, as the book is a light read and rather well-written, covering all the important topics.

One reason I’ve started to look at Lua is because it seems like a rather powerful language. Read more if you’re interested in why, and a first OO-system that I’ve rolled in it.


Powerful is a rather abstract word, and perhaps I should first explain why I consider Lua, powerful, especially since I come from a rather different programming language sphere (Haskell).
The reason that lua appeals to me is that it seems to enable a way of meta-programming that cuts cleanly through your code. It takes a scheme-approach of being built-up in an orthogonal manner with a small core. That being said, I find it more appealing than scheme as, at least to me, it is easier to code things in. The meta-programming seems to be more powerful than in scheme.

Anyways, I started coding in it yesterday, wanting to roll an OO-system that would be easy to use. Additionally, I wanted a way to nicely dump my state and then be able to reload it after potentially modifying some of the classes I use.

For the serialization aspect, I took the serialization scheme from the Lua Book and extended it to deal with custom serialization meta-methods as well as the possibilities of keys being tables. Note that it’s not yet finished, it need a small piece of code to generate unique names, but that’s rather trivial.

As for the OO system, this is a second version. My original version was rather primitive. One thing that I wanted was the ability to call the static-super method of a method. Now a simple way would be:


function Class:method(...)
SuperClass.method(self, ....)
end

I did not like this idea, however, as it requires explicitly referring to the super-class. Certainly, for simple class-trees this is a viable option. However, since I would like to introduce the concept of mixins, and in general don’t like the verbositoy of this, I introduced a ‘static’ way of defining __super (The super-method). Basically, whenever a method is defined, it’s function-environment is modified to have a link to __super (as well as __class). The reason for having a link to __class is that this is the static-class that the method belongs to, not the class of the object.

The (unfinished) code can be seen below. It is based on ideas from: ClassesAndMethodsExample and InheritanceTutorial. As mentioned before, this is only my second day of hacking Lua, so I’m certain there are things that could be done much better. Suggestions, as always, welcome


local setmetatable = setmetatable
--------------------------------------------------------------------------------
-- Helper functions:
--------------------------------------------------------------------------------
function memoize(f)
local self = setmetatable({}, {__mode = "k"})
self.__index = function(self, k) local v = f(k); self[k] = v return v end
self.__call = function(self, k) return self[k] end
return self
end

--------------------------------------------------------------------------------
-- Class System
--------------------------------------------------------------------------------
-- Methods should be called on the klass object with as parameter the object itself!
--
Root = {
super = nil;
name = "Root";
new =
function(class, ...)
local obj = {class = class}
local meta = {
__index = class.methods,
__serialize = class.__serialize or Root.__serialize
}
setmetatable(obj, meta)
if (class.methods.init) then
class.methods.init(obj, ...)
end
return obj
end;
methods = {
classname = function(self)
return (self.class.name)
end;
isa = function(self, aClass)
local cur_class = self.class
while (nil ~= cur_class) do
if cur_class == aClass then
return true
else
cur_class = cur_class.super
end
end
return false
end
};
data = {};
__serialize = function(self, serializer, name)
serializer:write("setmetatable({class = " .. self:classname() .. "}, {\n")
serializer:write(" __index = " .. self:classname() .. ".methods,\n")
serializer:write(" __serialize = " .. self:classname() .. ".__serialize or Root.__serialize\n")
serializer:write("})\n")
for k,v in pairs(self) do -- save its fields
if k ~= "class" then
local kname
if serializer:isBasic(k) then
kname = serializer:b asicSerialize(k)
elseif type(k) == "table" then
if saved[k] then
kname = saved[k]
else
kname = serializer:generateName()
serializer:serialize(kname, k)
end
end
local fname = string.format("%s[%s]", name, kname)
serializer:serialize(fname, v)
end
end
end
}

function Class(name, super)
super = super or Root
class = {
super = super,
name = name,
methods = {}
}

-- if class slot unavailable, check super class
-- if applied to argument, pass it to the class method new
setmetatable(class, {
__index = super,
__call = function(self,...) return self:new(...) end
})

setmetatable(class.methods, {
-- if instance method unavailable, check method slot in super class
__index = class.super.methods,
-- when defining a new method, set the environment for proper, static, class access and super-method access
__newindex = function (c, m, f)
if (class.super.methods[m]) then
setfenv(f, setmetatable({__class = class, __super = class.super.methods[m]}, {__index = getfenv(f) }))
else
setfenv(f, setmetatable({__class = class}, {__index = getfenv(f) }))
end
rawset(c,m,f)
end
})

return class
end

function Mixin(name, mixin, super)
-- return (name, nil, super)
end

function Singleton(name, super)
end

--------------------------------------------------------------------------------
-- Serializer
-- TODO: Make a way to easily define what attributes -not- to save
--------------------------------------------------------------------------------
Serializer = Class("Serializer")
function Serializer.methods:init(stream, temp, saved)
self.stream = stream or io.stdout
self.temp = temp or 0
self.saved = saved or {}
end

function Serializer.methods:basicSerialize(value)
if type(value) == "boolean" then
return tostring(value)
elseif type(value) == "number" then
return tostring(value)
elseif type(value) == "string" then
return string.format("%q", value)
end
end

function Serializer.methods:isBasic(value)
return type(value) == "boolean" or type(value) == "number" or type(value) == "string"
end

function Serializer.methods:write(...)
self.stream:write(...)
end

function Serializer.methods:serialize(name, value)
self:write(name, " = ")
if self:isBasic(value) then
self:write(self:basicSerialize(value), "\n")
elseif type(value) == "table" then
if self.saved[value] then -- value already saved?
self:write(self.saved[value], "\n") -- use its previous value
else
self.saved[value] = name -- save name for next time
serializer = (getmetatable(value) or self).__serialize or self.__serialize
serializer(value, self, name)
end
else
error("cannot save a " .. type(value))
end
end

function Serializer.methods:__serialize(serializer, name)
serializer:write("{}\n< /font>") -- create a new table
for k,v in pairs(self) do -- save its fields
local kname
if serializer:isBasic(k) then
kname = serializer:basicSerialize(k)
elseif type(k) == "table" then
if saved[k] then
kname = saved[k]
else
kname = serializer:generateName()
serializer:serialize(kname, k)
end
end
local fname = string.format("%s[%s]", name, kname)
serializer:serialize(fname, v)
end
end

function deserialize(o, stream)
stream = stream or io.stdin
end

--------------------------------------------------------------------------------
-- Test Cases
--------------------------------------------------------------------------------
Foo = Class("Foo")
function Foo.methods:init(...)
self.foo = 1
end

function Foo.methods:__serialize(serializer, name)
serializer:write("{foo = " .. self.foo .. "}\n")
end

Bar = Class("Bar", Foo)
function Bar.methods:init(...)
__super(self, ...)
self.bar = 2
end

function Bar.methods:boom()
if __super then __super(self) end
print "Bar:boom"
end

Bum = Class("Bum", Bar)
function Bum.methods:init(...)
if __super then __super(self, ...) end
self.bum = 3
end

function Bum.methods:boom()
if __super then __super(self) end
print "Bum:boom"
end

Be Sociable, Share!

One Comment

  1. e.v.e says:

    With the help of debug library it is possible to achieve even shorter syntax:

    function Child:method(…)
    super(…)
    end

    still such approach is rather slow (it requires one closure creation per call).
    Also take a look at metalua. It enables rather powerful techniques: e.g. one can make an extension that `saves` function body AST somewhere and this allows to get pretty OO-syntax at virtually no cost.

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>