Object Maps

Object maps are hash dictionaries. Properties are all dynamic values and can be freely added and retrieved.

type_of() an object map returns "map".

Tip: Object maps are FAST

Normally, when properties are accessed, copies of the data values are made. This is normally slow.

Object maps have special treatment – properties are accessed via references, meaning that no copies of data values are made.

This makes object map access fast, especially when deep within a properties chain.

// 'obj' is a normal custom type
let x = obj.a.b.c.d;

// The above is equivalent to:
let a_value = obj.a;        // temp copy of 'a'
let b_value = a_value.b;    // temp copy of 'b'
let c_value = b_value.c;    // temp copy of 'c'
let d_value = c_value.d;    // temp copy of 'd'
let x = d_value;

// 'map' is an object map
let x = map.a.b.c.d;        // direct access to 'd'
                            // 'a', 'b' and 'c' are not copied

map.a.b.c.d = 42;           // directly modifies 'd' in 'a', 'b' and 'c'
                            // no copy of any property value is made

map.a.b.c.d.calc();         // directly calls 'calc' on 'd'
                            // no copy of any property value is made

Literal Syntax

Object map literals are built within braces #{} with name:value pairs separated by commas ,:

#{ property : value,, property : value }

#{ property : value,, property : value , } // trailing comma is OK

The property name can be a simple identifier following the same naming rules as variables, or a string literal without interpolation.

Property Access Syntax

Dot notation

The dot notation allows only property names that follow the same naming rules as variables.

object . property

Elvis notation

The Elvis notation is similar to the dot notation except that it returns () if the object itself is ().

// returns () if object is ()
object ?. property

// no action if object is ()
object ?. property = value ;

Index notation

The index notation allows setting/getting properties of arbitrary names (even the empty string).

object [ property ]

Handle Non-Existent Properties

Trying to read a non-existent property returns () instead of causing an error.

This is similar to JavaScript where accessing a non-existent property returns undefined.

let map = #{ foo: 42 };

// Regular property access
let x = map.foo;            // x == 42

// Non-existent property
let x = map.bar;            // x == ()

Check for property existence

Use the in operator to check whether a property exists in an object-map.

let map = #{ foo: 42 };

"foo" in map == true;

"bar" in map == false;

Short-circuit non-existent property access

Use the Elvis operator (?.) to short-circuit further processing if the object is ().

x.a.b.foo();        // <- error if 'x', 'x.a' or 'x.a.b' is ()

x.a.b = 42;         // <- error if 'x' or 'x.a' is ()

x?.a?.b?.foo();     // <- ok! returns () if 'x', 'x.a' or 'x.a.b' is ()

x?.a?.b = 42;       // <- ok even if 'x' or 'x.a' is ()

Default property value

Using the null-coalescing operator to give non-existent properties default values.

let map = #{ foo: 42 };

// Regular property access
let x = map.foo;            // x == 42

// Non-existent property
let x = map.bar;            // x == ()

// Default value for property
let x = map.bar ?? 42;      // x == 42

Built-in Functions

The following methods operate on object maps.

FunctionParameter(s)Description
getproperty namegets a copy of the value of a certain property (() if the property does not exist)
set
  1. property name
  2. new element
sets a certain property to a new value (property is added if not already exists)
lennonereturns the number of properties
is_emptynonereturns true if the object map is empty
clearnoneempties the object map
removeproperty nameremoves a certain property and returns it (() if the property does not exist)
+= operator, mixinsecond object mapmixes in all the properties of the second object map to the first (values of properties with the same names replace the existing values)
+ operator
  1. first object map
  2. second object map
merges the first object map with the second
== operator
  1. first object map
  2. second object map
are the two object maps the same (elements compared with the == operator, if defined)?
!= operator
  1. first object map
  2. second object map
are the two object maps different (elements compared with the == operator, if defined)?
fill_withsecond object mapadds in all properties of the second object map that do not exist in the object map
contains, in operatorproperty namedoes the object map contain a property of a particular name?
keysnonereturns an array of all the property names (in random order)
valuesnonereturns an array of all the property values (in random order)
to_jsonnonereturns a JSON representation of the object map (() is mapped to null, all other data types must be supported by JSON)

Examples

let y = #{              // object map literal with 3 properties
    a: 1,
    bar: "hello",
    "baz!$@": 123.456,  // like JavaScript, you can use any string as property names...
    "": false,          // even the empty string!

    `hello`: 999,       // literal strings are also OK

    a: 42,              // <- syntax error: duplicated property name

    `a${2}`: 42,        // <- syntax error: property name cannot have string interpolation
};

y.a = 42;               // access via dot notation
y.a == 42;

y.baz!$@ = 42;          // <- syntax error: only proper variable names allowed in dot notation
y."baz!$@" = 42;        // <- syntax error: strings not allowed in dot notation
y["baz!$@"] = 42;       // access via index notation is OK

"baz!$@" in y == true;  // use 'in' to test if a property exists in the object map
("z" in y) == false;

ts.obj = y;             // object maps can be assigned completely (by value copy)
let foo = ts.list.a;
foo == 42;

let foo = #{ a:1, };    // trailing comma is OK

let foo = #{ a:1, b:2, c:3 }["a"];
let foo = #{ a:1, b:2, c:3 }.a;
foo == 1;

fn abc() {
    { a:1, b:2, c:3 }  // a function returning an object map
}

let foo = abc().b;
foo == 2;

let foo = y["a"];
foo == 42;

y.contains("a") == true;
y.contains("xyz") == false;

y.xyz == ();            // a non-existent property returns '()'
y["xyz"] == ();

y.len == ();            // an object map has no property getter function
y.len() == 3;           // method calls are OK

y.remove("a") == 1;     // remove property

y.len() == 2;
y.contains("a") == false;

for name in y.keys() {  // get an array of all the property names via 'keys'
    print(name);
}

for val in y.values() { // get an array of all the property values via 'values'
    print(val);
}

y.clear();              // empty the object map

y.len() == 0;

Special Support for OOP

Object maps can be used to simulate object-oriented programming (OOP) by storing data as properties and methods as properties holding function pointers.

If an object map’s property holds a function pointer, the property can simply be called like a normal method in method-call syntax.

This is a short-hand to avoid the more verbose syntax of using the call function keyword.

When a property holding a function pointer or a closure is called like a method, it is replaced as a method call on the object map itself.

let obj = #{
                data: 40,
                action: || this.data += x    // 'action' holds a closure
           };

obj.action(2);                               // calls the function pointer with 'this' bound to 'obj'

obj.call(obj.action, 2);                     // <- the above de-sugars to this

obj.data == 42;

// To achieve the above with normal function pointer call will fail.

fn do_action(map, x) { map.data += x; }      // 'map' is a copy

obj.action = do_action;                      // <- de-sugars to 'Fn("do_action")'

obj.action.call(obj, 2);                     // a copy of 'obj' is passed by value

obj.data == 42;                              // 'obj.data' is not changed