30log, in extenso 30 Lines Of Goodness is a minified framework for object-orientation in Lua.
It features named (and unnamed) classes, single inheritance and a basic support for mixins.
It makes 30 lines. No less, no more.
30log is Lua 5.1 and Lua 5.2 compatible.
##Contents
- Download
- Installation
- Quicktour
- Chained initialisation
- Mixins
- Printing classes and objects
- Class Commons support
- Specification
- Source
- Benchmark
- Contributors
##Download
You can download 30log via:
###Bash
git clone git://github.com/Yonaba/30log.git
###Archive
- Zip: 0.9.1 ( latest stable, recommended ) | older versions
- Tar.gz: 0.9.1 ( latest stable, recommended ) | older versions
###LuaRocks
luarocks install 30log
###MoonRocks
luarocks install --server=http://rocks.moonscript.org/manifests/Yonaba 30log
##Installation
Copy the file 30log.lua inside your project folder,
call it using require function. It will return a single local function,
keeping safe the global environment.
##Quicktour ###Creating a class Creating a new class is fairly simple. Just call the returned function, then add some properties to this class :
class = require '30log'
Window = class ()
Window.x, Window.y = 10, 10
Window.width, Window.height = 100,100
You can also make it shorter, packing the default properties and their values within a
table and then pass it as a single argument to the class
function :
class = require '30log'
Window = class { width = 100, height = 100, x = 10, y = 10}
###Named classes
Classes can be named.
To name a class, you will have to set the desired name as a string value to the reserved key __name
:
class = require '30log'
Window = class ()
Window.__name = 'Window'
This feature can be quite useful when debugging your code. See the section printing classes for more details.
###Instances
####Creating instances
You can easily create new instances (objects) from a class using the default instantiation method
named new()
:
appFrame = Window:new()
print(appFrame.x,appFrame.y) --> 10, 10
print(appFrame.width,appFrame.height) --> 100, 100
There is a shorter version though. You can call new class itself with parens, just like a function :
appFrame = Window()
print(appFrame.x,appFrame.y) --> 10, 10
print(appFrame.width,appFrame.height) --> 100, 100
From the two examples above, you might have noticed that once an object is created from a class, it
already shares the properties of his mother class. That's the very basis of inheritance
.
So, by default, the attributes of the newly created object will copy their values from its mother class.
Yet, you can init new objects from a class with custom values for properties. To accomplish that,
you will have to implement your own class constructor. Typically, it is a method (a function) that will be
called whenever the new() method is used from the class to derive a new object, and then define custom attributes and values for this object.
By default, 30log uses the reserved key __init
as a class constructor.
Window = class { width = 100, height = 100, x = 10, y = 10}
function Window:__init(x,y,width,height)
self.x,self.y = x,y
self.width,self.height = width,height
end
appFrame = Window:new(50,60,800,600)
-- same as: appFrame = Window(50,60,800,600)
print(appFrame.x,appFrame.y) --> 50, 60
print(appFrame.width,appFrame.height) --> 800, 600
__init
can also be a table with named keys.
In that case though, the values of each single object's properties will be taken from this table
upon instantiation, no matter what the values passed-in at instantiation would be.
Window = class()
Window.__init = { width = 100, height = 100, x = 10, y = 10}
appFrame = Window:new(50,60,800,600)
-- or appFrame = Window(50,60,800,600)
print(appFrame.x,appFrame.y) --> 10, 10
print(appFrame.width,appFrame.height) --> 100, 100
####Under the hood 30log classes are metatables of their own instances. This implies that one can inspect the mother/son relationship between a class and its instance via Lua's standard function getmetatable.
local aClass = class()
local someInstance = aClass()
print(getmetatable(someInstance) == aClass) --> true
Also, classes are metatables of their derived classes.
local aClass = class()
local someDerivedClass = aClass:extends()
print(getmetatable(someDerivedClass) == aClass) --> true
###Methods and metamethods Objects can call their class methods.
Window = class { width = 100, height = 100, w = 10, y = 10}
function Window:__init(x,y,width,height)
self.x,self.y = x,y
self.width,self.height = width,height
end
function Window:set(x,y)
self.x, self.y = x, y
end
function Window:resize(width, height)
self.width, self.height = width, height
end
appFrame = Window()
appFrame:set(50,60)
print(appFrame.x,appFrame.y) --> 50, 60
appFrame:resize(800,600)
print(appFrame.width,appFrame.height) --> 800, 600
Objects cannot be used to instantiate new objects though.
appFrame = Window:new()
aFrame = appFrame:new() -- Creates an error
aFrame = appFrame() -- Also creates an error
Classes supports metamethods as well as methods. Those metamethods can be inherited.
In the following example, we will use the +
operator to increase the window size.
Window.__add = function(w, size)
w.width = w.width + size
w.height = w.height + size
return w
end
window = Window() -- creates a new Window instance
window:resize(600,300) -- resizes the new window
print(window.width, window.height) --> 600, 300
window = window + 100 -- increases the window dimensions
print(window.width, window.height) --> 700, 400
Frame = Window:extends() -- creates a Frame class deriving from Window class
frame = Frame() -- creates a new Frame instance
frame:resize(400,300) -- Resizes the new frame
print(frame.width, frame.height) --> 400, 300
frame = frame + 50 -- increases the frame dimensions
print(frame.width, frame.height) --> 450, 350
###Inheritance
A class can inherit from any other class using a reserved method named extends
.
Similarly to class
, this method also takes an optional table with named keys as argument
to include new properties that the derived class will implement.
The new class will inherit his mother class properties as well as its methods.
Window = class { width = 100, height = 100, x = 10, y = 10}
Frame = Window:extends { color = 'black' }
print(Frame.x, Frame.y) --> 10, 10
appFrame = Frame()
print(appFrame.x,appFrame.y) --> 10, 10
A derived class can redefine any method implemented in its base class (or mother class).
Therefore, the derived class still has access to his mother class methods and properties via a
reserved key named super
.
-- Let's use this feature to build a class constructor for our `Frame` class.
-- The base class "Window"
Window = class { width = 100, height = 100, x = 10, y = 10}
function Window:__init(x,y,width,height)
self.x,self.y = x,y
self.width,self.height = width,height
end
-- A method
function Window:set(x,y)
self.x, self.y = x, y
end
-- A derived class named "Frame"
Frame = Window:extends { color = 'black' }
function Frame:__init(x,y,width,height,color)
-- Calling the superclass constructor
Frame.super.__init(self,x,y,width,height)
-- Setting the extra class member
self.color = color
end
-- Redefining the set() method
function Frame:set(x,y)
self.x = x - self.width/2
self.y = y - self.height/2
end
-- An appFrame from "Frame" class
appFrame = Frame(100,100,800,600,'red')
print(appFrame.x,appFrame.y) --> 100, 100
-- Calls the new set() method
appFrame:set(400,400)
print(appFrame.x,appFrame.y) --> 0, 100
-- Calls the old set() method in the mother class "Windows"
appFrame.super.set(appFrame,400,300)
print(appFrame.x,appFrame.y) --> 400, 300
###Inspecting inheritance
class.is
can check if a given class derives from another class.
local aClass = class()
local aDerivedClass = aClass:extends()
print(aDerivedClass:is(aClass)) --> true
It also returns true when the given class is not necessarily the immediate ancestor of the calling class.
local aClass = class()
local aDerivedClass = aClass:extends():extends():extends() -- 3-level depth inheritance
print(aDerivedClass:is(aClass)) --> true
Similarly instance.is
can check if a given instance derives from a given class.
local aClass = class()
local anObject = aClass()
print(anObject:is(aClass)) --> true
It also returns true when the given class is not the immediate ancestor.
local aClass = class()
local aDerivedClass = aClass:extends():extends():extends() -- 3-level depth inheritance
local anObject = aDerivedClass()
print(anObject:is(aDerivedClass)) --> true
print(anObject:is(aClass)) --> true
##Chained initialisation
In a single inheritance tree, the __init
constructor can be chained from one class to
another.
This is called initception.
And, yes, it is a joke.
-- A mother class 'A'
local A = Class()
function A.__init(instance,a)
instance.a = a
end
-- Class 'B', deriving from class 'A'
local B = A:extends()
function B.__init(instance, a, b)
B.super.__init(instance, a)
instance.b = b
end
-- Class 'C', deriving from class 'B'
local C = B:extends()
function C.__init(instance, a, b, c)
C.super.__init(instance,a, b)
instance.c = c
end
-- Class 'D', deriving from class 'C'
local D = C:extends()
function D.__init(instance, a, b, c, d)
D.super.__init(instance,a, b, c)
instance.d = d
end
-- Creating an instance of class 'D'
local objD = D(1,2,3,4)
for k,v in pairs(objD) do print(k,v) end
-- Output:
--> a 1
--> d 4
--> c 3
--> b 2
The previous syntax can also be simplified, as follows:
local A = Class()
function A:__init(a)
self.a = a
end
local B = A:extends()
function B:__init(a, b)
B.super.__init(self, a)
self.b = b
end
local C = B:extends()
function C:__init(a, b, c)
C.super.__init(self, a, b)
self.c = c
end
local D = C:extends()
function D:__init(a, b, c, d)
D.super.__init(self, a, b, c)
self.d = d
end
##Mixins
30log provides a basic support for mixins. This is a powerful concept that can
be used to implement a functionality into different classes, even if they do not have any special relationship.
30log assumes a mixin
to be a table containing a set of methods (function).
To include a mixin in a class, use the reserved key named include
.
-- A mixin
Geometry = {
getArea = function(self) return self.width, self.height end,
resize = function(self, width, height) self.width, self.height = width, height end
}
-- Let's define two unrelated classes
Window = class {width = 480, height = 250}
Button = class {width = 100, height = 50, onClick = false}
-- Include the "Geometry" mixin inside the two classes
Window:include(Geometry)
Button:include(Geometry)
-- Let's define objects from those classes
local aWindow = Window()
local aButton = Button()
-- Objects can use functionalities brought by the mixin.
print(aWindow:getArea()) --> 480, 250
print(aButton:getArea()) --> 100, 50
aWindow:resize(225,75)
print(aWindow.width, aWindow.height) --> 255, 75
Note that, when including a mixin into a class, only methods (functions, actually) will be imported into the class. Also, objects cannot include mixins.
aWindow = Window()
aWindow:include(Geometry) -- produces an error
##Properties
It's possible to define properties as pair of getter/setter method as a form of synctatic sugar for using them.
To define a property, use the reserved key name property
.
-- Defining properties
Window = class { _width = 200, _height = 200, max_width = 640, max_height = 480}
function Window:setWidth(width)
self._width = math.min(width, self.max_width)
end
function Window:setHeight(height)
self._height = math.min(height, self.max_height)
end
function Window:getWidth() return self._width end
function Window:getHeight() return self._height end
Window:property("width", "getWidth", "setWidth")
Window:property("height", "getHeight", "setHeight")
someWindow = Window()
print(someWindow.width, someWindow.height) --> 200, 200
someWindow.width = 512
print(someWindow.width, someWindow.height) --> 512, 200
someWindow.height = 1280
print(someWindow.width, someWindow.height) --> 512, 480
It's also possible to redefine getters/setters in subclasses
-- Redefining getters/setters in subclasses
DialogWindow = Window:extends { min_width = 80, min_height = 60}
function DialogWindow:setWidth(width)
self._width = math.max(math.min(width, self.max_width), self.min_width)
end
function DialogWindow:setHeight(height)
self._height = math.max(math.min(height, self.max_height), self.min_height)
end
otherWindow = DialogWindow()
print(otherWindow.width, otherWindow.height) --> 200, 200
otherWindow.width = 12
print(otherWindow.width, otherWindow.height) --> 80, 200
otherWindow.width = 1280
print(otherWindow.width, otherWindow.height) --> 640, 200
-- Defining anonymous getters/setters and read/write-only properties
You can also declare getters/setters using anonymous methods
-- Defining anonymous getters/setters and read/write-only properties
Canvas = class()
function Canvas:__init(parent_window)
self.parent = parent_window
self._random_seed = 0
end
Canvas:property("width", function() return self.parent.width end, nil)
Canvas:property("height", function() return self.parent.height end, nil)
Canvas:property("random_seed", nil, function(value) self._random_seed = value or 0 end)
drawingWindow = Window()
theCanvas = Canvas(drawingWindow)
print(theCanvas.width, theCanvas.height) --> 200, 200
drawingWindow.height = 400
print(theCanvas.width, theCanvas.height) --> 200, 400
theCanvas.width = 250 --> Error: example_code.lua:xxx: The width property is not writeable.
theCanvas.height = 50 --> Error: example_code.lua:xxx: The height property is not writeable.
theCanvas.random_seed = nil --> Sets _random_seed to 0
theCanvas.random_seed = 42 --> Sets _random_seed to 42
print(theCanvas.random_seed) --> Error: example_code.lua:xxx: The random_seed property is not readable.
##Printing classes and objects Any attempt to print or tostring a class or an instance will return the name of the class as a string. This feature is mostly meant for debugging purposes.
-- Let's illustrate this, with an unnamed __Cat__ class:
-- A Cat Class
local Cat = class()
print(Cat) --> "class(?):<table:00550AD0>"
local kitten = Cat()
print(kitten) --> "object(of ?):<table:00550C10>"
The question mark symbol ?
means here the printed class is unnamed (or the object derives from an unnamed class).
-- Let's define a named __Cat__ class now:
-- A Cat Class
local Cat = class()
Cat.__name = 'Cat'
print(Cat) --> "class(Cat):<table:00411858>"
local kitten = Cat()
print(kitten) --> "object(of Cat):<table:00411880>"
##Class Commons
Class-Commons is an interface that provides a common
API for a wide range of object orientation libraries in Lua. There is a small plugin, originally written by TsT
which provides compatibility between 30log and Class-commons.
See here: 30logclasscommons.
##Specification
You can run the included specs with Telescope using the following command from the root foolder:
lua tsc -f specs/*
###Source
###30logclean
30log was initially designed for minimalistic purposes. But then commit after commit, I came up with a source code
that was obviously surpassing 30 lines. As I wanted to stick to the "30-lines" rule, I had to use an ugly syntax which not much elegant, yet 100 % functional.
For those who might be interested though, the file 30logclean.lua contains the full source code,
properly formatted and well indented for your perusal.
###30logglobal
The file 30logglobal.lua features the exact same source as the original 30log.lua,
excepts that it sets a global function named class
. This is convenient for Lua-based frameworks such as Codea.
##Benchmark
Performance tests featuring classes creation, instantiation and such have been included. You can run these tests with the following command with Lua from the root folder, passing to the test script the actual implementation to be tested.
lua performance/tests.lua 30log
Find here an example of output.
##Contributors
- TsT2005, for the original Class-commons support.
##License
This work is under MIT-LICENSE
Copyright (c) 2012-2014 Roland Yonaba
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 AUTHORS OR COPYRIGHT HOLDERS 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.