A hash lets you find a record by its key in the same time whether you have 10 records or 10 million — there is no searching through a list, just a direct jump to the answer. Unlike sorted and index, a hash does not keep its records in order — that is exactly what makes the lookup fast. You can still iterate one and get a predictable order (ascending key), but the hash has to sort a snapshot to do it; see Iterating a Hash at the bottom of this page. List the key fields in brackets: hash<Type[field]>. Combine a hash with a vector when you want both fast lookup and a stable iteration order.
struct Keyword {
name: text
}
struct Data {
h: hash < Keyword[name] >
}
Combining Hash and Vector
Pairing a hash with a vector on the same record type gives you the best of both worlds: the hash for instant lookup by key, and the vector for iterating in insertion order.
struct Count {
t: text,
v: integer
}
struct Counting {
entries: vector < Count >,
lookup: hash < Count[t] >
}
fn fill(c: Counting) {
c.entries =[
{t: "One", v: 1 },
{t: "Two", v: 2 },
{t: "Three", v: 3 },
{t: "Four", v: 4 }
]
}
fn main() {
Basic Lookup
Fill a hash the same way you fill a vector. Look up by key with square brackets. A found record is truthy; a missing key returns null.
d = Data { };
d.h =[ {name: "one" }, {name: "two" }];
d.h +=[ {name: "three" }, {name: "four" }];
assert(d.h["three"], "Key exists");
assert(!d.h["None"], "Missing key returns null");
Hash + Vector Together
Here entries is the vector (keeps insertion order) and lookup is the hash (fast access). Both fields point to the same records — adding to one automatically updates the other.
c = Counting { };
fill(c);
assert(c.lookup["Three"].v == 3, "Hash lookup: Three");
assert(c.lookup["One"].v == 1, "Hash lookup: One");
assert(!c.lookup["Five"], "Missing key returns null");
Iterate in insertion order via the vector; look up by name via the hash. The vector also supports #first, #count, and #remove inside the loop.
add = 0;
for item in c.entries {
add += item.v;
}
assert(add == 10, "Sum via vector iteration: {add}");
Removing a Key
Assigning null to a hash subscript removes that element. Removing a key that is not present is a safe no-op.
d.h["three"] = null;
assert(!d.h["three"], "three was removed");
assert(d.h["one"], "one still present");
d.h["missing"] = null;
assert(d.h["one"], "removing a key that is not there leaves the others alone");
Iterating a Hash
for e in h { ... } walks the hash in ascending key order. The loop variable has the same shape as a sorted<T> iteration — a reference<T> with #index, #count, and #first available. Multi-field keys sort lexicographically.
Under the hood the compiler builds a one-shot sorted snapshot of the hash's record-numbers and walks it, so iteration cost is O(n) + O(n log n) per for, not amortised across calls. For a hot loop, pair the hash with a vector<T> or sorted<T[k]> on the same record type and iterate the companion collection.
e#remove is rejected at compile time — the snapshot is detached from the hash, so removing from it would not actually remove from the hash. Use h[key] = null to remove an entry. Adding the values up cannot show you the order — a sum is the same whichever way you walk. Collect the keys instead: they come out alphabetically, not in the order they went in.
sum = 0;
order = "";
for e in c.lookup { sum += e.v; order += "{e.t} "; }
assert(sum == 10, "Sum via hash iteration: {sum}");
assert(order == "Four One Three Two ", "hash walks in key order: {order}");
}
Run it yourself
This page is a loft program. It runs here, in your browser — press Run, then ask it something.
Stop on a line
// Copyright (c) 2024-2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
// @NAME: Hash
// @TITLE: Instant key lookup with a hash table
// A 'hash' lets you find a record by its key in the same time whether you have 10 records or
// 10 million — there is no searching through a list, just a direct jump to the answer.
// Unlike 'sorted' and 'index', a hash does not keep its records in order — that is exactly what
// makes the lookup fast. You can still iterate one and get a predictable order (ascending key),
// but the hash has to sort a snapshot to do it; see 'Iterating a Hash' at the bottom of this page.
// List the key fields in brackets: 'hash<Type[field]>'.
// Combine a hash with a vector when you want both fast lookup and a stable iteration order.
struct Keyword {
name: text
}
struct Data {
h: hash < Keyword[name] >
}
// ## Combining Hash and Vector
// Pairing a hash with a vector on the same record type gives you the best of both worlds:
// the hash for instant lookup by key, and the vector for iterating in insertion order.
struct Count {
t: text,
v: integer
}
struct Counting {
entries: vector < Count >,
lookup: hash < Count[t] >
}
fn fill(c: Counting) {
c.entries =[
{t: "One", v: 1 },
{t: "Two", v: 2 },
{t: "Three", v: 3 },
{t: "Four", v: 4 }
]
}
fn main() {
// ## Basic Lookup
// Fill a hash the same way you fill a vector. Look up by key with square brackets.
// A found record is truthy; a missing key returns null.
d = Data { };
d.h =[ {name: "one" }, {name: "two" }];
d.h +=[ {name: "three" }, {name: "four" }];
assert(d.h["three"], "Key exists");
assert(!d.h["None"], "Missing key returns null");
// ## Hash + Vector Together
// Here 'entries' is the vector (keeps insertion order) and 'lookup' is the hash (fast access).
// Both fields point to the same records — adding to one automatically updates the other.
c = Counting { };
fill(c);
assert(c.lookup["Three"].v == 3, "Hash lookup: Three");
assert(c.lookup["One"].v == 1, "Hash lookup: One");
assert(!c.lookup["Five"], "Missing key returns null");
// Iterate in insertion order via the vector; look up by name via the hash.
// The vector also supports #first, #count, and #remove inside the loop.
add = 0;
for item in c.entries {
add += item.v;
}
assert(add == 10, "Sum via vector iteration: {add}");
// ## Removing a Key
// Assigning null to a hash subscript removes that element.
// Removing a key that is not present is a safe no-op.
d.h["three"] = null;
assert(!d.h["three"], "three was removed");
assert(d.h["one"], "one still present");
d.h["missing"] = null;
assert(d.h["one"], "removing a key that is not there leaves the others alone");
// ## Iterating a Hash
// `for e in h { ... }` walks the hash in ascending key order. The loop
// variable has the same shape as a `sorted<T>` iteration — a `reference<T>`
// with `#index`, `#count`, and `#first` available. Multi-field keys sort
// lexicographically.
//
// Under the hood the compiler builds a one-shot sorted snapshot of the
// hash's record-numbers and walks it, so iteration cost is
// O(n) + O(n log n) per `for`, not amortised across calls. For a hot
// loop, pair the hash with a `vector<T>` or `sorted<T[k]>` on the same
// record type and iterate the companion collection.
//
// `e#remove` is rejected at compile time — the snapshot is detached from
// the hash, so removing from it would not actually remove from the hash.
// Use `h[key] = null` to remove an entry.
// Adding the values up cannot show you the order — a sum is the same whichever way you walk.
// Collect the keys instead: they come out alphabetically, not in the order they went in.
sum = 0;
order = "";
for e in c.lookup { sum += e.v; order += "{e.t} "; }
assert(sum == 10, "Sum via hash iteration: {sum}");
assert(order == "Four One Three Two ", "hash walks in key order: {order}");
}