8.12

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:

def neighborhood = Map(

  ["alice", Posn(4, 5)],

  ["bob", Posn(7, 9)],

)

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

def neighborhood = {

  "alice": Posn(4, 5),

  "bob": Posn(7, 9),

}

> neighborhood["alice"]

Posn(4, 5)

You can also put Map in from of {}, but that makes more sense with map constructors other than the Map default.

To functionally extend a map, use the ++ append operator:

def new_neighborhood = neighborhood ++ {"alice": Posn(40, 50)}

> 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 the default implementation of maps, 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 maps. For example, ++ will append lists and combine default-implementation maps, but it will not combine two vectors or combine a list and a default-implementation map with keys and values.

Map or its curly-braces shorthand is also an annotation and a binding constructor. As an annotation or binding constructor, Map refers to map values genercially, and not to a specific implementation. For example, a list can be passed to a function that expects a Map argument.

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:

fun locale(who, neighborhood :~ Map.of(String, Posn)):

  let p = neighborhood[who]

  p.x +& ", " +& p.y

> locale("alice", neighborhood)

"4, 5"

Unlike ., indexed access via [...] works even without static information to say that the access will succeed. Still, static information can select a more specific and potentially fast indexing operator. For example, buckets[0] above statically resolves to the use of array lookup, instead of going through a generic function for maps 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.

def {"bob": bob_home, & others} = neighborhood

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

def [key, ...] = ["a", "b", "c"]

def [val, ...] = [1, 2, 3]

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

def {key: val, ...} = {"b": 2, "a": 1, "c": 3}

> [key, ...]

["a", "c", "b"]

> [val, ...]

[1, 3, 2]