Dynamic objects can have values and methods being added and removed dynamically at runtime. You can think of it as an enhancement over using hash maps and putting closures in them.
Creating a dynamic object is as simple as calling the DynamicObject
function:
let foo = DynamicObject()
Dynamic objects have the following reserved methods, that is, methods that you cannot override:
define(name, value)
allows to define an object property, which can be either a value or a
closure, and
get(name)
gives the value or closure for a property name, or null
if there is none, and
undefine(name)
removes a property from the object, and
mixin(dynobj)
mixes in all the properties of the dynamic object dynobj
, and
copy()
gives a copy of a dynamic object, and
freeze()
locks an object, and calling define
will raise an IllegalStateException
, and
isFrozen()
checks whether a dynamic object is frozen or not, and
properties()
gives the set of entries in the dynamic object, and
invoker(name, type)
which is mostly used by the Golo runtime internals.
Defining values also defines getter and setter methods, as illustrated by the next example:
let person = DynamicObject(): define("name", "MrBean"): define("email", "mrbean@gmail.com") # prints "Mr Bean" println(person: name()) # prints "Mr Beanz" person: name("Mr Beanz") println(person: name())
Calling a setter method for a non-existent property defines it, hence the previous example can be rewritten as:
let person = DynamicObject(): name("MrBean"): email("mrbean@gmail.com") # prints "Mr Bean" println(person: name()) # prints "Mr Beanz" person: name("Mr Beanz") println(person: name())
Dynamic object methods are simply defined as closures. They must take the dynamic object object as
their first argument, and we suggest that you call it this
. You can then define as many parameters
as you want.
Here is an example where we define a toString
-style of method:
local function mrbean = -> DynamicObject(): name("Mr Bean"): email("mrbean@gmail.com"): define("toString", |this| -> this: name() + " <" + this: email() + ">") function main = |args| { let bean = mrbean() println(bean: toString()) bean: email("mrbean@outlook.com") println(bean: toString()) }
You cannot overload methods, that is, providing methods with the same name but different signatures.
It is strongly recommended that you use define
to create and update methods.
Consider the following example:
let obj = DynamicObject(): plop(|this| -> "Plop!")
Any call such as obj: plop()
properly calls plop()
. Because the dynamic object is fresh and new,
the first call to plop
creates a property since it is currently missing.
That being said, the following would fail:
obj: plop(|this| -> "Plop it up!")
Indeed, when the value of a dynamic object property is a function, it is understood to be a method,
hence calling plop
like it would be a setter method fails because there already exists a property
that is a function, and it has a different signature. It needs to be updated as in:
obj: define('plop', |this| -> "Plop it up!")
As a rule of thumb, prefer named setters for values and define
for methods. It is acceptable
to have named definitions for methods if and only if a call happens after the object creation and
before any call to mixin
(remember that it injects properties from other objects, including
methods).
The properties()
method returns a set of entries, as instances of java.util.Map.Entry
. You can
thus write code such as:
function dump = |obj| { foreach prop in obj: properties() { println(prop: getKey() + " -> " + prop: getValue()) } }
Because dynamic object entries mix both values and method handles, do not forget that the predefined
isClosure(obj)
function can be useful to distinguish them.