Objects
The object model is of key importance as it implements many features used extensively by objects like beam
, sequence
, mtable
, all the commands, all the elements, and the MADX
environment. The aim of the object model is to extend the scripting language with concepts like objects, inheritance, methods, metamethods, deferred expressions, commands and more.
In computer science, the object model of MAD-NG is said to implement the concepts of prototypical objects, single inheritance and dynamic lookup of attributes:
A prototypical object is an object created from a prototype, [1] named its parent.
Single inheritance specifies that an object has only one direct parent.
Dynamic lookup means that undefined attributes are searched in the parents at each read.
A prototype represents the default state and behavior, and new objects can reuse part of the knowledge stored in the prototype by inheritance, or by defining how the new object differs from the prototype. Because any object can be used as a prototype, this approach holds some advantages for representing default knowledge, and incrementally and dynamically modifying them.
Creation
The creation of a new object requires to hold a reference to its parent, i.e. the prototype, which indeed will create the child and return it as if it were returned from a function:
local object in MAD
local obj = object { }
The special root object object
from the MAD
environment is the parent of all objects, including elements, sequences, TFS tables and commands. It provides by inheritance the methods needed to handle objects, environments, and more. In this minimalist example, the created object has object
as parent, so it is the simplest object that can be created.
It is possible to name immutably an object during its creation:
local obj = object 'myobj' { }
print(obj.name) -- display: myobj
Here, [2] obj
is the variable holding the object while the string 'myobj'
is the name of the object. It is important to distinguish well the variable that holds the object from the object’s name that holds the string, because they are very often named the same.
It is possible to define attributes during object creation or afterward:
local obj = object 'myobj' { a=1, b='hello' }
obj.c = { d=5 } -- add a new attribute c
print(obj.name, obj.a, obj.b, obj.c.d) -- display: myobj 1 hello 5
Constructors
The previous object creation can be done equivalently using the prototype as a constructor:
local obj = object('myobj',{ a=1, b='hello' })
An object constructor expects two arguments, an optional string for the name, and a required table for the attributes placeholder, optionally filled with initial attributes. The table is used to create the object itself, so it cannot be reused to create a different object:
local attr = { a=1, b='hello' }
local obj1 = object('obj1',attr) -- ok
local obj2 = object('obj2',attr) -- runtime error, attr is already used.
The following objects creations are all semantically equivalent but use different syntax that may help to understand the creation process and avoid runtime errors:
-- named objects:
local nobj = object 'myobj' { } -- two stages creation.
local nobj = object 'myobj' ({ }) -- idem.
local nobj = object('myobj') { } -- idem.
local nobj = object('myobj')({ }) -- idem.
local nobj = object('myobj', { }) -- one stage creation.
-- unnamed objects:
local uobj = object { } -- one stage creation.
local uobj = object ({ }) -- idem.
local uobj = object() { } -- two stages creation.
local uobj = object()({ }) -- idem.
local uobj = object(nil,{ }) -- one stage creation.
Incomplete objects
The following object creation shows how the two stage form can create an incomplete object that can only be used to complete its construction:
local obj = object 'myobj' -- obj is incomplete, table is missing
print(obj.name) -- runtime error.
obj = obj { } -- now obj is complete.
print(obj.name) -- display: myobj
Any attempt to use an incomplete object will trigger a runtime error with a message like:
file:line: forbidden read access to incomplete object.
or
file:line: forbidden write access to incomplete object.
depending on the kind of access.
Classes
An object used as a prototype to create new objects becomes a class, and a class cannot change, add, remove or override its methods and metamethods. This restriction ensures the behavioral consistency between the children after their creation. An object qualified as final cannot create instances and therefore cannot become a class.
Identification
The object
module extends the typeid module with the is_object(a)
function, which returns true
if its argument a
is an object, false
otherwise:
local is_object in MAD.typeid
print(is_object(object), is_object(object{}), is_object{})
-- display: true true false
It is possible to know the objects qualifiers using the appropriate methods:
print(object:is_class(), object:is_final(), object:is_readonly())
-- display: true false true
Customizing creation
During the creation process of objects, the metamethod __init(self)
is invoked if it exists, with the newly created object as its sole argument to let the parent finalize or customize its initialization before it is returned. This mechanism is used by commands to run their :exec()
method during their creation.
Inheritance
The object model allows to build tree-like inheritance hierarchy by creating objects from classes, themselves created from other classes, and so on until the desired hierarchy is modeled. The example below shows an excerpt of the taxonomy of the elements as implemented by the element module, with their corresponding depth levels in comment:
local object in MAD -- depth level 1
local element = object {...} -- depth level 2
local drift_element = element {...} -- depth level 3
local instrument = drift_element {...} -- depth level 4
local monitor = instrument {...} -- depth level 5
local hmonitor = monitor {...} -- depth level 6
local vmonitor = monitor {...} -- depth level 6
local thick_element = element {...} -- depth level 3
local tkicker = thick_element {...} -- depth level 4
local kicker = tkicker {...} -- depth level 5
local hkicker = kicker {...} -- depth level 6
local vicker = kicker {...} -- depth level 6
Reading attributes
Reading an attribute not defined in an object triggers a recursive dynamic lookup along the chain of its parents until it is found or the root object
is reached. Reading an object attribute defined as a function automatically evaluates it with the object passed as the sole argument and the returned value is forwarded to the reader as if it were the attribute’s value. When the argument is not used by the function, it becomes a deferred expression that can be defined directly with the operator :=
as explained in the section Deferred expression. This feature allows to use attributes holding values and functions the same way and postpone design decisions, e.g. switching from simple value to complex calculations without impacting the users side with calling parentheses at every use.
The following example is similar to the second example of the section Deferred expression, and it must be clear that fun
must be explicitly called to retrieve the value despite that its definition is the same as the attribute v2
.
local var = 10
local fun = \-> var -- here := is invalid
local obj = object { v1 := var, v2 =\-> var, v3 = var }
print(obj.v1, obj.v2, obj.v3, fun()) -- display: 10 10 10 10
var = 20
print(obj.v1, obj.v2, obj.v3, fun()) -- display: 20 20 10 20
Writing attributes
Writing to an object uses direct access and does not involve any lookup. Hence setting an attribute with a non-nil
value in an object hides his definition inherited from the parents, while setting an attribute with nil
in an object restores the inheritance lookup:
local obj1 = object { a=1, b='hello' }
local obj2 = obj1 { a=\s-> s.b..' world' }
print(obj1.a, obj2.a) -- display: 1 hello world
obj2.a = nil
print(obj1.a, obj2.a) -- display: 1 1
This property is extensively used by commands to specify their attributes default values or to rely on other commands attributes default values, both being overridable by the users.
It is forbidden to write to a read-only objects or to a read-only attributes. The former can be set using the :readonly
method, while the latter corresponds to attributes with names that start by __
, i.e. two underscores.
Class instances
To determine if an object is an instance of a given class, use the :is_instanceOf
method:
local hmonitor, instrument, element in MAD.element
print(hmonitor:is_instanceOf(instrument)) -- display: true
To get the list of public attributes of an instance, use the :get_varkeys
method:
for _,a in ipairs(hmonitor:get_varkeys()) do print(a) end
for _,a in ipairs(hmonitor:get_varkeys(object)) do print(a) end
for _,a in ipairs(hmonitor:get_varkeys(instrument)) do print(a) end
for _,a in ipairs(element:get_varkeys()) do print(a) end
The code snippet above lists the names of the attributes set by:
the object
hmonitor
(only).the objects in the hierachy from
hmonitor
toobject
included.the objects in the hierachy from
hmonitor
toinstrument
included.the object
element
(only), the root of all elements.
Examples
Fig. 2 summarizes inheritance and attributes lookup with arrows and colors, which are reproduced by the example hereafter:
local element, quadrupole in MAD.element -- kind
local mq = quadrupole 'mq' { l = 2.1 } -- class
local qf = mq 'qf' { k1 = 0.05 } -- circuit
local qd = mq 'qd' { k1 = -0.06 } -- circuit
local qf1 = qf 'qf1' {} -- element
... -- more elements
print(qf1.k1) -- display: 0.05 (lookup)
qf.k1 = 0.06 -- update strength of 'qf' circuit
print(qf1.k1) -- display: 0.06 (lookup)
qf1.k1 = 0.07 -- set strength of 'qf1' element
print(qf.k1, qf1.k1) -- display: 0.06 0.07 (no lookup)
qf1.k1 = nil -- cancel strength of 'qf1' element
print(qf1.k1, qf1.l) -- display: 0.06 2.1 (lookup)
print(#element:get_varkeys()) -- display: 33 (may vary)
The element quadrupole
provided by the element module is the father of the objects created on its left. The black arrows show the user defined hierarchy of object created from and linked to the quadrupole
. The main quadrupole mq
is a user class representing the physical element, e.g. defining a length, and used to create two new classes, a focusing quadrupole qf
and a defocusing quadrupole qd
to model the circuits, e.g. hold the strength of elements connected in series, and finally the real individual elements qf1
, qd1
, qf2
and qd2
that will populate the sequence. A tracking command will request various attributes when crossing an element, like its length or its strength, leading to lookup of different depths in the hierarchy along the red arrow. A user may also write or overwrite an attribute at different level in the hierarchy by accessing directly to an element, as shown by the purple arrows, and mask an attribute of the parent with the new definitions in the children. The construction shown in this example follows the separation of concern principle and it is still highly reconfigurable despite that is does not contain any deferred expression or lambda function.
Attributes
New attributes can be added to objects using the dot operator .
or the indexing operator []
as for tables. Attributes with non-string keys are considered as private. Attributes with string keys starting by two underscores are considered as private and read-only, and must be set during creation:
mq.comment = "Main Arc Quadrupole"
print(qf1.comment) -- displays: Main Arc Quadrupole
qf.__k1 = 0.01 -- error
qf2 = qf { __k1=0.01 } -- ok
The root object
provides the following attributes:
- name
A lambda returning the string
__id
.- parent
A lambda returning a reference to the parent object.
Warning: the following private and read-only attributes are present in all objects as part of the object model and should never be used, set or changed; breaking this rule would lead to an undefined behavior:
- __id
A string holding the object’s name set during its creation.
- __par
A reference holding the object’s parent set during its creation.
- __flg
A number holding the object’s flags.
- __var
A table holding the object’s variables, i.e. pairs of (key, value).
- __env
A table holding the object’s environment.
- __index
A reference to the object’s parent variables.
Methods
New methods can be added to objects but not classes, using the :set_methods(set)
method with set
being the set of methods to add as in the following example:
sequence :set_methods {
name_of = name_of,
index_of = index_of,
range_of = range_of,
length_of = length_of,
...
}
where the keys are the names of the added methods and their values must be a callable accepting the object itself, i.e. self
, as their first argument. Classes cannot set new methods.
The root object
provides the following methods:
- is_final
A method
()
returning a boolean telling if the object is final, i.e. cannot have instance.- is_class
A method
()
returning a boolean telling if the object is a class, i.e. had/has an instance.- is_readonly
A method
()
returning a boolean telling if the object is read-only, i.e. attributes cannot be changed.- is_instanceOf
A method
(cls)
returning a boolean telling ifself
is an instance ofcls
.- set_final
A method
([a])
returningself
set as final ifa ~= false
or non-final.- set_readonly
A method
([a])
returningself
set as read-only ifa ~= false
or read-write.- same
A method
([name])
returning an empty clone ofself
and named after the stringname
(default:nil
).- copy
A method
([name])
returning a copy ofself
and named after the stringname
(default:nil
). The private attributes are not copied, e.g. the final, class or read-only qualifiers are not copied.- get_varkeys
A method
([cls])
returning both, the list of the non-private attributes ofself
down tocls
(default:self
) included, and the set of their keys in the form of pairs (key, key).- get_variables
A method
(lst, [set], [noeval])
returning a set containing the pairs (key, value) of the attributes listed inlst
. Ifset
is provided, it will be used to store the pairs. Ifnoveval == true
, the functions are not evaluated. The full list of attributes can be retrieved fromget_varkeys
. Shortcutgetvar
.- set_variables
A method
(set, [override])
returningself
with the attributes set to the pairs (key, value) contained inset
. Ifoverride ~= true
, the read-only attributes (with key starting by"__"
) cannot be updated.- copy_variables
A method
(set, [lst], [override])
returningself
with the attributes listed inlst
set to the pairs (key, value) contained inset
. Iflst
is not provided, it is replaced byself.__attr
. Ifset
is an object andlst.noeval
exists, it is used as the list of attributes to copy without function evaluation.[3] Ifoverride ~= true
, the read-only attributes (with key starting by"__"
) cannot be updated. Shortcutcpyvar
.- wrap_variables
A method
(set, [override])
returningself
with the attributes wrapped by the pairs (key, value) contained inset
, where the value must be a callable(a)
that takes the attribute (as a callable) and returns the wrapped value. Ifoverride ~= true
, the read-only attributes (with key starting by"__"
) cannot be updated.The following example shows how to convert the length
l
of an RBEND from cord to arc, [4] keeping its strengthk0
to be computed on the fly:local cord2arc in MAD.gmath local rbend in MAD.element local printf in MAD.utility local rb = rbend 'rb' { angle=pi/10, l=2, k0=\s s.angle/s.l } printf("l=%.5f, k0=%.5f\n", rb.l, rb.k0) -- l=2.00000, k0=0.15708 rb:wrap_variables { l=\l\s cord2arc(l(),s.angle) } -- RBARC printf("l=%.5f, k0=%.5f\n", rb.l, rb.k0) -- l=2.00825, k0=0.15643 rb.angle = pi/20 -- update angle printf("l=%.5f, k0=%.5f\n", rb.l, rb.k0) -- l=2.00206, k0=0.07846
The method converts non-callable attributes into callables automatically to simplify the user-side, i.e.
l()
can always be used as a callable whatever its original form was. At the end,k0
andl
are computed values and updatingangle
affects both as expected.- clear_variables
A method
()
returningself
after setting all non-private attributes tonil
.- clear_array
A method
()
returningself
after setting the array slots tonil
, i.e. clear the list part.- clear_all
A method
()
returningself
after clearing the object except its private attributes.- set_methods
A method
(set, [override])
returningself
with the methods set to the pairs (key, value) contained inset
, where key must be a string (the method’s name) and value must be a callable (the method itself). Ifoverride ~= true
, the read-only methods (with key starting by"__"
) cannot be updated. Classes cannot update their methods.- set_metamethods
A method
(set, [override])
returningself
with the attributes set to the pairs (key, value) contained inset
, where key must be a string (the metamethod’s name) and value must be a callable(the metamethod itself). Ifoverride == false
, the metamethods cannot be updated. Classes cannot update their metamethods.- insert
A method
([idx], a)
returningself
after insertinga
at the positionidx
(default:#self+1
) and shifting up the items at positionsidx..
.- remove
A method
([idx])
returning the value removed at the positionidx
(default:#self
) and shifting down the items at positionsidx..
.- move
A method
(idx1, idx2, idxto, [dst])
returning the destination objectdst
(default:self
) after moving the items fromself
at positionsidx1..idx2
todst
at positionsidxto..
. The destination range can overlap with the source range.- sort
A method
([cmp])
returningself
after sorting in-place its list part using the ordering callable (cmp(ai, aj)
) (default:"<"
), which must define a partial order over the items. The sorting algorithm is not stable.- bsearch
A method
(a, [cmp], [low], [high])
returning the lowest indexidx
in the range specified bylow..high
(default:1..#self
) from the ordered list ofself
that comparestrue
with itema
using the callable (cmp(a, self[idx])
) (default:"<="
for ascending,">="
for descending), orhigh+1
. In the presence of multiple equal items,"<="
(resp.">="
) will return the index of the first equal item while"<"
(resp.">"
) the index next to the last equal item for ascending (resp. descending) order. [5]- lsearch
A method
(a, [cmp], [low], [high])
returning the lowest indexidx
in the range specified bylow..high
(default:1..#self
) from the list ofself
that comparestrue
with itema
using the callable (cmp(a, self[idx])
) (default:"=="
), orhigh+1
. In the presence of multiple equal items in an ordered list,"<="
(resp.">="
) will return the index of the first equal item while"<"
(resp.">"
) the index next to the last equal item for ascending (resp. descending) order. [5]- get_flags
A method
()
returning the flags ofself
. The flags are not inherited nor copied.- set_flags
A method
(flgs)
returningself
after setting the flags determined byflgs
.- clear_flags
A method
(flgs)
returningself
after clearing the flags determined byflgs
.- test_flags
A method
(flgs)
returning a boolean telling if all the flags determined byflgs
are set.- open_env
A method
([ctx])
returningself
after opening an environment, i.e. a global scope, usingself
as the context forctx
(default: 1). The argumentctx
must be either a function or a number defining a call level \(\geq 1\).- close_env
A method
()
returningself
after closing the environment linked to it. Closing an environment twice is safe.- load_env
A method
(loader)
returningself
after calling theloader
, i.e. a compiled chunk, usingself
as its environment. If the loader is a string, it is interpreted as the filename of a script to load, see functionsload
andloadfile
in Lua 5.2 §6.1 for details.- dump_env
A method
()
returningself
after dumping its content on the terminal in the rought form of pairs (key, value), including content of table and object value, useful for debugging environments.- is_open_env
A method
()
returning a boolean telling ifself
is an open environment.- raw_len
A method
()
returning the number of items in the list part of the object. This method should not be confused with the native functionrawlen
.- raw_get
A method
(key)
returning the value of the attributekey
without lambda evaluation nor inheritance lookup. This method should not be confused with the native functionrawget
.- raw_set
A method
(key, val)
setting the attributekey
to the valueval
, bypassing all guards of the object model. This method should not be confused with the native functionrawset
. Warning: use this dangerous method at your own risk!- var_get
A method
(key)
returning the value of the attributekey
without lambda evaluation.- var_val
A method
(key, val)
returning the valueval
of the attributekey
with lambda evaluation. This method is the complementary ofvar_get
, i.e.__index
\(\equiv\)var_val
\(\circ\)var_get
.- dumpobj
A method
([fname], [cls], [patt], [noeval])
returnself
after dumping its non-private attributes in filefname
(default:stdout
) in a hierarchical form down tocls
. If the stringpatt
is provided, it filters the names of the attributes to dump. Iffname == '-'
, the dump is returned as a string in place ofself
. The logicalnoeval
prevents the evaluatation the deferred expressions and reports the functions addresses instead. In the output,self
and its parents are displayed indented according to their inheritance level, and preceeded by a+
sign. The attributes overridden through the inheritance are tagged with \(n\)*
signs, where \(n\) corresponds to the number of overrides since the first definition.
Metamethods
New metamethods can be added to objects but not classes, using the :set_metamethods(set)
method with set
being the set of metamethods to add as in the following example:
sequence :set_metamethods {
__len = len_mm,
__index = index_mm,
__newindex = newindex_mm,
...
}
where the keys are the names of the added metamethods and their values must be a callable accepting the object itself, i.e. self
, as their first argument. Classes cannot set new metamethods.
The root object
provides the following metamethods:
- __init
A metamethod
()
called to finalizeself
before returning from the constructor.- __same
A metamethod
()
similar to the methodsame
.- __copy
A metamethod
()
similar to the methodcopy
.- __len
A metamethod
()
called by the length operator#
to return the size of the list part ofself
.- __call
A metamethod
([name], tbl)
called by the call operator()
to return an instance ofself
created fromname
andtbl
, i.e. usingself
as a constructor.- __index
A metamethod
(key)
called by the indexing operator[key]
to return the value of an attribute determined by key after having performed lambda evaluation and inheritance lookup.- __newindex
A metamethod
(key, val)
called by the assignment operator[key]=val
to create new attributes for the pairs (key, value).- __pairs
A metamethod
()
called by thepairs
function to return an iterator over the non-private attributes ofself
.- __ipairs
A metamethod
()
called by theipairs
function to return an iterator over the list part ofself
.- __tostring
A metamethod
()
called by thetostring
function to return a string describing succinctlyself
.
The following attributes are stored with metamethods in the metatable, but have different purposes:
- __obj
A unique private reference that characterizes objects.
- __metatable
A reference to the metatable itself protecting against modifications.
Flags
The object model uses flags to qualify objects, like class-object, final-object and readonly-object. The difference with boolean attributes is that flags are not inherited nor copied.
The flags of objects are managed by the methods :get_flags
, :set_flags
, :clear_flags
and :test_flags
. Methods like :is_class
, :is_final
and :is_readonly
are roughly equivalent to call the method :test_flags
with the corresponding (private) flag as argument. Note that functions from the typeid
module that check for types or kinds, like is_object
or is_beam
, never rely on flags because types and kinds are not qualifers.
From the technical point of view, flags are encoded into a 32-bit integer and the object model uses the protected bits 29-31, hence bits 0-28 are free of use. Object flags can be used and extended by other modules introducing their own flags, like the element
module that relies on bits 0-4 and used by many commands. In practice, the bit index does not need to be known and should not be used directly but through its name to abstract its value.
Environments
The object model allows to transform an object into an environment; in other words, a global workspace for a given context, i.e. scope. Objects-as-environments are managed by the methods open_env
, close_env
, load_env
, dump_env
and is_open_env
.
Things defined in this workspace will be stored in the object, and accessible from outside using the standard ways to access object attributes:
local object in MAD
local one = 1
local obj = object { a:=one } -- obj with 'a' defined
-- local a = 1 -- see explication below
obj:open_env() -- open environment
b = 2 -- obj.b defined
c =\ -> a..":"..b -- obj.c defined
obj:close_env() -- close environment
print(obj.a, obj.b, obj.c) -- display: 1 2 1:2
one = 3
print(obj.a, obj.b, obj.c) -- display: 3 2 3:2
obj.a = 4
print(obj.a, obj.b, obj.c) -- display: 4 2 4:2
Uncommenting the line local a = 1
would change the last displayed column to 1:2
for the three prints because the lambda defined for obj.c
would capture the local a
as it would exist in its scope. As seen hereabove, once the environment is closed, the object still holds the variables as attributes.
The MADX environment is an object that relies on this powerful feature to load MAD-X lattices, their settings and their “business logic”, and provides functions, constants and elements to mimic the behavior of the global workspace of MAD-X to some extend:
MADX:open_env()
mq_k1 = 0.01 -- mq.k1 is not a valid identifier!
MQ = QUADRUPOLE {l=1, k1:=MQ_K1} -- MADX environment is case insensitive
MADX:close_env() -- but not the attributes of objects!
local mq in MADX
print(mq.k1) -- display: 0.01
MADX.MQ_K1 = 0.02
print(mq.k1) -- display: 0.02
Note that MAD-X workspace is case insensitive and everything is “global” (no scope, namespaces), hence the quadrupole
element has to be directly available inside the MADX environment. Moreover, the MADX object adds the method load
to extend load_env
and ease the conversion of MAD-X lattices. For more details see MADX.
Footnotes