LuaClassSystem (LCS) is a Lua library for emulating some features of object-oriented programming (classes, inheritance) on top of Lua. The version used on Leaguepedia is derived from the original version on GitHub that hasn't been maintained since mid-2014.
User documentation[]
This section of the documentation is intended for those who wish to use LCS in their modules.
System requirements[]
Compatibility with the following environments is intended:
- MediaWiki Scribunto (both standalone Lua and LuaSandbox).
- Non-MediaWiki Lua 5.1 and all newer versions, including other standalone implementations based on these versions (for an incomplete list, see the one on the LuaUsers wiki). Please note that none of these environments have yet been tested with the modified version, though they are expected to work because at least some of them work with the original version.
Importing[]
LuaClassSystem should be placed into a separate file (or, when using MediaWiki, a module) and imported with a require()
call. The value returned by the call is a table with the following functionality:
_VERSION
– a string designating the current version of LCS. At the moment it's equal to2.1.1
.class
(details) – a table with the functionality to create new classes:- field
abstract
– a class constructor that creates an abstract class. Attempting to instantiate an abstract class is an error. - field
final
– a class constructor that creates a final class. Attempting to inherit from a final class is an error. - The table
class
itself is also callable as a class constructor that creates a regular class, i. e. one that is neither abstract nor final.
- field
instanceOf
(details) – a function that checks whether an object belongs to a given class. See the documentation forinstanceOf
for details.xtype
(details) – a function that behaves like the builtintype
, but returns"object"
or"class"
for LCS values. See the documentation forxtype
for details.isObject
(details) – a function that returnstrue
/false
depending on whether it is provided an LCS object.isClass
(details) – a function that returnstrue
/false
depending on whether it is provided an LCS class.debug
– features intended to be used by people developing LCS itself. They are not intended for non-developers.
In code examples provided in the documentation, the table as listed above is referenced by the variable LCS
. This can be achieved in Scribunto like: local LCS = require("Module:LuaClassSystem")
.
Creating classes[]
LuaClassSystem can create base classes by means of any of the three class constructors:
LCS.class
, which produces a regular class.LCS.class.abstract
, which produces an abstract class that can't be instantiated.LCS.class.final
, which produces a final class that can't be extended.
All of the three class constructors are callable values with the same syntax. Class constructors may optionally accept a member table that will be used as the base for the class itself and all of its instances. The member table for the class is a deep copy of the one provided to the class constructor, and instances are created as deep copies of the class. Deep copies created in this manner copy references to LCS objects or classes instead of cloning the objects or classes. The member table may not contain functions directly.
The following code creates a class:
local MyClass = LCS.class()
Instance constructors[]
To create an instance of a class, use the :new()
method of the class or call the class directly.
local MyClass = LCS.class()
-- These are functionally identical, and the former internally falls back to the latter.
local object1 = MyClass()
local object2 = MyClass:new()
You can have an instance constructor for a class that does more than merely creates an empty instance. For example, such a constructor could initialize some fields of the class. To do that, declare an :init()
method for the class.
The :init()
method is called as part of the :new()
method, receives all values passed to :new()
, and should not return any values. (Any values returned by :init()
implementations will be ignored.) Do not override the :new()
method itself. See Reserved names for more information.
The following code creates a class, declares a custom constructor for a class, and uses it to initialize a field.
local MyClass = LCS.class()
function MyClass:init(length)
local length = tonumber(length) or 0
self.array = {}
for i = 1, length do
self.array[i] = math.sqrt(i)
end
end
local object1 = MyClass:new(4)
print(object1.array[2]) -- prints sqrt(2), approximately 1.414
print(object1.array[4]) -- prints 2
Declaring methods[]
Methods are declared on classes in the same manner as seen above with init
.
local MyClass = LCS.class()
function MyClass:init()
self.value = 10
end
function MyClass:runCountdownTick()
if self.value > 0 then
self.value = self.value - 1
end
return self.value
end
function MyClass:isReady()
return self.value == 0
end
function MyClass:reset()
self.value = 10
end
local object = MyClass:new()
for i = 1, 8 do
object:runCountdownTick()
end
-- value is now 2
local one = object:runCountdownTick()
local zero = object:runCountdownTick()
assert(object:isReady())
object:reset() -- back to 10
Reserved names[]
Some built-in class fields rely on internal functionality and/or implement such functionality. If users set these fields to other values, such functionality is likely to break, causing runtime errors. Classes should not have elements with the following names:
new
: used for instance creation.extends
,abstractExtends
,finalExtends
: used for extending classes (see below).super
: used when calling original versions of methods from superclasses.getClass
,getSubClasses
: built-in functionality.__index
,__call
: reserved metamethods, used for class system functionality.
Extending classes[]
Subclassing (inheritance) allows having a single source of shared functionality between multiple classes where the subclasses are "subtypes" of the base type. For example, this is a common approach in GUI libraries where "Component" or such is the base class that implements functionality common to all GUI components, and every subclass ("Button", "Image", "ScrollPane"...) implements its own functionality in addition to the common features.
Lua does not implement classes and inheritance natively (why libraries like LCS are needed), but it does implement features useful in construction of class systems (the :method()
syntactic sugar, __index
metamethods, and maybe others).
To create a subclass, call the :extends()
method on the base class. The method takes an optional "member table" with functionality identical to that of class constructors.
local BaseClass = LCS.class()
local DerivedClass = BaseClass:extends()
Derived classes (subclasses) do not clone class fields. Methods and fields not specified in the subclass are accessible from the superclass via an __index
fallback. Modifications performed to table fields through a subclass are replicated in superclasses:
local BaseClass = LCS.class()
BaseClass.values = {1, 2, 3}
function BaseClass:sum()
local sum = 0
for _, v in ipairs(self.values) do
sum = sum + v
end
return sum
end
print(BaseClass:new():sum()) -- prints 6
local DerivedClass = BaseClass:extends()
table.insert(DerivedClass.values, 4)
print(DerivedClass:new():sum()) -- prints 10. Note that :sum() is not defined in the derived class
print(BaseClass:new():sum()) -- ...also prints 10 now
Interacting with superclasses[]
Subclasses often override methods defined in their superclasses, and also need defer to the original implementation. This is what the :super()
method is for.
Unlike some other Lua class system implementations, LCS does not require to retain a reference to the superclass to call an original version of a method from that superclass. Syntax like SuperClass.super(self:method(args))
isn't needed and is replaced with self:super('method', args)
. While using the function name (as a key to the superclass) as a string might look less aesthetically pleasing, it should be noted that in Lua, .field
is syntactic sugar for ['field']
.
local Counter = LCS.class()
function Counter:init()
self.value = 0
end
local DoubleCounter = Counter:extends()
function DoubleCounter:init()
self:super('init')
self.value2 = 0
end
Passing a method name that isn't overridden in the inheritance chain for the calling class is invalid. This is likely to produce an error.
local Counter = LCS.class()
function Counter:init()
self.value = 0
end
local DoubleCounter = Counter:extends()
function DoubleCounter:init()
self:super('meow') -- no cats involved (sadly), so Lua just crashes.
self.value2 = 0
end
If the original method isn't overridden, there is no need to use :super()
, and the method can be called directly.
Abstract and final classes[]
A feature inspired by some class system implementations such as the one in Java, abstract and final classes restrict some inheritance-related behavior.
Abstract classes do not allow creation of instances. To create an abstract class, use LCS.class.abstract()
. To make a derived abstract class, use BaseClass:abstractExtends()
. Note that the base class does not need to be abstract in the latter case.
Final classes do not allow creation of subclasses. To create a final class, use LCS.class.final()
. To make a derived final class, use BaseClass:finalExtends()
.
For hopefully obvious reasons, a class cannot be both abstract and final. It should not be possible to make such a class, but if one is made, the program is considered to be in an invalid state.
Checking types[]
LCS exports some functions for checking types of objects in an LCS-aware manner.
LCS.instanceOf
allows to check whether a given object is an instance of a given class or its direct or indirect superclass.
local Animal = LCS.class()
local Feline = Animal:extends()
local Cat = Feline:extends()
local Tiger = Feline:extends()
local flash = Cat()
assert(LCS.instanceOf(flash, Animal))
assert(LCS.instanceOf(flash, Feline))
assert(LCS.instanceOf(flash, Cat))
assert(not LCS.instanceOf(flash, Tiger))
LCS.xtype
stands for extended type
and works like type()
, but returns "object"
or "class"
instead of "table"
for known LCS objects and classes.
LCS.isObject
and LCS.isClass
return boolean values for whether the given value is a known LCS object or class respectively.
Advanced usage[]
There are some nontrivial possibilities allowed by LCS. Here are some of them.
Types as parameters[]
An LCS class can accept another LCS class as a parameter and expect to be able to perform some operations with instances of that class. While not enforced in any way by LCS itself, this offers the possibility for informal "interface"/"trait" specifications. Alternatively, the parameter class may be expected to be a subclass of some superclass, the functionality of which is then used. Compare it to T implements Interface
and T extends BaseClass
in Java terminology.
Additional considerations[]
Cloning[]
General-purpose cloning functions attempt to reconstruct LCS objects in a manner that does not make LCS aware of these new objects. Such cloned objects trigger state validity checks if used with some LCS internal methods. For example, calling :new()
from a cloned class or :super()
for a cloned object will cause an error because the class / object is not known to LCS.
mw.clone()
is known to cause such errors.
LCS exports LCS.deepCopy
, which is an LCS-aware deep copying / cloning function. References to stored objects or classes are preserved. In addition, this function disregards metatables on any non-LCS values in the original table. This makes it incidentally work on mw.loadData()
tables (mw.clone()
breaks on them because of the __index
/__newindex
-based write protection), but if any contained tables rely on metatables being preserved for correct operation, LCS.deepCopy
is unsuitable.
Two registries[]
It is invalid for two LCS instances (each with its own registry) to exist simultaneously. This can happen, for example, if LCS is imported twice as two different modules. In such a case, a lot can go wrong, up to the point of the program crashing due to an attempt to deep-copy a recursive object.
Developer documentation[]
Debug features[]
debug.isKnownObject
, debug.isKnownClass
: same as LCS.isObject
and LCS.isClass
(but the debug versions came before). The debug versions also give errors if given something other than tables.
Object registry data[]
__superClass
(class): the class the object is an instance of
Class registry data[]
__superClass
(class|false
): the superclass of this class, orfalse
if this is a root class__subClass
(map: class →true
): stores weak references to direct subclasses as indexes so that indexing the table is enough to say whether some other class is a direct subclass of the current class.__abstract
(boolean):true
if this class is abstract__final
(boolean):true
if this class is final
Invariants[]
If any of the following is broken, the LCS state is invalid, which incurs undefined behavior.
- No value may be registered as both a class and an object.
- No registry data value may be of an inappropriate type.
- There may be no inheritance loops.
- No class may be both abstract and final.
- Only one registry may exist in a single Lua state at any given time.
- All LCS classes and objects must be referenced in the LCS registry with valid data.
Dev stories[]
Good First Bug[]
...except there wasn't anything good about it.
The first bug discovered with LuaClassSystem was rather damaging because it made some pages give errors that looked like the user was doing something wrong. Yet the code was just about exactly the same in two different pages, one of which worked fine, and the other gave the error. Running the code in the debug console also didn't show any errors.
I was randomly poking around the module for maybe an hour or two and eventually edited out the registry's __mode
settings, hoping that they weren't the issue. They turned out to be the issue. I wasn't happy.
A bit of an explanation, because the feature I refer to is probably not in the spellbook of almost any Scribunto wizard. Lua uses a garbage collector to keep track of what values are created and which of them are still used. Unlike with manual memory management (like in C), this does not require the programmer to manually malloc
/free
in just the right ways to avoid very serious bugs (from memory leaks eventually crashing programs or player characters' heads turning into brown blobs... to anything like major security vulnerabilities). Garbage collection is not suitable for lower-level programming, but for high-level features such as Scribunto it's fine.
With garbage collectors, any object still referenced is considered used and is not collected. (Maybe also with some variations to prevent reference cycles from causing memory leaks.) As the LCS registry keeps track of all objects it creates by referencing them, it means Lua never stops "using" any LCS objects even if they are just tracked in the registry. In other terms, the registry causes a memory leak.
That's why weak references exist. Weak references allow to refer to an object in a manner that doesn't prevent the garbage collector from collecting it. In Lua, weak references are assigned as values of the __mode
setting in the metatable. The value must be a string: if it contains k
, the keys in the table are weak, and if it contains v
, the values are weak. __mode
may contain either or both letters.
The registry associates references to LCS objects / classes with tables storing internal data. As such, the keys should have been stored as weak references. But the original LCS assigned __mode
as v
, and since the internal tables weren't used elsewhere, any garbage collection (an event the programmer cannot predict or observe) wiped out every single entry in the registry, causing data corruption.
So this sequence of events caused an error:
- User code creates a class.
- Lua collects garbage. Suddenly the above class isn't a class any more! And the user has no idea.
- User code attempts to instantiate the class that's no longer a class. LCS has checks against instantiating from non-classes (likely to guard against
Class.new()
instead ofClass:new()
), which get triggered and crash Lua with an error.
Oops.
This is Super Fun[]
Oh, that one wasn't really fun. That time I had to wait until I can mess with the staging environment so that I can safely break everything.
Spent five hours trying to understand why :super()
wasn't working in some module. Turned out that many lines of code before, the table storing LCS objects got fed to a utility function called safe
that did... I don't remember what it did, but part of what it did was cloning the table via mw.clone()
. That turned out rather unsafe instead: the cloned objects weren't recognized as such by LCS, and while they worked normally for regular usage (modifying, calling methods) a :super()
call down the line exposed the error.
The Bloodthirster, Blade of the Ruined King... and pineapples, kittens, kittens, kittens![]
That one was even worse. Didn't help that resolution was a priority due to active performance issues.
With UCP, Leaguepedia eventually got LuaSandbox to significantly improve Scribunto performance. Esports wikis had to delay LuaSandbox rollout due to an issue with a module producing incorrect output.
At first glance, __ipairs
, a Lua 5.2 feature backported by Scribunto devs to the 5.1 baseline, stopped working. Yet after further investigation it became clear it stopped working only in some specific scenario.
I had the possibility of using a hacky workaround to maybe stop the issue, but:
- the issue wouldn't be gone,
- the workaround would have been tailored to one specific Leaguepedia module and might have caused other issues I wouldn't be able to easily detect.
So I tried poking around, like in the last case. I eventually hit upon that inheritance breaks __ipairs
, and later that the __ipairs
metamethod wasn't in the metatable itself, but with LCS, that metatable itself had a metatable, and the __index
fallback for the first metatable had __ipairs
. In other terms, originally with LuaStandalone the __ipairs
backport used something like (getmetatable(t) or {}).__ipairs
, while LuaSandbox used something like rawget(getmetatable(t) or {}, '__ipairs')
. At the same time, LCS didn't propagate metavalues to subclasses, and so it quietly relied on non-rawget
access to __ipairs
to work.
I thought it was an error in LuaSandbox at first, but I consulted Lua docs, and found out metatable access must be raw. I admit I didn't know of that before, like I didn't know of a use for __mode
before that first LCS issue.
By the way, this LCS-free snippet allows to check whether your interpreter has this problem:
function test()
local base = {}
base.__index = base
base.__ipairs = function(self)
local i = 0
return function()
i = i + 1
local value = self.values[i]
if value then return i, value; end
end
end
local derived = {}
derived.__index = base
setmetatable(derived, base)
local instance = { values = {100, 20, 3} }
setmetatable(instance, derived)
local sum = 0
for i, v in ipairs(instance) do
sum = sum + v
end
return sum
end
print(test()) -- if using Scribunto, works only in the debug console
This might look like it should print 123
, but it's wrong: because of the rawget
requirement, the code should print 0
. If it prints 123
, watch out.
As an extra note, I found out this line: base.__index = base
is needed to actually replicate LCS behavior and reproduce the issue. However, note that it makes base
recursive. When debugging the issue, a mistake caused the creation of two simultaneous LuaClassSystem instances with two registries, something I predicted was the issue with the clone
bug above (it wasn't; it can only happen if LCS is in two separate modules at the same time). This time it was, and I suspect due to some deep-cloning involved (and LCS' deepCopy
not having any recursion protection, and relying on the registry being valid), it overflowed all available memory, crashing the interpreter.
[]
-- THIS IS A MODIFIED (NOT THE ORIGINAL) VERSION OF THIS SOFTWARE.
-- Copyright (c) 2012-2014 Roland Yonaba
--[[
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source
distribution.
--]]
local pairs, ipairs = pairs, ipairs
local assert = assert
local setmetatable, getmetatable = setmetatable, getmetatable
local type = type
local insert = table.insert
-- Internal register
local _registry = {
class = setmetatable({}, {__mode = 'k'}),
object = setmetatable({}, {__mode = 'k'})
}
-- Checks if thing is a kind or whether an 'object' or 'class'
local function isObject(thing)
return _registry.object[thing] ~= nil
end
local function isClass(thing)
return _registry.class[thing] ~= nil
end
-- Given an object and a class, checks whether the object is an instance of the
-- class or one of its superclasses.
local function instanceOf(thing, class)
assert(isObject(thing), 'instanceof: `thing` must be an LCS object')
assert(isClass(class), 'instanceof: `class` must be an LCS class')
local thingClass = _registry.object[thing].__superClass
if class == thingClass then
return true
end
local thingSuperClass = _registry.class[thingClass].__superClass
while thingSuperClass do
if class == thingSuperClass then
return true
else
thingSuperClass = _registry.class[thingSuperClass].__superClass
end
end
-- loop terminated = reached a base class and still haven't found one `thing` is an instance of
return false
end
-- tostring
local function __tostring(self, ...)
-- support for tostring as a method is deprecated and only here for backwards compatibility
-- it will probably be removed in a future release
if self.tostring then
return self:tostring(...)
end
if isClass(self) then
return "LCS class: ?"
elseif isObject(self) then
return "LCS object: ?"
end
return tostring(self)
end
-- Base metatable
local baseClassMt = {
__call = function (self, ...) return self:new(...) end,
__tostring = __tostring
}
local Class
-- Simple helper for building a raw copy of a table
-- Only pointers to classes or objects stored as instances are preserved
local function deepCopy(t)
local r = {}
for k, v in pairs(t) do
if type(v) == 'table' then
if (_registry.class[v] or _registry.object[v]) then
r[k] = v
else
r[k] = deepCopy(v)
end
else
r[k] = v
end
end
return r
end
-- Checks for a method in a list of attributes
local function checkForMethod(list)
for k, attr in pairs(list) do
assert(type(attr) ~= 'function', 'Cannot assign functions as members')
end
end
-- Instantiation
local function instantiateFromClass(self, ...)
assert(isClass(self), 'Class constructor must be called from a class')
assert(not _registry.class[self].__abstract, 'Cannot instantiate from abstract class')
local instance = deepCopy(self)
_registry.object[instance] = {
__superClass = self,
}
local instance = setmetatable(instance, self)
if self.init then
self.init(instance, ...)
end
return instance
end
-- Classes may not override these metavalues.
local restrictedMetavalues = { __index = true, __call = true }
-- Class derivation
local function extendsFromClass(self, extra_params)
assert(isClass(self), 'Inheritance must be called from a class')
assert(not _registry.class[self].__final, 'Cannot derive from a final class')
local class = Class(extra_params)
class.__index = class
class.__tostring = __tostring
_registry.class[class].__superClass = self
_registry.class[self].__subClass[class] = true
for k, v in pairs(self) do
if type(k) == 'string' and k:find("^__") and not restrictedMetavalues[k] then
class[k] = v
end
end
return setmetatable(class, self)
end
-- Abstract class derivation
local function abstractExtendsFromClass(self, extra_params)
local c = self:extends(extra_params)
_registry.class[c].__abstract = true
return c
end
-- Final class derivation
local function finalExtendsFromClass(self, extra_params)
local c = self:extends(extra_params)
_registry.class[c].__final = true
return c
end
-- Super methods call
local function callFromSuperClass(self, f, ...)
assert(isClass(self) or isObject(self), 'attempted to call :super from an unknown object/class')
local superClass = getmetatable(self)
if not superClass then return nil end
local super
if isClass(self) then
super = superClass
else -- must be an object due to the assert above
assert(isClass(superClass), 'attempted to call :super with an object that has an unknown class')
super = _registry.class[superClass].__superClass
end
local s = self
while s[f] == super[f] do
s = super
super = _registry.class[super].__superClass
end
-- If the superclass also has a superclass, temporarily set :super to call THAT superclass' methods
local supersSuper = _registry.class[super].__superClass
if supersSuper then
_registry.class[superClass].__superClass = supersSuper
end
local method = super[f]
local result = method(self, ...)
-- And set the superclass back, if necessary
if supersSuper then
_registry.class[superClass].__superClass = super
end
return result
end
-- Gets the superclass
local function getSuperClass(self)
local super = getmetatable(self)
return (super ~= baseClassMt and super or nil)
end
-- Gets the subclasses
local function getSubClasses(self)
assert(isClass(self), 'getSubClasses() must be called from class')
return _registry.class[self].__subClass or {}
end
-- Class creation
Class = function(members)
if members then checkForMethod(members) end
local newClass = members and deepCopy(members) or {} -- includes class variables
newClass.__index = newClass -- prepares class for inheritance
_registry.class[newClass] = { -- builds information for internal handling
__abstract = false,
__final = false,
__superClass = false,
-- Superclasses have no logical dependency on their subclasses.
__subClass = setmetatable({}, {__mode = 'k'}),
}
newClass.new = instantiateFromClass -- class instanciation
newClass.extends = extendsFromClass -- class derivation
newClass.abstractExtends = abstractExtendsFromClass -- abstract class deriviation
newClass.finalExtends = finalExtendsFromClass -- final class deriviation
newClass.__call = baseClassMt.__call -- shortcut for instantiation with class() call
newClass.super = callFromSuperClass -- super method calls handling
newClass.getClass = getSuperClass -- gets the superclass
newClass.getSubClasses = getSubClasses -- gets the subclasses
newClass.__tostring = __tostring -- tostring
return setmetatable(newClass, baseClassMt)
end
-- Static classes
local function abstractClass(members)
local class = Class(members)
_registry.class[class].__abstract = true
return class
end
-- Final classes
local function finalClass(members)
local class = Class(members)
_registry.class[class].__final = true
return class
end
-- These functions are exported for debugging.
local debug = {}
-- Given a table `which`, returns `true`/`false` for whether `which` is a known LCS object.
-- If given something that is not a table, produces an error. As this is a debug function,
-- it checks for errors more extensively.
function debug.isKnownObject(which)
assert(type(which) == 'table', 'debug.isKnownObject not given a table. This should never happen.')
return isObject(which)
end
-- Given a table `which`, returns `true`/`false` for whether `which` is a known LCS class.
-- If given something that is not a table, produces an error. As this is a debug function,
-- it checks for errors more extensively.
function debug.isKnownClass(which)
assert(type(which) == 'table', 'debug.isKnownClass not given a table. This should never happen.')
return isClass(which)
end
-- Stands for "e*x*tended type". Like the built-in `type`, returns a string for
-- the type of the given object. If the value `which` is a known object or a
-- class, returns 'object' or 'class' respectively, otherwise defers to `type`.
local function xtype(which)
if isObject(which) then
return 'object'
elseif isClass(which) then
return 'class'
else
return type(which)
end
end
-- Returns utilities packed in a table (in order to avoid polluting the global environment)
return {
_VERSION = "2.1.1",
class = setmetatable(
{
abstract = abstractClass,
final = finalClass
},
{
__call = function(self, ...) return Class(...) end
}
),
-- custom LCS patches come below
deepCopy = deepCopy,
instanceOf = instanceOf,
xtype = xtype,
isObject = isObject,
isClass = isClass,
debug = debug,
}