10. Dynamic objects

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.

10.1. Creating dynamic objects

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.

10.2. Defining values

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())

10.3. Defining methods

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())
}

Warning

You cannot overload methods, that is, providing methods with the same name but different signatures.

Warning

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).

10.4. Querying the properties

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.