F4.04 · part 3 of 3
Hashing & collisions
Both a map lookup and a set membership test rest on the same trick: a hash function
turns a key into an integer, the integer picks a slot, and the value lives there — no
scanning, so the cost does not grow with the collection. When two keys pick the same slot, that is a
collision, resolved in place. Elixir’s term hash is :erlang.phash2.
Hashing the route "/elixir/algorithms/maps" to a slot, and watching a
second key collide with it. Select a step.
Key, hash, slot
The key is hashed to a large integer; reducing that modulo the number of slots gives an index. Lookups repeat the computation and go straight to the slot. Two distinct keys can land together — both are kept and the slot is searched, which stays cheap as long as collisions are rare. Step through it.
The step · select one
role: any term to an integer
result: 48721903
Hash, reduce, place: the same three steps on every read and write, independent of how many keys exist. Collisions are inevitable once there are more keys than slots, so a real implementation grows the table and keeps the average bucket short — which is where the HAMT comes in.
Advanced: the 32-way HAMT
A single modulo table would need resizing and could degrade if many keys collide. Elixir instead stores a
large map as a hash array mapped trie: a tree that consumes the key’s hash a few bits
at a time, branching up to 32 ways at each level. Five bits choose one of 32 children, the next five choose
the next, and so on, so the depth is about log₃₂ n — roughly
four levels for millions of keys, which is why lookup is treated as O(1). Small maps skip the tree and stay a
flat sorted array until they outgrow 32 keys.
Any term can be a key, so the course’s branded Snowflake ids work directly: a 64-bit integer rendered as base62 with a namespace prefix, it hashes like any other key, and it stays stable as a pivot across the BEAM, Node, and Go runtimes that share the queue. That persistent 32-way trie is the whole of F4.05.
A map as a 32-way trie on the key’s hash
:erlang.phash2("/elixir/algorithms/maps") # => 48721903 (a term hash) :erlang.phash2("/elixir/algorithms/maps", 8) # => 7 (bounded to one of 8 slots) # a branded Snowflake id is a term like any other — it hashes and keys the same %{} |> Map.put("PGE0NbWMtkolM0", page) # PGE = page namespace # small map (<= 32 keys): flat sorted array # large map: a 32-way HAMT keyed by chunks of the hash, depth ~ log32 n
F4.04 · a good hash spreads keys across slots
The hash sends each key to a slot; collisions stay rare.
F4.05 · the 32-way HAMT
Elixir stores the entries in a persistent hash array mapped trie — the chapter spine.
That completes F4.04: a map looks up a value by key, a set tests membership, and a hash makes both constant-time. Next module: F4.05 — Hash Array Mapped Tries (HAMT), which builds the 32-way trie sketched above — the start of the persistent-map spine that leads to CHAMP and the branded-CHAMP map.
References
Primary sources for this lesson, and where it connects in the course.
Sources
- Hash table — Wikipedia — hashing and collisions.
Map/MapSet— Elixir documentation — maps and sets in Elixir.