Introduction
The meta
module extends Lua with the semantics of LuaGravity.
With a special syntax, the programmer creates reactors instead of functions and
reactive variables instead of conventional Lua variables.
The call to meta.new
returns a new special table t
in which the semantic
extensions of LuaGravity are applied:
local t = meta.new()
function t.funcA () ... end
function t._reactorA () ... end
function t.__reactorB () ... gvt.await(...) end
t.funcA()
t._reactorA() -- equivalent to gvt.call(t._reactorA)
t.__reactorB() -- equivalent to gvt.call(t.__reactorB)
Any function whose name is prefixed by underscores becomes a reactor. For names starting with one underscore, instantaneous reactors are created; for names starting with two underscores, reactors that can await are created. Also, to call a reactor, the conventional function call syntax can be used.
It is also possible to change the global environment of a function to reflect the semantic extensions:
APP = meta.apply(function ()
function funcA () ... end
function _reactorA () ... end
function __reactorB () ... await(...) end
funcA()
_reactorA() -- equivalent to gvt.call(_reactorA)
__reactorB() -- equivalent to gvt.call(__reactorB)
end)
gvt.loop(APP)
The function environment is also extended with all LuaGravity primitives, such
as await
, link
, spawn
, etc.
Reactive Expressions
Reactive expressions are another must of reactive languages. The value of a reactive expression is updated whenever one of its operands changes, always reflecting the operation first defined.
The best way to understand reactive expressions is through a simple example:
meta.apply(function()
_b = 1
_c = 2
_a = _b + _c
print(_a()) -- prints 3
_b = 5
print(_a()) -- prints 7
end)
The reactive variable _a
depends on _b
and _c
so that anytime they
change, the value of _a
is automatically updated to reflect the sum.
As reactive variables are represented as objects, to get their actual values
the call syntax is used, as in _a()
.
Reactive expressions are implemented on top of the available reactivity primitives of LuaGravity.
Lifting
When applying functions to reactive expressions, it is expected that the result become also reactive. However, functions and operators in conventional languages like Lua are not prepared to accept reactive parameters. It is necessary, then, to modify each of these operations to work reactively, a process known as lifting.
LuaGravity provides the L
operator to lift functions.
In the following example, the function assert
is lifted and is recalculated
whenever its parameters change:
_b = 1
L(assert)(_b < 10, 'b must be lesser than 10')
gvt.await(...)
_b = 10 -- yields 'b must be lesser than 10'
Operators
LuaGravity automatically lifts all operators that Lua allows to overload:
+ - * / .. % ^
The other operators are available as pre-defined functions in a meta.apply
environment:
Lua | LuaGravity | Example |
---|---|---|
# | LEN | LEN(t) |
== | EQ | EQ(_a, 1) |
~= | NEQ | NEQ(_a, 1) |
< | LT | LT(_a, 10) |
<= | LE | LE(_a, _b) |
> | GT | GT(a, _a) |
>= | GE | GE(10, _b) |
not | NOT | NOT(_a) |
or | OR | OR(_a, _b) |
and | AND | AND(_a, _b) |
(Note in the previous example that _b<10
should actually be
LT(_b,10)
)
Conditionals
Sometimes it is useful to take actions when a condition is satisfied.
LuaGravity provides the cond
and notcond
operator that can be applied to
reactive expressions and used as conditions in link
and await
calls:
link(notcond(_b), reactorA) -- reactorA is executed when _b is false
await( cond(GT(_a,10)) ) -- the running reactor awaits _a be greater than 10
Integral & Derivative
LuaGravity provides primitives for the integration and derivation (in the sense of calculus) of expressions over the time:
_s = S(10)
_d = D(s)
await(10)
assert(_s() >= 100 and _s() <= 101)
assert(_d() == 10)
The following example defines the position _p
in terms of the speed _v
:
_p = _p0 + S(_v)
Causality Cycles
It is not possible to have reactive variables depending on themselves.
Suppose the position of an object depends on a speed that, in turn, depends on the position:
_v = _pos + 1
_pos = S(_v)
This creates a dependency cycle that, when started, would run forever, freezing the application.
To break such cycles, LuaGravity provides a delay
operator that can be
applied to expressions:
_v = delay(_pos) + 1
_pos = S(_v)
Another situation in which cycles can appear is when trying to build a reactive variable with a loop. Suppose you have an array of reactive variables that you want to concatenate to create another reactive variable that changes whenever one of them changes:
t = { _a, _b, _c, ... }
_all = ''
for i, _v in ipairs(t) do
_all = _all .. _v
end
This code makes the variable _all
depend on itself, when the intention is to
depend on its current dependencies.
The correct form is to use the field _all.src
:
t = { _a, _b, _c, ... }
_all = ''
for i, _v in ipairs(t) do
_all = _all.src .. _v
end
Object Orientation
Although the colon syntax for calling methods in object orientation works fine
with reactors, primitives like link
and await
are not aware of OO.
To work with objects, the meta.new
constructor must receive true
as its
first parameter:
local obj = meta.new(true)
function obj:_reactorA (self,...) ... end
function obj:_reactorB (self,...) ... end
link(obj._reactorA, obj._reactorB)
obj:_reactorB()
obj._reactorB()
This way, whenever _reactorB
is called it always gets obj
as its first
parameter, being it from a link, colon syntax, or even normal call syntax.
API
newt = meta.new (t, env, isObj)
Creates a new table that supports the LuaGravity extensions. |
f, newt = meta.apply (f, env)
Changes the environment of the given function to support the LuaGravity extensions. |
ret = meta.dofile (filename, env)
Executes `filename` with support to the LuaGravity extensions. |
newt = meta.new (t, env, isObj)
Creates a new table that supports the LuaGravity extensions.
The returned table looks at env
for non-existent fields in the returned
table.
If isObj
is true, every reactor defined inside the returned table receives
itself as the first parameter.
Parameters:
t
:[table]
If given, its values are copied tonewt
.env
:[table]
If given, the returned table looks at it for non-existent fields. Defaults to the current environment.isObj
:[boolean]
Whether the returned table should behave as an object or not. Defaults tofalse
.
Returns:
newt
: [table] A reference to the created table.
Changes the environment of the given function to support the LuaGravity extensions.
The environment is also extended with the following primitives:
spawn call kill link unlink await cancel post deactivate reactivate
delay cond L S D
LEN EQ LT LE GT GE NOT OR AND
Parameters:
f
: [function] The function to apply the new environment. If no function is given, it is assumed the function that calledmeta.apply
.env
: [table] Behave as in meta.new.
Returns:
f
: [function] The same function passed as parameter.newt
: [table] The new environment forf
.
ret = meta.dofile (filename, env)
Executes filename
with support to the LuaGravity extensions.
filename
runs in the new environment extended with env
.
Parameters:
filename
: [string] The filename to execute.env
: [table] Behave as in meta.new.
Returns:
ret
: [any] The value returned by executing the file, or its environment.