Simple Lua Classes |
|
Out of the box, Lua does not have a class system, but its powerful metaprogramming facilities makes defining classic objects straightforward. In fact, there's a number of ways of doing this; together with the unfamiliar notation, this makes object-orientation in Lua seem a little intimidating at first.
The method described here is the most common and flexible method, using metatables. A table's behavior can be customized by giving it a metatable. For instance, if the metatable has an __index
function, then any failed attempt to look up something in the table will be passed to __index
. If __index
is itself a table, the symbol will be looked up in that table. (Please see the excellent discussion in PiL [1]) Here is the basic idea:
Account = {} Account.__index = Account function Account:create(balance) local acnt = {} -- our new object setmetatable(acnt,Account) -- make Account handle lookup acnt.balance = balance -- initialize our object return acnt end function Account:withdraw(amount) self.balance = self.balance - amount end -- create and use an Account acc = Account:create(1000) acc:withdraw(100)
Here, Account objects are represented by tables, which contain precisely one field, the balance. Lua tries to look up withdraw
in acc, and cannot find it. Because acc has a metatable that defines __index
, it will then look up withdraw
in that metatable. So acc:withdraw(100)
is actually the call Account.withdraw(acc,100)
. We could have actually put withdraw
directly into acc
, but this would be wasteful and inflexible - adding a new method would require a change to the create
function, etc.
I'll define a function class
which does all this (and more) transparently.
Account = class(function(acc,balance) acc.balance = balance end) function Account:withdraw(amount) self.balance = self.balance - amount end -- can create an Account using call notation! acc = Account(1000) acc:withdraw(100)
In this scheme, one supplies an initialization function to the new class, and a 'constructor' is automatically generated.
Simple inheritance is supported. For example, here a base class Animal
is defined, and several specific kinds of animals are declared. All classes made using class
have a is_a
method, which you can use to find out the actual class at runtime:
-- animal.lua require 'class' Animal = class(function(a,name) a.name = name end) function Animal:__tostring() return self.name..': '..self:speak() end Dog = class(Animal) function Dog:speak() return 'bark' end Cat = class(Animal, function(c,name,breed) Animal.init(c,name) -- must init base! c.breed = breed end) function Cat:speak() return 'meow' end Lion = class(Cat) function Lion:speak() return 'roar' end fido = Dog('Fido') felix = Cat('Felix','Tabby') leo = Lion('Leo','African')
D:\Downloads\func>lua -i animal.lua > = fido,felix,leo Fido: bark Felix: meow Leo: roar > = leo:is_a(Animal) true > = leo:is_a(Dog) false > = leo:is_a(Cat) true
All Animal
does is define __tostring
, which Lua will use whenever a string representation is needed of the object. In turn, this relies on speak
, which is not defined. So it's what C++ people would call an abstract base class; the specific derived classes like Dog
define speak
. Please note that if derived classes have their own initialization functions, they must explicitly call init
for their base class.
class()
uses two tricks. It allows you to construct a class using the call notation (like Dog('fido')
above) by giving the class itself a metatable which defines __call
. It handles inheritance by copying the fields of the base class into the derived class. This isn't the only way of doing inheritance; we could make __index
a function which explicitly tries to look a function up in the base class(es). But this method will give better performance, at a cost of making the class objects somewhat fatter. Each derived class does keep a field _base
that contains the base class, but this is to implement is_a
.
Note that modification of a base class at runtime will not affect its subclasses.
-- class.lua -- Compatible with Lua 5.1 (not 5.0). function class(base, init) local c = {} -- a new class instance if not init and type(base) == 'function' then init = base base = nil elseif type(base) == 'table' then -- our new class is a shallow copy of the base class! for i,v in pairs(base) do c[i] = v end c._base = base end -- the class will be the metatable for all its objects, -- and they will look up their methods in it. c.__index = c -- expose a constructor which can be called by <classname>(<args>) local mt = {} mt.__call = function(class_tbl, ...) local obj = {} setmetatable(obj,c) if init then init(obj,...) else -- make sure that any stuff from the base class is initialized! if base and base.init then base.init(obj, ...) end end return obj end c.init = init c.is_a = function(self, klass) local m = getmetatable(self) while m do if m == klass then return true end m = m._base end return false end setmetatable(c, mt) return c end
--- class_orig.lua 2009-07-24 20:53:25.218750000 -0400 +++ class.lua 2009-07-24 20:53:49.734375000 -0400 @@ -21,8 +21,8 @@ mt.__call = function(class_tbl,...) local obj = {} setmetatable(obj,c) - if ctor then - ctor(obj,...) + if class_tbl.init then + class_tbl.init(obj,...) else -- make sure that any stuff from the base class is initialized! if base and base.init then
A = class() function A:init(x) self.x = x end function A:test() print(self.x) end B = class(A) function B:init(x,y) A.init(self,x) self.y = y end
function A:__add(b) return A(self.x + b.x) end
c.init = ctor. I changed this argument's name to 'init'. --DeniSpir
function class(def) local class = {} local parents = {} local upv local env = _G local wraps local function super(parent_class) if not parent_class then parent_class = parents[1] end local this = this local that = {} for k,v in pairs(parent_class) do that[k] = type(v) == 'function' and wraps(this, v) or v end return setmetatable(that, that) end function wraps(this, func) return function(...) local t = env.this local s = env.super env.this = this env.super = super local ret = pcall(func, ...) env.this = t env.super = s return ret end end function class.__init()end for i=1,math.huge do inherit, v = debug.getlocal(def, i) if not inherit then break end local parent_class = _G[inherit] for i=1,math.huge do local name, pclass = debug.getlocal(2,i,1) if not name then break elseif name == inherit then parent_class = pclass break end end if parent_class and type(parent_class) == 'table' then table.insert(parents, parent_class) for k,v in pairs(parent_class) do class[k] = v end else error(string.format('Class "%s" not valid.', name)) end end for i=1,math.huge do local name, value = debug.getupvalue(def, i) if not name then break elseif name == '_ENV' then env = value upv = i break end end local _env = setmetatable({}, { __index= function(t, name) local value = class[name] return value ~= nil and value or env[name] end, __newindex = function(t, name, value) class[name] = value end }) local function senv(env) if upv then debug.setupvalue(def, upv, env) else _G = env end end senv(_env) env.pcall(def, env.table.unpack(parents)) senv(env) return setmetatable({}, { __ipairs = function() return ipairs(class) end, __pairs = function() return pairs(class) end, __index = function(t, name) return class[name] end, __index_new = function(t, name, value) class[name] = value end, __call = function(...) local this = {} for k,v in pairs(class) do this[k] = type(v) == 'function' and wraps(this, v) or v end this.__class = class this.__init(...) return setmetatable(this, this) end }) end
global = true Inherit = class(function() this_is_a_property_of_Inherit = true function __init() print('Inherit().__init()') this.init = true end function __call() print('Yay! You\'re calling for me :) init:', this.init, '\n') end end) Example = class(function(Inherit) print('Inherited property:', this_is_a_property_of_Inherit) print('Global variable: ', global, '\n') function __init() print('Example().__init()') super().__init() print('this.init:', this.init) end function test(...) print(..., this.__init, '\n') end end) example = Example() example.test('__init:') example() example.property = 'I\'m a property of instance "example"' print('example.property', example.property) print('Example.property', Example.property) -- Inherited property: true -- Global variable: true -- Example().__init() -- Inherit().__init() -- this.init: true -- __init: function: 0x15dd5f0 -- Yay! You're calling for me :) init: true -- example.property I'm a property of instance "example" -- Example.property nil