Skip to main content

Maps

A map initializer is a semicolon-delimited sequence of zero or more pairs of field names and associated values of a map, enclosed in square brackets []. Maps can also be initialized by evaluating a subroutine or function that returns a map.

m.1 .= [a::'foo'; b::42; c::[]]
console\log(m.1\a) % Expected output: "foo"

Maps are also used for positional elements by just not giving the value a left-hand side field name.

m.2 .= ['foo';123] % two positional elements
console\log(m.2\2) % Expected output: 123

Positional elements can be accessed with 1-based numerical indexes.

toppings .= ['pepperoni', 'bell peppers']
toppings[1] %= pepperoni
Note: Accessing all elements

If you want to grab all list elements and leave the map fields behind, you can access them with the field operator [0], the zeroth element represents only the positional elements of a given map.

You can add fields to a map literal by using the .. or :: assignment operators.

[foo..1;bar::2] % foo is immutable, bar is mutable

We'll go over these operators in greater detail later in this tutorial. For now, just understand that labeling occurs via operators only.

Access

To access a map's fields or positional elements, you use the postcircumfix version of the map operator []. You suppy a numerical value to get positional elements and a text or key value to get at field elements.

some-map[1] % The first element is the 1 index value
some-map[foo]

The first version uses a literal value but the second version will evaluate the foo label which could either invoke a subroutine or acquire direct access to the value in memory.

If you supply a text or key literal, you can get access to the associated field label within the map.

some-map['foo']
some-map["f$('o' ^^ 2)"]

These can also be written with the syntax sugar \ operator.

some-map\1
some-map\foo
some-map\'foo'
some-map\"f$('o' ^^ 2)"

The syntax some-map\foo, some-map['foo'] and some-map\'foo' are equivalent.

Computed Field Names

Map initializer syntax also supports computed field names. That allows you to put an expression in brackets <>, that will be computed and used as the property name.

i := 0
a .= [
<"foo$(i := i ++ 1)"> :: i
<"foo$(i := i ++ 1)"> :: i
<"foo$(i := i ++ 1)"> :: i
]
console\log(a\foo1) %= 1
console\log(a\foo2) %= 2
console\log(a\foo3) %= 3

items .= ['A'; 'B'; 'C']
map .= [
<items>..'Hello'
]
console\log(map) %= "A;B;C" .. "Hello"
console\log(map\"A;B;C") %= "Hello"

param .. 'size'
config .. [
<param> :: 12
<"mobile$(param[1]\upper-case)$(param[2|+])"> .. 4
]

console\log(config) %= [size::12; mobileSize..4]

Spread fields

Map literals support the spread operator. It copies the prefixed map's base fields (excluding subfields) from a provided map into a new map. Note that it also spreads the positional elements as well.

m1 .= [foo :: 'bar'; x :: 42]
m2 .= [foo :: 'qux'; y :: 13]
cloned-map .= [&m1] %= [foo::"bar";x::42]
merged-map .= [&m1; &m2] %= [foo::"qux";x::42;y::13]

Submaps

Submaps are "mini-proto-environments" that allow you to specify label references as a sequence of elements. Each label reference element exists in a kind of "label limbo" where they await assignment. They can be invoked like functions where each argument element gets assigned to the label element at that position. They can also be given to the applicative operator -> which then becomes your named parameters for a function. The difference is that when invoking, you are creating a map with fields. When you supply it to the applicative op, the labels become local labels within the routine body.

foo ++ bar
warehouse .= <[name; maximum; current::0]>
north .= warehouse('Northside', 2,000.-)
south .= warehouse('South River', 2,500.-)
supply-run .= [load; &warehouse] -> (
remaining .= maximum -- current -- load
remaining << 0 => $not-enough-space(-remaining)
current := current ++ load
)

You can spread them with the & operator and they will flatten all their labels as parameters in the main parameter list (itself a submap).

Destructuring

The destructuring syntax is a Rhumb expression that makes it easy to excise just the important data you need from a map and place them into labels in the current scope.

Description

The map literal expression provides an easy way to create ad hoc packages of data.

x .= [1; 2; 3; 4; 5]

The destructuring assignment uses similar syntax, but flipped to the left-hand side of the assignment instead. The map literal now serves to define what elements and fields to destructure from the sourced map.

x .= [1; 2; 3; 4; 5]
[y; z] ^= x
console\log(y) %= 1
console\log(z) %= 2

a .= [b..1; c.. 2]
[.b;.c] ^= a
% equivalent to
b .= a\b; c = [a\c]

Previously Declared Locations

You can even destructure directly into some already existing map.

numbers .= []
map .= [a..1; b..2; c..3]
[a..numbers[1]; b..numbers[2]; c..numbers[>]] ^= map

You can use the append and unshift field ops as well.

Default Value

Each destructured label can have a default value by using the ?? operator. This value will be used only if the value at this label is empty.

[1..a??1] ^= [] % a is 1
[.b??2] ^= [b..empty] % a is 2
[.c??3] ^= [c..0] % c is 0

The right-hand side of the default operator is actually auto-referenced as a subroutine so you can do any kind of expression here.

[.b??console\log('hey!')] ^= [b..2]
% nothing is logged because b is not empty

Slurp expressions

You can take arbitrary amounts of elements of positional elements by using the & operator. This element will greedily take all surrounding elements up to the next positional field given. It also greedily takes all uncaptured fields.

[.a; &others] ^= [a..1;b..2;c..3]
console\log(others) % [b..2;c..3]

[1..first; &others] ^= ['a';'b';'c']
console\log(others) % [1..'b';2..'c']

You can place as many as you want within a destructuring pattern but only the last slurp argument will receive the fields.

[3..prefix; &name; 10..separator; &suffix] .= get-chars(file)
% here, name contains the positional elements from 4 through 9
% suffix contains anything 11 and after
% (and elements 1 and 2 are ignored)

Destructuring with more elements than source.

In a positional destructuring pattern, you can specify elements that do not exist just like you can with nominal destructuring.

foo .= ['one'; 'two']
[red;yellow;green;blue] ^= foo
console\log(red) %= 'one'
console\log(yellow) %= 'two'
console\log(green) %= empty
console\log(blue) %= empty

Swapping variables

Two labels can have their values swapped in one destructuring expression.

a := 1
b := 3
[a; b] ^= [b; a]
console\log(a) %= 3
console\log(b) %= 1

fruits .= ['apples'; 'oranges'; 'bananas']
[fruits\3; fruits\2] ^= [fruits\2; fruits\3]
console\log(fruits) %= [1..'apples'; 2..'bananas'; 3..'oranges';]

Parsing a subroutine map return

You can return a map from a subroutine or function. Destructuring can make working with a map return value more concise.

dangerous .= [n] -> [yes; n ** 1,000,000.-]
[success; reward] ^= dangerous(5)
console\log(success) %= yes
console\log(reward) %= 5,000,000.-

Picking a first positional element

If the map being destructured contains a large number of elements, you can pick an arbitrary point to start your positional elements. In fact, you could just use this syntax for all positional destructuring.

foo .= ['a';'b';'c';'d';'e';'f';'e';'g']
[4..fourth; fifth; sixth] ^= foo
console\log(fourth) %= 'd''
console\log(fifth) %= 'e'
console\log(sixth) %= 'f'

Ignoring a map field or element

You can soft-ignore fields by just not referencing them in a destructuring pattern. However, using the slurp operator will inevitably cause these to be captured. You can explicitly ignore by using the placeholder label.

foo .= [a..'adam'; b..'brian'; c..'charles']
[a.._; &names] ^= foo
console\log(names) %= [b..'brian'; c..'charles']

To ignore positional elements, use the same placeholder but with a positional field.

fruits .= ['apples'; 'oranges'; 'bananas']
[1.._; &non-favorites] ^= fruits
console\log(non-favorites) %= ['oranges'; 'bananas']

The slurp will start from after the most recent positional field has been captured. You can effectively slice into a map by just using destructuring syntax.

name .= ['j'; 'a'; 'c'; 'o'; 'b']
[1.._;&mid;5.._] ^= name

Taking fields from maps passed as function parameters

Maps passed into function as an argument can be destructured into labels with then can be accessed within the subroutine. As for map assignment, the destructuring syntax allows for the new label to have the same name or a different name from the original field and to assign default values for the case when the original map does not define that field.

Consider this map which contains info about a user.

user .= [
id .. 42
display-name :: 'jdoe'
full-name :: [
first-name :: 'Jane'
last-name :: 'Doe'
]
]

Here we show how to destructure a field of the passed map into a label with the same name. The parameter value [.id] indicates that the id field of the map passed to the function should be destructured into a label with the same name, which can then be used from within the subroutine.

user-id .= [[.id]] -> id

console\log(user-id(user)) %= 42

You can define the name of the destructured label. Here we destructure the field named display-name and rename it to dname for use within the subroutine body.

user-display-name .= [[display-name..dname]] -> dname
console\log(user-display-name(user)) %= 'jdoe'

Inner maps can also be destructured. This example shows the field full-name\first-name being destructured into a label called name.

who-is .= [[.display-name; full-name..[first-name..name]]] ->
"$display-name is $name"
console\log(who-is(user)) %= jdoe is Jane

Setting a function parameter's default value

Default values can be specified using assignment operators, and will be used as label values if a specified field does not exist in the passed map.

Below we show a function where the default size is 'big', default co-ordinates are x::0; y::0 and default radius is 25.

draw-chart .= [[
:size??'big'
:coords??[x::0; y::0]
:radius??25
]??[]] -> (
console\log(size; coords; radius)
% do some chart drawing
)

draw-chart([coords::[x::18; y::30]; radius::30])

In the function submap for draw-chart above, the destructured left-hand side has a default value of an empty map ??[].

You could also have written the function without that default. However, if you leave out that default value, the function will look for at least one argument to be supplied when invoked, whereas in its current form, you can call draw-chart without supplying any arguments. Otherwise, you nead to at least supply an empty map literal.

Nested map destructuring

metadata .= [
title :: "Scratchpad"
translations :: [
{
locale :: "en"
localizationTags :: []
lastEdit :: "2023-07-18T21:48:37"
url :: "/en/docs/Tools/Scratchpad"
title :: "Rhumb Environment"
}
]
url :: "/en-US/docs/Tools/Scratchpad"
]

[
title :: english-title
translations :: [
[
title :: locale-title
]
]
] ^= metadata

console\log(english-title)
console\log(locale-title)

<> iteration and destructuring

people .= [
[
name :: "Mike Smith"
family :: [
mother :: "Jane Smith"
father :: "Harry Smith"
sister :: "Samantha Smith"
]
age :: 35
]
[
name :: "Tom Jones"
family :: [
mother :: "Norah Jones"
father :: "Richard Jones"
brother :: "Howard Jones"
]
age :: 25
]
]

people <> [[name..n; family..[father..f]]] ->
console\log("Name: $n, Father: $f")
% "Name: Mike Smith, Father: Harry Smith"
% "Name: Tom Jones, Father: Richard Jones"

Computed map field names and destructuring

Computed field names, like on map literals, can be used with destructuring.

key .= 'z'
[<key>..foo] ^= [z::'bar']
console\log(foo)

Subfields are looked up when destructuring maps

When desturing a map, if a field is not directly on the base map, it will continue to look into the attached subfields.

my-map .= [
foo :: '123'
@my-sub :: [
bar :: '456'
]
]
[:foo; :bar] ^= my-map
console\log(foo) % '123'
console\log(bar) % '456'

Subfields can also be directly destructured

You can reference a subfield by name to destructure its contents.

my-map .= [
foo :: '123'
@my-sub :: [
bar :: '456'
]
]
[:foo; @my-sub[:bar]] ^= my-map
console\log(foo) % '123'
console\log(bar) % '456'