Lua doesn't have classes and objects builtin, but provides mechanisms to define your own helper functions for object oriented programming. Many Lua programmers have done so, this module is one such attempt.
Features:
-
Simple object layout
The objects are just tables with a common metatable per class for looking up methods. All instance variables are stored in the object table, and there are no
__index
-chains for inheritance. Therefore, there is very little overhead (both runtime, and memory). -
Multiple inheritance
A class can inherit from multiple base classes. The inherited methods are looked up in width-first search order at class creation time (or when a base class is updated).
-
Multimethods
In case you need polymorphism for more than a single argument, this module allows you to define multimethods. The dispatch works for classes created via this module, and for builtin types.
-
Easy definition of (meta-)methods
Storing a new field in the class table will make it available in objects of this class and all derived classes. If the new field name is one of the valid metatable keys, the function is stored in the object's metatable instead. Only adding previously undefined metamethods is allowed this way, however (no overwriting of existing ones). So you can add new operator overloads to objects without direct access to their metatable.
-
Common utility functions
The module provides common utility functions, for e.g. detecting the class of an object, or figuring out if a class inherits from a certain other class.
This module doesn't set any global variables, so you have to store the
return value of require
somewhere, e.g. in a local variable class
.
local class = require( "classy" )
Result of the call to require
is a functable, a table that can
also be called like a function. The function call syntax is used for
defining classes, while the table holds the helper functions defined
by this OO module.
local SomeClass = class( "SomeClass" )
print( class.name( SomeClass ) ) --> SomeClass
local AnotherClass = class( "AnotherClass", SomeClass )
print( class.is_a( AnotherClass, SomeClass ) ) --> 1
When defining a class, the first argument is the class name, which can
be retrieved for a class/object table later using the class.name()
function. Additional arguments are base classes, which must be class
tables returned by previous calls to this module.
The resulting class tables have __call
metamethods defined, so that
you can create an object of a class using function call syntax. You
can define methods simply by storing a function in the class table. Of
course, the usual colon-syntax is supported. If the class has an
__init
method defined, this method is called during construction of
an object with the object as first argument, and any additional
arguments passed from the call of the class table. The __init
method
is never inherited by sub-classes, but you can of course call it via
the class table (e.g. in the constructor of a sub-class).
function SomeClass:__init( a, b )
self.a, self.b = a, b
end
function AnotherClass:__init( a, b, c )
SomeClass.__init( self, a, b )
self.c = c
end
function AnotherClass:print()
print( self.a, self.b, self.c )
end
local anObject = AnotherClass( 1, 2, 3 )
anObject:print() --> 1 2 3
Creating classes and defining methods (especially on classes with many sub-classes) involve some bookkeeping, so that object construction and method lookup on objects can be fast.
Adding methods to base classes will also make them available for objects of derived classes. Metamethods are not inherited, however.
function SomeClass:say_hello()
print( "hello from " .. class.name( self ) )
end
local someObject = SomeClass( 1, 2 )
someObject:say_hello() --> hello from SomeClass
anObject:say_hello() --> hello from AnotherClass
function SomeClass:__add( rhs )
return SomeClass( self.a + rhs.a, self.b + rhs.b )
end
local _ = someObject + someObject --> ok!
local _ = anObject + anObject --> error!
You can pretty much define any metamethod except __index
(which
is already used for method lookup), and maybe __gc
(works on tables
starting from Lua 5.2, and only for objects created after the __gc
metamethod has been set).
If you override a method in a sub-class, and you want to call the method of the base class, you can use the class table of the base class for that.
function AnotherClass:say_hello()
SomeClass.say_hello( self )
print( "good bye" )
end
anObject:say_hello() --> hello from AnotherClass
--> good bye
If the type of self
is not sufficient to select a suitable method,
you can define a multimethod:
local multi = class.multimethod( 1, 2 ) -- dispatch via args 1 and 2
class.overload( multi, SomeClass, SomeClass, function( a, b )
print( "SomeClass, SomeClass" )
end )
class.overload( multi, SomeClass, AnotherClass, function( a, b )
print( "SomeClass, AnotherClass" )
end )
class.overload( multi, AnotherClass, AnotherClass, function( a, b )
print( "AnotherClass, AnotherClass" )
end )
multi( anObject, anObject ) --> AnotherClass, AnotherClass
multi( someObject, anObject ) --> SomeClass, AnotherClass
multi( someObject, someObject ) --> SomeClass, SomeClass
multi( anObject, someObject ) --> SomeClass, SomeClass
This also works for builtin types, and for type checking functions
following the same protocol as io.type
or lpeg.type
:
local dispatch = class.multimethod( 1, 2 )
class.overload( dispatch, "string", "number", function( a, b )
print( "string, number" )
end )
class.overload( dispatch, "number", "string", function( a, b )
print( "number, string" )
end )
class.overload( dispatch, "number", io.type, "file", function( a, b )
print( "number, file" )
end )
dispatch( "xy", 2 ) --> string, number
dispatch( 2, "xy" ) --> number, string
dispatch( 3, io.stdout ) --> number, file
dispatch( 1, 2 ) --> error!
For builtin types the argument type must match exactly (there are no superclasses to take into account). Of course, you can also mix classes and builtin types.
And that's basically it!
class( class_name [, ...] ) ==> table
class_name: string -- name of the class
... : table* -- class tables for base classes
Calling the result of the require call (named class
in the above
code snippet) creates a new class table. Zero or more base classes may
be given as extra arguments after the class name. There is no general
superclass like Object
from which all classes inherit by default!
The resulting class has a __call
metamethod defined for constructing
objects of this class, and basically acts like a normal table, except
that fields with metamethod-names don't end up in the class table.
Iteration via pairs()
provides all available fields of the class
(including fields of base classes), but it only works for Lua 5.2 and
up.
class.of( object ) ==> table/nil
The of
function returns the class table of an object, or nil
if
the argument isn't an object created via this module.
class.name( obj_or_cls ) ==> string/nil
obj_or_cls: table -- an object table or a class table
The name
function returns the class name specified during class
definition for a given object or class. If the argument is not a class
table created using this module or an object of such a class, this
function returns nil
.
class.is_a( obj_or_cls, base ) ==> integer/nil
obj_or_cls: table -- an object table or a class table
base : table -- a class table
The is_a
function checks if a given object or class is a sub-class
of certain class.
class.cast( object, class ) ==> object
class: table -- a class table
The cast
function changes the class of a given object (or normal
table) to the given class (it replaces the metatable) and returns the
object. No constructors are called in the process, so the object might
be in an invalid state for the new class. If you want to prevent
objects of a certain class to be casted, define a __metatable
field
in the metatable.
class.delegate( class, fname [, ...] ) ==> table
class: table -- a class table
fname: string -- name of a field in the objects
... : table/string* -- vararg list or array of method names
The delegate
function creates new methods for a class that forward
to methods on an object stored inside of objects of this class. The
stored object can be found via the given fieldname. The method names
to delegate can be specified as varargs or in an array. The class
table is returned.
class.multimethod( ... ) ==> function -- returns the multimethod
... : integer,integer* -- indices of polymorphic parameters
The multimethod
function creates a function, that tries to dispatch
calls to suitable registered functions depending on the arguments
passed. Only arguments specified during the creation of the
multimethod are used to search for a matching implementation.
class.overload( mm, ... )
mm : function -- the multimethod
... : (string/table/(function,string))*, function
Calling this function adds an overload to a given multimethod, i.e. a
new set of parameter types for which a new implementation method is
called. The additional arguments to overload
are either strings
(names of builtin types), class tables (for classes), or pairs of a
function and a string for external type checking functions like
io.type
(the function is the type checker, the string is the
type name to match). The number of types for this call must match the
number of arguments to the original class.multimethod
call. The last
argument of overload
is the function (or anything callable) to
register for this specific overload.
Philipp Janda, siffiejoe(a)gmx.net
Comments and feedback are always welcome.
classy
is copyrighted free software distributed under the MIT
license (the same license as Lua 5.1). The full license text follows:
classy (c) 2013-2014 Philipp Janda
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHOR OR COPYRIGHT HOLDER BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.