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".
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.
| Function | Parameter(s) | Description |
|---|---|---|
get | property name | gets a copy of the value of a certain property (() if the property does not exist) |
set |
| sets a certain property to a new value (property is added if not already exists) |
len | none | returns the number of properties |
is_empty | none | returns true if the object map is empty |
clear | none | empties the object map |
remove | property name | removes a certain property and returns it (() if the property does not exist) |
+= operator, mixin | second object map | mixes in all the properties of the second object map to the first (values of properties with the same names replace the existing values) |
+ operator |
| merges the first object map with the second |
== operator |
| are the two object maps the same (elements compared with the == operator, if defined)? |
!= operator |
| are the two object maps different (elements compared with the == operator, if defined)? |
fill_with | second object map | adds in all properties of the second object map that do not exist in the object map |
contains, in operator | property name | does the object map contain a property of a particular name? |
drain | function pointer to predicate (usually a closure) | removes all elements (returning them) that return true when called with the predicate function taking the following parameters:
|
retain | function pointer to predicate (usually a closure) | removes all elements (returning them) that do not return true when called with the predicate function taking the following parameters:
|
filter | function pointer to predicate (usually a closure) | constructs a object map with all elements that return true when called with the predicate function taking the following parameters:
|
keys | none | returns an array of all the property names (in random order) |
values | none | returns an array of all the property values (in random order) |
to_json | none | returns 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