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 was written with Lua 5.1 in mind, but is compatible with Lua 5.2.

Table of Contents

Download

Bash

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

Archive

LuaRocks

luarocks install 30log

MoonRocks

moonrocks install 30log

[⬆]

Adding 30log to your code

Copy the file 30log.lua inside your project folder, call it using require function. It will return a local table, keeping safe the global environment.

[⬆]

A quicktour of the library

Declaring classes

Let us create a Window class.

class = require "30log"
Window = class("Window")

That's it. The first argument is the name of the class (defined as string). Later, we can query that name by indexing the .name key. This argument is optional.

print(Window.name) -- "Window"

Custom attributes can be added to any class. Here, we can define a "width" and "height" attributes and assign them values.

Window.width, Window.height = 100,100

But we could have also have the same result by passing a table with named keys as a params argument when declaring the Window class. This argument is also optional.

Window = class("Window", {width = 150, height = 100})
print(Window.width) -- 150
print(Window.height) -- 100

When a class has a .name attribute, it is considered to be a named class. In that case, passing that class to tostring or any function that triggers its __tostring metamethod returns the following:

print(Window) -- "class 'Window' (table: 0x0002cdc0)"

In case the class has no name, it is considered to be an unnamed class. In that case, when passing it to a function like print or tostring, the output is slightly different.

Window = class(nil, {width = 150, height = 100}) -- no name argument specified
print(Window.name) -- nil
print(Window.width) -- 150
print(Window.height) -- 100
print(Window) -- "class '?' (table: 0x0002cdb8)"

The ability to turn classes to string is mostly meant for debugging purposes. One can change this behavior by modifying the __tostring function of any class metatable.

Window = class('Window')
getmetatable(Window).__tostring = function(t)
  return "I am class "..(t.name or "unnamed")
end
print(Window) -- "I am class Window"

[⬆]

Creating instances

The class:new() method

An instance of class is created using the class method new():

appWindow = Window:new()

Once created, an instance can access its class attributes.

print(appWindow.width,appWindow.height) -- 100, 100

But this instance has its own copy of those attributes. Changing them will affect neither the class itself, nor any other instance.

-- assigning new values to the instance attributes
appWindow.width, appWindow.height = 720, 480
print(appWindow.width,appWindow.height) -- 720, 480

-- Class attributes are left untouched
print(Window.width, Window.height) -- 100, 100

[⬆]

The class() call way

An instance can also be created calling the class itself as a function. It is just a syntactic sugar.

appWindow = Window() -- same as Window:new()
print(appWindow.width,appWindow.height) -- 100, 100

[⬆]

class:init(...)

From the two examples above, you might have noticed that once an instance is created from a class, it already shares the properties of his mother class.

Yet, instances can be initialized when creating them from a class. In this way, they already have their attributes set with custom values right after being created.

It just requires to implement a class constructor. Typically, it is a method (a function) that will be called right after class:new() method. The class constructor will take as a first argument the instance and customize it.
30log uses the reserved key init for class constructors.

Window = class("Window")
function Window:init(width,height)
  self.width,self.height = width,height
end

appWindow = Window:new(800,600) -- or appFrame = Window(800,600)
print(appWindow.width,appWindow.height) -- 800, 600

init can also be defined as a table with named keys, instead of a function. In that case, any new instance created will get a raw copy of the keys and values found in this table.

Window = class("Window")
Window.init = { width = 500, height = 500}

appWindow = Window()
print(appFrame.width,appFrame.height) --> 500, 500

[⬆]

some other features of instances`

Passing an instance to print or tostring returns a string representing the instance itself. As for classes, this behavior is meant for debugging. And it can be customized from the user code.

-- example with a named class
Window = class("Window")
appWindow = Window()
print(appWindow) -- "instance of 'Window' (table: 0x0002cf70)"

-- example with an unnamed class
Window = class()
appWindow = Window()
print(appWindow) -- "instance of '?' (table: 0x0002cf70)"

Any instance has an attribute .class which points to its class.

Window = class("Window")
appWindow = Window()
print(appWindow.class) -- "class 'Window' (table: 0x0002cdf8)"

Also, 30log classes are metatables of their own instances. This implies that one can inspect the relationship between a class and its instances via Lua's standard function getmetatable.

local aClass = class()
local someInstance = aClass()
print(getmetatable(someInstance) == aClass) -- true

[⬆]

Methods and metamethods

Instances have access their class methods.

Window = class("Window", {width = 100, height = 100})

function Window:init(width,height)
  self.width,self.height = width,height
end

function Window:cap(maxWidth, maxHeight)
  self.width = math.max(self.width, maxWidth)
  self.height = math.max(self.height, maxHeight)
end

appWindow = Window(200, 200)
appWindow:cap(Window.width, Window.height)
print(appWindow.width,appWindow.height) -- 100,100

Instances cannot be used to instantiate new objects though. They are not meant for this.

appWindow = Window:new()
aWindow = appWindow:new() -- Creates an error
aWindow = appWindow()     -- Also creates an error

Classes support metamethods as well as methods. Those metamethods are inherited by subclasses.

function Window:__add(size) 
  self.width = self.width + size
  self.height = self.height + size
  return self
end

local window = Window(600,300)      -- creates a new Window instance
print(window.width, window.height)  -- 600, 300
window = window + 100               -- increases dimensions
print(window.width, window.height)  -- 700, 400

[⬆]

Inheritance

A subclass can be derived from a class from any other class using a reserved method named extend. Similarly to class, this method also takes an optional argument name and an optional table with named keys params as arguments.

The new subclass will inherit his superclass attributes and methods.

Window = class ("Window",{ width = 100, height = 100})
Frame = Window:extend("Frame", { color = "black" })

appFrame = Frame()
print(appFrame.width, appFrame.height, appFrame.color) -- 100,100,"black"

Any subclass has a .super attribute which points to its superclass.

print(Frame.super) -- "class 'Window' (table: 0x0002ceb8)"

A subclass can redefine any method implemented in its superclass without affecting the superclass method itself. Also, the subclass still has access to his mother class methods and properties via a the super key.

-- A base class "Window"
Window = class ("Window", {x = 10, y = 10, width = 100, height = 100})

function Window:init(x, y, width, height)
  Window.set(self, x, y, width, height)
end

function Window:set(x, y, w, h)
  self.x, self.y, self.width, self.height = x, y, w, h
end

-- a "Frame" subclass
Frame = Window:extend({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

Also, classes are metatables of their subclasses.

local aClass = class("aClass")
local someDerivedClass = aClass:extend()
print(getmetatable(someDerivedClass)) -- "class 'aClass' (table: 0x0002cee8)"

[⬆]

Introspection

class.isClass(class, super)

class.isClass returns true if the only argument given, class, is a 30log class.

local aClass = class()
local notaClass = {}
print(class.isClass(aClass)) -- true
print(class.isClass(notaClass)) -- false

If a second argument super is passed, it returns true if and only if class is an immediate subclass of super.

local superclass = class()
local subclass = superclass:extend()
print(class.isClass(subclass, superclass)) -- true 

[⬆]

class.isInstance(instance, class)

class.isInstance returns true if the only argument given, instance, is an instance of a 30log class.

local aClass = class()
local instance = aClass()
local noinstance = {}
print(class.isInstance(instance)) -- true
print(class.isInstance(noinstance)) -- false

If a second argument class is passed, it returns true if and only if instance is an instance of class.

local aClass = class()
local instance = aClass()
print(class.isInstance(instance, aClass)) -- true 

[⬆]

class:extends()

class:extends() returns true if a class derives from a superclass, even if the superclass is not an immediate ancestor.

local superclass = class()
local subclass = superclass:extend():extend():extend()
print(subclass:extends(superclass)) -- true

[⬆]

Mixins

30log provides a basic support for mixins. This is a powerful concept that can be used to share the same functionality among different classes even if they are unrelated.

30log assumes a mixin to be a table containing a set of methods (functions). A mixin is included in a class using class:include() method:

-- A mixin
Geometry = {
  getArea = function(self) return self.width * self.height end,
}

-- Let us define two unrelared classes
Window = class ("Window", {width = 480, height = 250})
Button = class ("Button", {width = 100, height = 50, onClick = false})

-- Include the "Geometry" mixin
Window:include(Geometry)
Button:include(Geometry)

-- Let us define instances from those classes
local aWindow = Window()
local aButton = Button()

-- Instances can use functionalities brought by the mixin.
print(aWindow:getArea()) -- 120000
print(aButton:getArea()) -- 5000

It is possible to check if a class includes a particular mixin using class:includes().

print(Window:includes(Geometry)) -- true
print(Button:includes(Geometry)) -- true

[⬆]

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 Lua from the root foolder:

lua tsc -f specs/*

[⬆]

About the source

30logclean.lua

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 that defines the name of this library, 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.lua

The file 30logglobal.lua features the exact same source as the original 30log.lua, excepts that it sets a global 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/test.lua 30log

Find here an example of output for the latest version of 30log.

[⬆]

Contributors

[⬆]

License

This work is under MIT-LICENSE
Copyright (c) 2012-2015 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

[⬆]