This is Yet Another Small Library for Lua, providing a foundation for derivative Object-oriented Programming.
Here’s a non-trivial example.
local Map = require 'base' : derive(function (fn)
function fn:has (key)
return rawget(self.data, key) ~= nil
end
function fn:get (key)
return rawget(self.data, key)
end
function fn:set (key, value)
rawset(self.data, key, value)
end
function fn:size ()
local sz = 0
for _, _ in pairs(self.data) do sz = sz + 1 end
return sz
end
return function (self)
self.data = {}
end
end)
local WeakMap = Map:derive(function ()
return function (source, self, mode)
source(self)
setmetatable(self.data, { __mode = mode or 'k' })
end
end
local map = Map()
map:set('a', 1)
map:set('b', 2)
print(map:has('a'), map:get('b'), map:has('c')) -- true, 2, false
local wmap = WeakMap()
wmap:set(map, {})
print(wmap:has(map), wmap:size()) -- true, 1
map = nil
collectgarbage() -- Force GC
print(wmap:size()) -- 0
There are a number of OOP libraries for Lua. Why another?
With base
, I wanted two main things:
-
Tiny, uncomplicated library
-
Clear and concise implementations
I didn’t want auxiliary functions, extra padding tables, or an (overly) confusing lexicon. There are no fluffy, duck-typing methods. No add-ons. Nothing special.
There are two distinct notions with base
: sources, and derivatives. They are relative terms.
In the example above, WeakMap
is a derivative of Map
, which makes Map
a source for WeakMap
. You can think of these as parent/children 'classes', but it is important to make the distinction that everything is hot - functions are shared, not copied.
Base
is the root source for all derivatives.
map
and wmap
are instances of Map
and WeakMap
, respectively.
base
is bilateral in the way that both shared function tables (Foo.fn
) and the derivatives themselves (Foo
) can look to their sources for methods.
If
Bar
is a derivative ofFoo
, andFoo
has a methodFoo:abc
, thenBar
will also have access to that method.
If
b
is an instance ofBaz
, andBaz
is a derivative ofQux
, which has the shared methodQux.fn:xyz
, thenb
will have access to that method.
This is why :derive
is available to all derivatives.
Why 2.0.0
?
Version 2.0.0
is a reimplementation of the same ideas. After using base
for some time, I noticed certain patterns. Mainly, the use of single files for derivatives was common, but lead to a lot of repeated statements. 2.0.0
aims to cut down on those statements, and provide a more concise implementation pattern.
A typical single derivative file, shown in both styles.
1.0.0
:
local Foobar = require 'base' : derive(function (_, self, ...)
self.qux = { ... }
end)
function Foobar:satic_method () [[ ... ]] end
function Foobar.fn:foo () [[ ... ]] end
function Foobar.fn:bar () [[ ... ]] end
return Foobar
2.0.0
:
return require 'base' : derive(function (fn, D)
function D:static_method () [[ ... ]] end
function fn:foo () [[ ... ]] end
function fn:bar () [[ ... ]] end
return function (self, ...)
self.qux = { ... }
end
end)
There’s not a whole lot to base
.
local Base = require 'base'
In this case, the return value from require
is a singleton table, which we call Base
.
Base
has a single method, used to create derivatives, :derive
, which in turn takes a single argument, context
.
context
is a function with the signature (fn, Derivative) → function
. This function is called when the new derivative is formed, and is passed the following:
-
fn
is the shared function table, a shortcut forDerivative.fn
. -
Derivative
is the new derivative.
local List = Base:derive(function (fn, Derivative)
[[ ... ]]
end)
Each derivative has a shared function table, .fn
, which can be used to create methods that any instances of the derivative, or instances of any derivatives of the derivative have access to.
-
Note:
Base
also has a shared function table, in the event you want to add some kind of universally shared method. However, generally speaking, this is not a great idea.
local List = Base:derive(function (fn, Derivative)
function fn:each (action)
for i, v in ipairs(self.data) do
action(v, i)
end
end
[[ ... ]]
end)
The return value of context
must be a function, which acts as an initializer for new instances of the derivative. We’ll simply call it initializer
.
The function signature of initializer
depends on whether you are deriving a new derivative directly from Base
or not:
When deriving a new derivative directly from
Base
, thesource
argument is absent, and the argument list begins fromself
.
-
source
is a function which provides access to the initializer of the derivative's closest source. It has the signature(instance, …) → nil
. -
self
is the newly formed instance. -
…
are any arguments passed to the constructor.
local List = Base:derive(function (fn, Derivative)
return function (self, ...)
self.data = { ... }
end
end)
local List2 = List:derive(function (fn, Derivative)
function fn:print ()
self:each(print)
end
return function (source, self, ...)
source(self, ...)
end
end)
All derivatives act as constructors when directly invoked, returning the newly formed instance.
-
Note:
Base
is unique in that it is not a derivative, has no initializer, and does not act as a constructor.
local ls = List('a', 'b', 'c')
ls:each(print)
local ls2 = List2('d', 'e', 'f')
ls2:print()
It should be noted that, for simplicity’s sake, derivatives, their shared function tables, and their instances all act as their own metatables. You might notice an index
metaproperty on each object created with this library, as well as some extras on derivatives. It’s best to not mess with these members.
MIT, just like Lua.