Data structures
Choosing between str and String, arrays and Vector, and the three hash-based collections.
| You need | Use | Because |
|---|---|---|
| A literal or borrowed string | str |
Language primitive, no ownership |
| An owned, growable string | String |
Owns its bytes, implements Drop, can be mutated |
| A sequence sized at compile time | T[N] |
Language primitive, stored inline, never grows |
| A sequence sized at runtime | Vector<T> |
Heap-backed, grows geometrically |
| Lookup by key | HashMap<K, V> |
Average constant-time insert, get and remove |
| Membership only | HashSet<T> |
A thin set over the same table |
Membership over dense u32 indexes |
BitSet |
Packed bit storage |
Standard-library records are constructed with ::new(). Some older ones still answer to ::init()
for compatibility; new code should not use those.
str and String
str is the language’s immutable string type, and every string literal is one.
function main(): i32 {
let name: str = "ignis";
return 0;
}
Reach for it when the bytes already exist and you neither own nor mutate them.
String owns a heap buffer and can grow.
import String from "std::string";
function main(): i32 {
let mut value: String = String::create("ignis");
value.pushStr(" compiler");
return value.length() as i32;
}
Reach for it when you need concatenation, trimming, splitting or cloning — and whenever you want to
store a string inside a Vector, HashMap or HashSet, since those need an owned value.
Arrays and Vector
This is the comparison that matters most, and the two sit on opposite sides of the language boundary.
A fixed-size array T[N] is part of the type system. Its length is part of its type, it lives
inline, and it never grows.
function main(): i32 {
let values: i32[3] = [10, 20, 30];
return values[0];
}
Vector<T> is a standard-library container: heap-backed, growable, cleaned up through Drop.
import Vector from "std::vector";
function main(): i32 {
let mut values: Vector<i32> = Vector::new<i32>();
values.push(10);
values.push(20);
values.push(30);
return values.length() as i32;
}
It carries the operations you would expect — push, pop, get, getMut, clear, map,
filter, fold, sorting.
The rule is short: compile-time size, use the array; runtime size, use the vector.
HashMap
HashMap<K, V> maps keys to values with average constant-time lookup. Keys must satisfy Hash and
Eq, which for your own types means implementing both.
import Eq from "std::hash";
import Hash from "std::collections";
import HashMap from "std::collections";
import Hasher from "std::hash";
@implements(Hash, Eq)
record Key {
id: i32;
hash(&self, hasher: &mut Hasher): void {
let mut state: &mut Hasher = hasher;
state.writeI32(self.id);
return;
}
equals(&self, other: &Key): boolean {
return self.id == other.id;
}
}
function main(): i32 {
let mut map: HashMap<Key, i32> = HashMap::new<Key, i32>();
map.insert(Key { id: 1 }, 100);
map.insert(Key { id: 2 }, 200);
let lookup: Key = Key { id: 2 };
return match (map.get(&lookup)) {
Option::SOME(value) -> *value,
Option::NONE -> -1,
};
}
get hands back an Option, so a missing key is a case you handle rather than a sentinel you
remember to check.
Do not reach for it when order matters, when you need range queries, or when the collection is tiny and fixed — an array is simpler and faster at that size.
HashSet
Same requirements as HashMap, no payload.
import Eq from "std::hash";
import Hash from "std::collections";
import HashSet from "std::collections";
import Hasher from "std::hash";
@implements(Hash, Eq)
record Key {
id: i32;
hash(&self, hasher: &mut Hasher): void {
let mut state: &mut Hasher = hasher;
state.writeI32(self.id);
return;
}
equals(&self, other: &Key): boolean {
return self.id == other.id;
}
}
function main(): i32 {
let mut set: HashSet<Key> = HashSet::new<Key>();
set.insert(Key { id: 7 });
set.insert(Key { id: 7 });
let lookup: Key = Key { id: 7 };
return set.contains(&lookup) ? 0 : 1;
}
If you find yourself keeping a parallel structure with the values that go alongside the keys, you
wanted a HashMap.
BitSet
BitSet stores membership for u32 indexes in packed u64 words. It is the right shape for
compiler-style ids and dataflow sets, where indexes are dense and small.
import BitSet from "std::collections";
function main(): i32 {
let mut visited: BitSet = BitSet::new();
visited.insert(42);
return visited.contains(42) ? 0 : 1;
}
When the values are not dense integers, the packing works against you and HashSet<T> is the
better fit.