Loxy is short coming from 'Lua Object ProXY'
require 'loxy'
local PI = 3.14
Circle = object({
radius = 0,
getArea = function(self)
return self.radius^2 * PI
end,
})
circle = Circle()
circle.radius = 10
area = circle:getArea()
assert(area == 10^2*PI)
While you create instance, you can set value of member radius
for object circle
(instance of Circle
class) via constructor:
circle = Circle{ radius = 10 }
area = circle:getArea()
assert(area == 10^2*PI)
More about constructors you can see in section Constructor
There is difference between Loxy and traditional Lua access to undefined member (or nil equal).
While Lua returns nil on undefined member, Loxy will throw error
(there is exception for setters/getters - see next section)
t = {}
print(t.a) -- it will be OK due to Lua returns 'nil' for nonexisting member
but with loxy you will get error
.
o = object{}() -- create instance of anonymous class - see section "Anonymous class"
print(o.a) -- throw error "read unknown attribute: a"
Loxy add some syntax sugar for object members:
In example with Circle
you can access:
- property
circle.radius
throughtcircle:getRadius()
method. - method
circle:getArea()
by propertycicrcle.area
It means, following code is valid:
circle = Circle{ radius = 10 }
assert( circle.area == 10 * PI)
assert( circle:getRadius() == 10)
Similar syntax sugar can be used for setter. We will extend our class Circle
with setter:
Circle = object({
radius = 0,
getArea = function(self)
return self.radius^2 * PI
end,
setArea = function(self, area)
self.radius = math.sqrt(area / PI)
end,
})
Now You can use Circle
in following way:
c = Circle({ area = 20^2*PI })
assert(c.radius == 20)
How you can see, in constructor I fill undefined member area
. This pseudo attribute si valid due to method setArea()
There is (in current version) limitation around getter/property:
You may not define property and getter for one attribute in same time
Reason is following: while accessing property, getter has higgher priority than direct access to property. Then if you accessing property inside getter calling, loxy invoke getter repeatly.
C = object({
attr = 0, -- attr
getAttr = function(self) -- attr getter
return self.attr
end
}) -- This is invalid: both property and getter are defined
c = C()
print(c.attr)
If you try to do it, you will get error
while accessing property or getter:
./loxy/object.lua:142: undefined behavior, there are defined both property and getter for: attr
stack traceback:
[C]: in function 'error'
./loxy/object.lua:142: in function <./loxy/object.lua:115>
...
stdin:1: in main chunk
[C]: ?
You can avoid this limitation by using property with diferent name (e.g started by underscore) for use inside setter/getter. You can see usage of this concept on Circle.area
attribute
Concept of getter give you ability to simulate Read Only attribute
C = object({
getConst = function()
return "Read Only Member"
end
})
c = C()
assert(c.const == "Read Only Member")
c.const = 1 -- throws error "write unknown attribute: const"
Loxy implements simple inheritance mechanism throught call object(<Parent>, <Class implementation>)
Base = object({ -- define parent class 'Base'
name = 'Base',
selfIntroduce = function(self)
return "Hello, my name is " .. self.name
end
})
Class = object(Base,{ -- and 'Class' now inherits properties and methods from `Base`
name = 'Class' -- we override 'name' property in 'Class'
})
b,c = Base(),Class()
assert(b:selfIntroduce() == 'Hello, my name is Base')
assert(c:selfIntroduce() == 'Hello, my name is Class')
More complex example you can see in file spec/inheritance_spec.lua
:
local shape = object({ -- create base object
name = 'shape',
draw = function(self) return 'draw '..self.name end
})
local s = shape() -- create shape instance
assert.is.equal(shape:draw(), 'draw shape')
s.name = 'shape instance' -- overwrite name in instance
assert.is.equal(s:draw(), 'draw shape instance')
local circle = object(shape,{ -- inherit from shape
name = 'circle',
radius = 0,
getArea = function(self)
return self.radius^2 * 3.14
end
})
local c = circle() -- create instance
c.radius = 10 -- set instance
assert.is.equal(c:draw(), 'draw circle') -- try to invoke parent class
local filledCircle = object(circle,{ -- inherit from cicrle, but override draw
fill = 'none',
draw = function(self) return string.format("draw %s %s", self.fill, self.name) end,
})
local fc = filledCircle({ fill = 'red' })
assert.is.equal(fc:draw(), 'draw red circle') -- invoke overrided draw()
Loxy provide two kinds of constructor mechanism.
Copy members of table to object. If there is setter for attribute, implicit constructor will invoke it.
You can avoid setter invocation in constructor call by second parameter false
to constructor
C = object({
a = 0,
setA = function(self, a)
self.a = a + 1
end
})
local c1 = C() -- empty constructor call
assert(c1.a == 0) -- no parmeters to constructor - it will inherit value from class C
local c2 = C{ a = 3 } -- implicit constructor invoked
assert(c2.a == 4) -- assignment in constructor invoke setter mechanism if setter exists - it is default behavior
local c3 = C({ a = 3 }, false) -- avoid setter invocation while implicit c-tor
assert(c3.a == 3)
You can override implicit constructor by __init()
method.
C = object({
a = 0,
__init = function(self, a) -- explicit c-tor
self.a = a + 2
end
})
local c = C(2) -- this will invoke your __init() method instead of implicit constructor
assert(c.a == 4)
You can see, __init()
will receive as first param self
followed by all others param sent to constructor
Loxy allows override metamethods:
C = object({
name = 'class',
__tostring = function(self)
return "I'm: " .. self.name
end,
})
c = C{ name = 'instance of C' }
print(c) -- will print: "I'm: instance of C"
Accepted overridable metamethods are:
- strings:
__tostring
,__concat
, - arithmetic:
__add
,__mul
,__sub
,__div
,__unm
,__pow
, - comparable:
__eq
,__lt
,__le
,
Other metamethods are not allowed in current version.
Loxy provide is_a()
function to RTI
C = object({})
c = C()
assert(is_a(c,C) == true) -- use as function
assert(c:is_a(C) == true) -- use as object method
is_a()
function allows testing is not limited to loxy classes. You can compare to any other Lua type
assert(c:is_a({}) == false)
assert(is_a({},C) == false)
assert(is_a(c,1) == false)
Part of loxy library is signal/callback
mechanism.
This mechanism allows invoke callback in properly situation
Class = object({
attr = 0,
onAttrChanged = signal(), -- define signal handler
setAttr = function(self, attr)
if attr ~= self.attr then -- emit signal only if value really changes
self.onAttrChanged(attr)
self.attr = attr
end
end,
})
counter = 0
c = Class()
c.onAttrChanged:connect(function() counter = counter + 1 end)
c.attr = 1 -- now is setter invoked and will emit signal
assert(counter == 1)
c.attr = 1 -- invoke setter again,
assert(counter == 1) -- but signal was not emited due to condition 'attr ~= self.attr'
c.attr = 10 -- invoke setter again, it will emit signal
assert(counter == 2)
__Signal__s can be used independently on loxy objects
Mechanism is very simple, you will create instance of signal
s = signal()
you will connect some callback(s) to signal
s:connect(function(arg) print(arg) end)
s:connect(function(arg) print(arg + 1) end)
and now by calling signal:emit()
you will invoke all connected callbacks and you can send some paramters to registered callbacks
s:emit(1)
There is additional syntax sugar for emitting via Lua __call
metamethod
s(1) -- it is equal to call s:emit(1)
All parameters sent to signal will receive function connected to signal
> s = signal()
> s:connect(function(...) print(...) end)
> s(1,3,4,8,{},6)
1 3 4 8 table: 0x9d281a8 6
>
Calbacks are not limited to closures. You can connect to signal many diferent types.
s = signal()
s:connect(function() print('closure') end)
f = function() print('function') end
s:connect(f)
t = { m = function(self) print('table method') end}
s:connect(t.m)
ti = { m = function(self) print('table method - with instance') end}
s:connect(ti,ti.m)
ts = { m = function(self) print('table method - by function name') end}
s:connect(ts,'m')
tc = setmetatable({},{__call = function() print('metamethod') end})
s:connect(tc)
o = object({ m = function() print('loxy object method') end})()
s:connect(o,o.m)
os = object({ m = function() print('loxy object method - by name') end})()
s:connect(os,'m')
output from emiting s()
will be:
closure
function
table method
table method - with instance
table method - by function name
metamethod
loxy object method
loxy object method - by name
Parameterss sent to signal:connect()
are internally packed by callback()
and later invoked by callback:invoke()
You can see, in ti
, ts
o
and os
usage of instance callback.
In this case, there is additionaly sent self
as fisrt param.
It use Lua syntax sugar. t:m()
is same as t.m(t)
. Same logic is used in callback.
By marshalling mechanism you can receive returned value(s) from callback(s)
s = signal()
s:connect(function() return 1 end)
r = s()
assert(r == 1)
You can inject marshaller by parameter sent to signal()
If you don't provide any marshaller to signal()
, then preddefined marshaller.last
is used
There are two preddefined marshalers:
- marshaller.last - signal emit will return result of last callback
- marshaller.table - signal emit will return table with result of every one registered callback
Last value marshaller usage:
sl = signal(marshaller.last) -- it is equal to call signal() w/o parameter
sl:connect(function() return 1 end)
sl:connect(function() return 2 end)
sl:connect(function() return 3 end)
r = sl() -- r has value 3 - result of last registered callback
A table marshaller usage is similar
st = signal(marshaller.table)
st:connect(function() return 1 end)
st:connect(function() return 2 end)
st:connect(function() return 3 end)
r = st() -- r is now table {1,2,3} - collected result of all callback
Both preddefined marshallers will store just first retuned value of callback
eg. for function() return 9,7,5,3,1 end
will be stored just first value 9
You can create your own marshallers (for example return sum of callbacks)
API for marshaller is very simple
m = marshaller() -- must be callable - it will create marshaller instance
m:add(self,val) -- called on every callback invocation
m:res(self) -- return result collected by add()
For example there is marshaller which will return sum of returned values
SumMarshaller = function()
return {
val = 0,
add = function(self,val)
self.val = self.val + val
end,
res = function(self)
return self.val
end,
}
end
And how to use it
s = signal(SumMarshaller)
s:connect(function() return 5 end)
s:connect(function() return 6 end)
s:connect(function() return 7 end)
assert(s() == 18)
As side effect of calling `object()`` you can directly invoke constructor. You will create, by this way, instance of 'anonymous class'
See pair of parenthesis at end of line - this will invoke constructor and will return new instance
c = object({a = 0})()
It is not allowed to use table with connected metatable as class implementation. It is dou to Loxy internaly handle inheritance througth metatables.
If you try to do it, loxy throws error while object()
is invoked
- ?? access parent methods via 'super'
- add extension method (mixins)
- allow memoizeable getters
- ?? allow "protected" attrs via "_" prefix
- reflection