The LuaGravity Meta Language

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 to newt.
  • 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 to false.

Returns:

  • newt: [table] A reference to the created table.

f, newt = meta.apply (f, env)

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:

Returns:

  • f: [function] The same function passed as parameter.
  • newt: [table] The new environment for f.

ret = meta.dofile (filename, env)

Executes filename with support to the LuaGravity extensions. filename runs in the new environment extended with env.

Parameters:

Returns:

  • ret: [any] The value returned by executing the file, or its environment.