30log

A 30-lines library for object-orientation in Lua

30log

Build Status Coverage Status License

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

You can download 30log via:

Bash

git clone git://github.com/Yonaba/30log.git

Archive

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

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

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.

Bitdeli Badge