2.3 Maps
The Map constructor creates an immutable mapping of arbitrary keys to values. A map is indexable using […] with a key, and the result is the corresponding value.
The Map constructor can be used like a function, in which case it accepts keys paired with values in two-item lists to create a map:
> neighborhood["alice"]
Posn(4, 5)
> neighborhood["clara"]
Map.get: no value found for key
key: "clara"
Curly braces {…} can be used as a shorthand for writing Map(...). Within curly braces, the key and value are joined by :. (If a key expression needs to use : itself, the expression will have to be in parentheses.)
> neighborhood["alice"]
Posn(4, 5)
You can also put Map in front of {…}, but that makes more sense with map constructors other than the Map default, such as MutableMap.
To functionally extend a map, use the ++ append operator:
> new_neighborhood["alice"]
Posn(40, 50)
> neighborhood["alice"]
Posn(4, 5)
When ++ is used with a left-hand side that is statically known to be a map, and when the right-hand argument is an immediate map construction with a single element, then the use of ++ is compiled as an efficient single-key update of the map. Whether optimized or general, the ++ operator will only combine certain compatible kinds of values. For example, ++ will append two lists or combine two maps, but it will not combine a list and a map.
Map or its curly-braces shorthand is also an annotation and a binding constructor. As an annotation or binding constructor, Map refers to immutable maps.
In a binding use of Map, the key positions are expressions, not bindings. The binding matches an input that includes the keys, and each corresponding value is matched to the value binding pattern.
fun alice_home({"alice": p}):
p
> alice_home(neighborhood)
Posn(4, 5)
The Map.of annotation constructor takes two annotations, one for keys and one for values:
> locale("alice", neighborhood)
"4, 5"
Note that neighborhood[who] above statically resolves to the use of map lookup, instead of going through a generic lookup at run time.
The MutableMap constructor works similarly to the Map constructor, but it creates a mutable map. A mutable map can be updated using [...] with := just like an array.
def locations = MutableMap{
"alice": Posn(4, 5),
"bob": Posn(7, 9),
}
> locations["alice"] := Posn(40, 50)
> locations["alice"]
Posn(40, 50)
In a map {…} pattern, a & form binds to map for the “rest” of the map, analogous to the way & binds with lists. In a map {…} expression, & splices in the content of another map, similar to the way & works for list construction.
> others
{"alice": Posn(4, 5)}
> {& others, "clara": Posn(8, 2)}
{"alice": Posn(4, 5), "clara": Posn(8, 2)}
Map patterns can also bind repetitions, and map constructions can use repetitions. These repetition constructions tend to go through intermediate lists, and so they tend to be less efficient than using & to work with maps, but they are especially useful when the intent is to convert between lists and maps.
Before ... in a map construction, supply one repetition for keys before : , and supply another repetition for values. The repetitions must have the same length.
> {key: val, ...}
{"a": 1, "b": 2, "c": 3}
In a map pattern, :-separated key and value bindings should appear before .... Unlike key expressions for individual keys, the key part of a repetition binding is a binding. There is no guarantee about the order of the keys and values, except that those two repetitions use the same order (i.e., keys with associated values in parallel).