Maps
Looking up a key returns a value or an error.
A map associates keys with values. Its type is written {K: V}, where K is the key type and V is the value type. A lookup can find the key or fail; the result type represents both cases.
m[k] is a V?, not a V
Write the lookup on its own and the compiler tells you what you have:
$ heroes check maps.hero
error[type_mismatch]: expected `i64`, found `i64?`
at maps.hero:9:12
|
9 | return scores[name]
| ^^^^^^^^^^^^
fix (guess): `.must()` — abort on the error case, giving `i64`
i64? is the fallible type from chapter 5: an integer or an error. If the key is missing, the error code is missing_key. You must handle that possibility before using the number.
Four ways to handle a lookup
# The four ways to read a `V?`, on one line each (§4.6).
function score_of(scores: {str: i64}, name: str) -> i64
return scores[name].default(0)
.default(v) returns the stored value or v if the key is missing. In this example, the result has type i64.
function known(scores: {str: i64}, name: str) -> bool
return !scores[name].is_err()
.is_err() returns true if the key is missing. It does not extract the value. To check that the key is present, the example negates the result with !.
# `.is_err()` asks without extracting; the `match` extracts without aborting.
function report(scores: {str: i64}, name: str) -> str
got = scores[name]
if got.is_err()
return name + " has no score"
return name + " scored " + got.must().to_str()
.must() returns the stored value and stops the program if the key is missing. Here the preceding line has already checked that the key exists. Use this form when you have verified that the lookup succeeds.
# The general way, and the only one that names the error: `match` on `.ok`/`.err`.
function describe(scores: {str: i64}, name: str) -> str
return match scores[name]
.ok v => v.to_str()
.err e => e.code
match handles both results and lets you read the error’s contents. Both branches are required.
Building one
# A map is built as a literal. There is no insert in the built-in inventory
# (§4.20) — `push` is for arrays — so this table is written, not accumulated.
function table() -> {str: i64}
return {"ada": 10, "grace": 9, "alan": 8}
You can write all the pairs together or declare a mutable map with @= and add them one at a time. m[k] @ v inserts the value for key k, replacing the previous value if it exists. An empty map needs an explicit type: m: {str: i64} @= {}.
A map has no order
keys(m) returns the keys without a guaranteed order. The order may differ between runs. Use sort when you need a stable order:
w = make()
for name in sort(keys(w.rooms))
room = room_at(w, name).must()
for direction in sort(keys(room.exits))
there = room.exits[direction].must()
This test visits rooms and exits in a fixed order, so its output stays the same on each run. The Heroes compiler had a similar problem: some lists followed map order, making two builds of the same program differ. Adding the missing sort calls fixed it. Read the account in the section about the compiler written in Heroes. Map comparison does not depend on order: two maps with the same key-value pairs compare equal with ==.
The whole thing, running
test "a missing key is a type, not a crash"
scores = table()
assert score_of(scores, "ada") == 10
assert score_of(scores, "nobody") == 0
assert known(scores, "grace")
assert known(scores, "nobody") == false
assert scores.len() == 3
function total_of(scores: {str: i64}, names: [str]) -> i64
sum: i64 @= 0
for name in names
sum @ sum + score_of(scores, name)
return sum
function main()
print(total_of(table(), ["ada", "grace", "nobody"]))
$ heroes run examples/gallery/10-maps.hero
19
The result is 10 + 9 + 0. The third name is absent from the map; score_of explicitly uses zero as the default.
Next: Values all the way down, learn to build trees with independent values and no pointers. Back to the chapter list. ← Previous: Strings
Run a program of your own: it takes four commands. Or take the whole language in one file, and every program already written in it.