Awesome
classic - Class and module system for Lua.
classic
is a simple class system for Lua.
Its features include:
- named classes; nesting of names permitted
- does not pollute the global namespace
- compatibility with torch.save / torch.load
- reflection
- 'fat' inheritance, mixins
- strictifiability
- interfaces
- sourcing the same definition twice is permitted
Usage
Basic usage
local classic = require 'classic'
local MyClass = classic.class("MyClass")
function MyClass:_init(opts)
self.x = opts.x
end
function MyClass:getX()
return self.x
end
local instance_a = MyClass{x = 3}
local instance_b = MyClass{x = 4}
print(instance_a, instance_a:getX())
print(instance_b, instance_b:getX())
Inheritance
local classic = require 'classic'
local Base = classic.class("Base")
function Base:_init()
self._x = "base"
end
function Base:getX()
return self._x
end
local Child, super = classic.class("Child", Base)
function Child:_init(y)
super._init(self) -- call the superconstructor, *passing in self*
self._y = assert(y)
end
function Child:getY()
return self._y
end
local obj = Child("y")
print(obj:getY()) -- y
print(obj:getX()) -- base
print(obj:class():isSubclassOf(Base)) -- true
Reflection
print(instance_a:class():name())
print(instance_a:class():methods())
Torch IO Compatibility
Note: you must require classic.torch
for classic classes to work properly with
torch serialization!
local classic = require 'classic'
require 'classic.torch'
local A = classic.class("A")
function A:foo()
return 3
end
local a = A()
torch.save("a.t7", a)
local loaded = torch.load("a.t7")
print(a:foo())
After having required classic.torch
, custom __read
/__write
methods can now
be added to the class (as regular methods rather than metamethods). torch.load
and torch.save
will invoke these methods if present, as the read
/write
metamethods used in torch.class
instances.
Strictness
local classic = require 'classic'
local A = classic.class("A")
local a = A()
classic.strict(a)
-- Error!
print(a.thisAttributHasATypo)
Class attributes
You can store data on class objects - for example, if you want to share something between all instances of that class. Note only that you cannot store a function as a class attribute, as that is indistinguishable from defining an instance method.
local classic = require 'classic'
local A = classic.class("A")
A.var = 3
print(A.var)
Static methods
You can define static methods, which pertain to the class as a whole rather than any particular instance. Static methods do not receive any instance or class object in their parameters, and are declared and called with a '.'.
local classic = require 'classic'
local A = classic.class("A")
function A.static.myStaticMethod(x)
return x
end
print(A.myStaticMethod(3))
'mustHave' methods
When defining an abstract base class, which relies on the presence of certain methods but does not provide any implementation for them, it can be useful to mark these methods as being required to be implemented by inheriting classes.
This is akin to pure virtual methods in C++, or an interface in Java.
In classic, marking a method as mustHave()
in a class will cause an error to
be thrown when that class, or descendants, are instantiated - if the method has
not been implemented. This feature can also be used in mixins.
local classic = require 'classic'
local A = classic.class("A")
A:mustHave("essentialMethod")
function A:getResult()
return self:essentialMethod() + 1
end
local B = classic.class("B", A)
function B:essentialMethod()
return 2
end
-- OK: method is implemented.
local b = B()
local C = classic.class("C", A)
-- Error: 'essentialMethod' is marked 'mustHave' but was not implemented.
local c = C()
'final' methods
It can also be useful to indicate that a particular method should not be
overridden by subclasses. This is done using final()
.
Any attempt to override a final method in a subclass will trigger an error.
Methods can also be marked as final in mixins.
You may only mark a method as final after it has been defined.
local classic = require 'classic'
local A = classic.class("A")
function A:finalMethod()
print("This should not be meddled with!")
end
A:final("finalMethod")
local B = classic.class("B", A)
-- Error: this override is no longer permitted.
function B:finalMethod()
print("Attempted meddling!")
end
Metamethods
It is possible to define special methods that override certain operators for an object. Rather than manually setting the metatable, as you would do if you weren't using classic, you simply define appropriately named methods in your class.
local classic = require 'classic'
local A = classic.class("A")
function A:__index(name)
-- custom index method
end
function A:__call(arg1, arg2)
return self[arg1] + arg2
end
-- ...
Metamethods that can be set in this way include:
__add
- addition operator.__call
- function call.__concat
- concatenation (..
) operator.__div
- division operator.__index
- key lookup. (obj[key]
)__mul
- multiplication operator.__newindex
- set value corresponding to a key. (obj[key] = x
)__pow
- exponentiation operator.__sub
- subtraction operator.__tostring
- string conversion.__unm
- unary minus operator.__write
- Torch serialization hook.__read
- Torch serialization hook.
Please consult the Lua/Torch documentation as appropriate, for further details.
Modules
As well as the class system, classic has a way of defining modules. You don't have to use the module system to use the class system. However, it can be a clean way of organising your code and reducing boilerplate.
The rule that it asks you to abide by is as follows: each class should be defined in its own file, and the filename is what determines the name of the class.
The best way of explaining this is with an example.
Here is a typical top-level classic module definition:
local classic = require 'classic'
local my_project = classic.module(...)
my_project:class("MyClass")
my_project:submodule("utils")
return my_project
Now, if you save this in "my_project/init.lua", it can be loaded as usual via
local my_project = require 'my_project'
and when the code is run, the ...
symbol in the aforementioned module
definition will be set to the require name: 'my_project'.
This pattern both ensures that your module's name is set correctly, and saves you typing it lots of times.
The name of the local variable you use when defining the module does not actually matter - so you could equally well write:
local classic = require 'classic'
local M = classic.module(...)
M:class("MyClass")
M:submodule("utils")
return M
which some may find preferable. With this approach, renaming a module is simply a matter of renaming the directory that contains it.
Now, what about these 'class' and 'submodule' calls? These simply outline the things the module contains. The calls do not load the things they refer to; they just register the fact that they exist. The advantage of this is that code can use something from a module without having to load the whole thing. This includes code in the module itself.
So, M:class("MyClass")
says that require 'my_project.MyClass'
is going to
return the definition of that class. Similarly M:submodule("utils")
says that
there is a submodule that can be loaded by calling require 'my_project.utils'
.
With this in mind, we just need to define the corresponding objects in the right
places - note that we can use the ...
trick again to save writing the full
names everywhere.
In my_project/MyClass.lua
, we write:
local MyClass = classic.class(...)
function MyClass:_init(opts)
self.x = opts.x
end
return MyClass
and in my_project/utils/init.lua
, we write:
local utils = classic.module(...)
local my_project = require 'my_project'
function utils.makeTestObject()
return my_project.MyClass{x=3}
end
return utils
We could just as well have saved this as my_project/utils.lua
, but using a
separate subdirectory leaves more opportunity for expanding the utils submodule
without resulting in a single large file.
Note that in the utils submodule, we referred to MyClass by requiring 'my_project', and 'my_project' itself contains 'utils' - but this does not result in a circular dependency! This is because the declaration of 'utils' in 'my_project' does not cause 'utils' to actually be loaded. Only when somebody accesses 'my_project.utils' will the definition really be loaded. This pattern can make things cleaner in large projects.
You can even specify that individual functions should be loaded lazily, if you want to use this pattern everywhere in a project:
local utils = classic.module('utils')
utils:moduleFunction('myFunction')
return utils
This assumes that require 'utils.myFunction'
will return the function in
question.
Adding torch.class instances to classic modules
In module definition:
MyModule = classic.module(...) -- note: this is global.
local MyClass = torch.class('MyModule.MyClass')
MyClass:__init(opts)
self.x = opts.x
end
and in client code:
local my_project = require 'path.to.MyModule'
local obj = my_project.MyClass{x = 1}
Callbacks
You can register your own functions to be called when classic does various things. For instance, for debugging purposes you might want to be notified every time a class is defined.
local classic = require 'classic'
classic.addCallback(classic.events.CLASS_INIT, function(name)
print("A class was defined: ", name)
end)
See the table in classic/init.lua
for the full list of events that you can use
to trigger callbacks, and the details of what the callback functions will be
passed.