Lesson 11 / 12
Ordered and Set Structures
The same four keys are put into an ordered map and a hash map: the ordered map's traversal gives the same order on every run, the hash map's is never printed — this is the course's most concrete example of the data-must-not-depend-on-the-environment rule. The limiting measurement shows that the ordering guarantee depends on a condition, using the Point type from the Structs lesson.
Published Updated
In this lesson
- Measures that an ordered map's traversal is the same on every run and that a hash map's cannot be printed
- Explains why a hash map is never printed directly, through the random starting state of the hash function
- Shows that set insertion returns a bool and that this return is independent of any ordering guarantee
- Shows that the ordering guarantee depends on a condition, bounding it with a type that fails to satisfy that condition
Contents
The Collections lesson did not print the hash map directly; it first gathered the keys, sorted them, and only then printed them. Why this step was necessary was left open in that lesson. This lesson — the last of the topic’s five data-modeling lessons — fills exactly that gap: what is the guarantee gap between the ordered and the hash structure, and why does this gap force a sorting step in the source?
The four lessons so far measured, in order, a value’s fields, its states, how it is taken apart, and how it is held in bulk. This lesson stands on a different axis: when a choice is made between two structures, does that choice have an observable consequence? The answer is yes — but this consequence does not appear in most of the two structures’ operations, only in one, traversal.
Rust’s standard library offers two separate implementations for both maps and sets: a
hash-based one (HashMap, HashSet) and an ordered one (BTreeMap, BTreeSet). Both
support the same basic operations — insert, query, delete — and carry the same basic
names: insert, contains_key / contains, len. A piece of source, by writing
BTreeMap instead of HashMap, keeps working unchanged most of the time. The difference
between them shows up not in the result of the operations, but in the structure’s
traversal order — and this lesson measures exactly this one difference.
Set Insertion: the bool Return Is the Same in Both Structures
The return type of adding an element to a set is bool: true if the element was genuinely
new, false if the set already carried that element. This behavior is the same for both an
ordered set and a hash set — it is a measurement independent of the ordering guarantee.
// set_insert_bool.rs — set insertion returns bool, independent of the ordering guarantee
use std::collections::{BTreeSet, HashSet};
fn main() {
let keys = ["triangle", "circle", "rectangle", "square"];
let mut ordered_set: BTreeSet<&str> = BTreeSet::new();
for k in keys {
let inserted = ordered_set.insert(k);
println!("{:<30}: {}", format!("ordered_set.insert({})", k), inserted);
}
let inserted_again = ordered_set.insert("circle");
println!("{:<30}: {}", "ordered_set.insert(circle) 2nd", inserted_again);
println!();
let mut hash_set: HashSet<&str> = HashSet::new();
for k in keys {
let inserted = hash_set.insert(k);
println!("{:<30}: {}", format!("hash_set.insert({})", k), inserted);
}
let inserted_again = hash_set.insert("circle");
println!("{:<30}: {}", "hash_set.insert(circle) 2nd", inserted_again);
}
ordered_set.insert(triangle) : true ordered_set.insert(circle) : true ordered_set.insert(rectangle) : true ordered_set.insert(square) : true ordered_set.insert(circle) 2nd: false hash_set.insert(triangle) : true hash_set.insert(circle) : true hash_set.insert(rectangle) : true hash_set.insert(square) : true hash_set.insert(circle) 2nd : false
Four different keys were added to both the ordered set and the hash set; in both
structures, the first four insertions gave true, and re-inserting "circle" gave false.
This run never asks in which order the set will be traversed — it only seeks the answer
to “did this element already exist,” and this question is entirely independent of a set’s
internal order.
DM16. — The claim that “set insertion’s bool return is independent of the ordering
guarantee” is measured by adding the same four keys to both BTreeSet and HashSet and
both giving back exactly the same true/false sequence. ordered_set and hash_set
differ, at the source level, only in their type names — every other line is the same; and
yet the results overlap line by line. This overlap held across five separate runs, because
insert’s bool return looks at whether the element is in the set, not at how the set is
laid out in memory.
The Ordered Map’s Traversal Is the Same on Every Run
BTreeMap is a map that always keeps its keys ordered. This order comes not from the
insertion order but from the keys’ own values: whatever order they are inserted in,
traversal always proceeds from smallest to largest.
// ordered_map_traversal.rs — the ordered map's traversal is the same on every run
use std::collections::BTreeMap;
fn main() {
let keys = ["triangle", "circle", "rectangle", "square"];
let mut ordered_map: BTreeMap<&str, i32> = BTreeMap::new();
for (i, k) in keys.iter().enumerate() {
ordered_map.insert(k, i as i32);
}
for (key, value) in &ordered_map {
println!("{:<27}: {}", format!("ordered_map[{}]", key), value);
}
}
ordered_map[circle] : 1 ordered_map[rectangle] : 2 ordered_map[square] : 3 ordered_map[triangle] : 0
The keys were inserted in the source in the order "triangle", "circle", "rectangle",
"square" — triangle first, square last. But the order seen in the output is entirely
different: circle, rectangle, square, triangle — alphabetical order. A for loop
traversing a BTreeMap does not follow insertion order, it follows the keys’ own mutual
ordering.
This is a guarantee entirely different from Vec’s behavior. As measured in the
Collections lesson, traversing a growable array always preserves insertion order — the
third item added stays in third place. BTreeMap does not give this guarantee; it detaches
keys entirely from insertion order and rearranges them according to their own mutual size
relationship. This rearrangement holds even for a single insertion: if a fifth key, say
"ellipse", were inserted into ordered_map, it would not land at the end of the list —
it would land in its alphabetical place, between "circle" and "rectangle".
DM17. — The claim “the ordered map’s traversal is the same on every run” is not just
left unmeasured here — quite the opposite, this lesson’s own code verification rests on
this exact claim: the output above is compared against a fixed text recorded from a single
run of this lesson. If BTreeMap‘s traversal changed from run to run, this comparison
would sooner or later stop matching. The order of the four recorded lines — circle,
rectangle, square, triangle — overlaps exactly with the keys’ alphabetical order; this
is indirect proof that BTreeMap’s actual internal ordering follows this same order.
This determinism was already an implicitly assumed rule throughout every lesson of the
course: every run had to give a result identical to the output produced at the moment the
source was written to this file. BTreeMap satisfies this rule on its own; whatever the
insertion order, the same set of keys always produces the same traversal order. This makes
BTreeMap not just “ordered,” but also the only map implementation that satisfies this
course’s general requirement.
A Hash Map Is Never Traversed Directly — the Rule Itself
A HashMap‘s traversal order comes not from the keys’ values but from the starting state of
the hash function at that particular run; this state is freshly and randomly determined at
every program start. This randomness is not a flaw but a deliberate design decision: a map
using a fixed hash function can be deliberately slowed down with maliciously chosen input —
an attacker could precompute which keys would fall into the same internal bucket.
Re-choosing the hash function’s starting state randomly on every run makes this
precomputation impossible; the cost is that two separate runs of the same program can
produce a different traversal order for the same keys.
Even if the same four keys are inserted in the same order, they may be traversed in one order in one run and another order in another run. The rule followed throughout the course can be written right here, together with its own justification: a hash map or hash set is never printed directly or traversed as-is in any lesson — because even if the printed output can be verified in this lesson, there is no guarantee it will stay the same in the next run.
This rule does not make the hash map unusable; it only limits which operations are
safe. Reading a value by a specific key, asking whether a key exists, getting the total
element count — none of these depend on traversal order, and all give the same result on
every run. The return value of insert, encountered in the Collections lesson, also falls
into this safe set: reading a key’s previous value as Some/None has nothing to do with
which position that key holds in the map.
The one unsafe operation is traversing the map’s entirety in raw order and using that
order as a result — walking all the pairs with a for loop and printing them as they come,
or taking the first or last element “by its position in traversal order.” In this lesson and
the previous one, every hash structure was either queried key by key, or had its keys
sorted before being traversed; the raw traversal order was never used as a result anywhere.
// hash_map_single_key.rs — a hash map is not traversed directly, a single key is deterministic
use std::collections::HashMap;
fn main() {
let keys = ["triangle", "circle", "rectangle", "square"];
let mut hash_map: HashMap<&str, i32> = HashMap::new();
for (i, k) in keys.iter().enumerate() {
hash_map.insert(k, i as i32);
}
println!("{:<28}: {}", "hash_map[circle]", hash_map["circle"]);
println!("{:<28}: {}", "contains (square)", hash_map.contains_key("square"));
println!("{:<28}: {}", "length", hash_map.len());
}
hash_map[circle] : 1 contains (square) : true length : 4
In this run, hash_map is never traversed anywhere; it is only queried by a single key
(hash_map["circle"]), asked whether a key exists (contains_key), and its total length is
read (len()). All three are results entirely independent of which order the map’s
elements are laid out in; "circle" always carries the value 1, regardless of which
position it happens to occupy in the map.
DM18. — The claim “a hash map’s single-key query is deterministic” is measured by this
run giving exactly the same three lines across five separate runs. This is a different
measurement from the traversal test in the BTreeMap example: there, the whole map was
traversed and its order was verified; here, the map is never traversed at all, only
point queries are checked. Together, the two measurements draw the exact boundary of “which
operations are safe, which are not.”
The practical consequence of this boundary is this: source using a HashMap should not be
declared “unsafe” without regard to how it uses the map. hash_map_single_key.rs shows
exactly HashMap‘s most common usage pattern — reaching a value by a specific key, asking
whether a key exists — and none of this needs traversal order anywhere. What is risky is
writing code that treats a map’s entire order as meaningful; none of the code written
in this lesson does that.
Limiting Measurement: the Ordering Guarantee Is Not Free
For an ordered map or an ordered set to be able to preserve order, the key type has to satisfy a condition: it must always be possible to answer the question “which one comes first” between two keys. This condition is called the ordering requirement, and not every type satisfies it.
The Point type defined in the Structs lesson (x: i32, y: i32) does not satisfy this
condition. There is no single answer to the question “which one comes first” between two
Point values — should it be ordered by x, by y, or by some combination of the two?
Rust’s standard library does not make this decision automatically for Point; source that
tries to put Point values into a BTreeSet or use them as a BTreeMap key does not
compile, because the ordering requirement is not satisfied. This lesson does not place such
source in a code block — source that does not compile does not enter a block — but the
rejection can be described here: the compiler says it does not know how to compare two
Point values.
DM19. — The claim that “the ordering guarantee is not free” is measured here from the
positive side: every key used in this lesson (&str) already satisfies the ordering
requirement — strings can be compared alphabetically, integers can be compared by
magnitude. ordered_map_traversal.rs running and giving the expected output is indirect
proof that this condition is satisfied for &str; a type like Point, which does not
satisfy this condition, being unable to enter the same structure shows that the condition
is required, not optional.
Hash structures ask for a different condition: not that the key be comparable, but that it
be convertible to a hash value. This condition is likewise not automatically satisfied for
Point. The two conditions do not substitute for each other — a type can satisfy one and
not the other — but they share one thing in common: neither is automatically provided on a
“blank” type; the standard library’s own types (like i32, &str) already carry these
conditions, while a user-defined type has to earn them separately.
This is also a reminder of another distinction measured in the Structs lesson: even though
Point { x: 3, y: 4 } and Point { y: 4, x: 3 } were equal field by field, the Point
type itself did not yet carry an automatic answer to “are two values equal” — in that
lesson this comparison was done by hand, writing d.x == e.x && d.y == e.y, not d == e.
Both the ordering requirement and the hashability requirement need this equality condition
first: without knowing when two values count as “the same,” neither an ordering nor a hash
value can be computed. Because Point does not yet automatically carry even this equality
condition, it cannot enter an ordered or a hash structure directly.
How a type earns these conditions — which keyword or declaration gets added — falls outside
this lesson’s scope; only that the condition exists and is not automatically
satisfied for every type is measured here. Because the standard library’s own types carry
these conditions from the start, the keys used throughout every lesson of this course —
integers, strings — never ran into this boundary anywhere; the boundary only becomes
visible when a type like Point, whose fields are defined by the programmer, is involved.
The shared conclusion of five lessons is this: Rust’s data-modeling tools — the struct, the
enum, pattern matching, the collection, the ordered and hash structure — all answered the
same two questions from different angles. Does something written in the source produce a
value, or does it only declare a name; and is an operation’s result read from its return
type, or guessed from “what it does.” The second question became especially clear in this
last lesson: insert returning a bool has nothing to do with a set’s ordering guarantee;
a map’s traversal order, on the other hand, is the product of a condition tied to the key’s
own type.
Summary
- Set insertion’s
boolreturn behaves the same in the ordered and the hash set; it is a measurement independent of the ordering guarantee. Because it carries a return value, insertion here is an expression. - The ordered map’s (
BTreeMap) traversal follows the keys’ own order and is the same on every run. - A hash map (
HashMap) is never printed directly or traversed in any lesson; only order-independent operations like a single-key query, an existence check, and length are used. - The ordering guarantee depends on a condition: the key type has to satisfy the ordering requirement; not every type satisfies this automatically.
Next Step
The topic’s five data-modeling lessons close here: structs, enums, pattern matching, collections, ordered and set structures — across five lessons, the same question was asked throughout: does something written in the source produce a value, or does it only declare a name? None of these five lessons asked where in the source these types and functions can appear — a struct declaration, a map variable, a function, which boundary is it defined within and how is it seen from outside that boundary? The course’s closing lesson opens this question: modules and crates determine which boundary all of this appears within.
To keep your progress and take notes, Log in
My notes
Log in to take notes.