plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
plum-std/Map.plum
| 5f5d99d | 1 | module std |
| b7071c9 | 2 | import std/List |
| b7071c9 | 3 | import std/Option |
| b7071c9 | 4 | import std/Array |
| 73b5e55 | 5 | import std/Bool |
| ca5fd6f | 6 | import std/Number |
| 29313cb | 7 | import std/Str |
| ea9b8c1 | 8 | |
| ea9b8c1 | 9 | # Any type usable as a `Map` key needs a `hash`, so keys landing in different |
| ea9b8c1 | 10 | # buckets (`hash(k) % BUCKET_COUNT`) can be told apart in O(1) without a full |
| ea9b8c1 | 11 | # linear scan of the whole map — see `Str.hash`/`Int.hash` in `str.plum`/ |
| ea9b8c1 | 12 | # `int.plum` for the two implementations `libs/std` provides today. Declared |
| ea9b8c1 | 13 | # here (rather than alongside `ToStr`/`Comparable` in `str.plum`) since `Map` |
| ea9b8c1 | 14 | # is its only real consumer; like `Comparable`/`Readable`/`Writable` |
| ea9b8c1 | 15 | # elsewhere in `libs/std`, `Str`/`Int` don't formally list it in their own |
| ea9b8c1 | 16 | # `implements` clause (that would need them to import `map.plum`, an |
| ea9b8c1 | 17 | # unwanted dependency direction for such foundational types) — this is |
| ea9b8c1 | 18 | # checked the same "unenforced, trust the method exists" way those already |
| ea9b8c1 | 19 | # are (see the README's note on `checkTraitConformance`). |
| ea9b8c1 | 20 | trait Hashable = |
| ea9b8c1 | 21 | hash() -> Int |
| 5f5d99d | 22 | |
| a271f34 | 23 | # A Pair is a grouping of a key with a value. |
| a271f34 | 24 | # |
| a271f34 | 25 | # `toStr` (unlike `Option`'s — see `libs/std/option.plum`'s note on |
| a271f34 | 26 | # `toOptionStr`) IS safe as an ordinary method here: `Pair[K, V]` is only |
| a271f34 | 27 | # ever used as `List[Pair[K, V]]`'s element type inside `Map[K, V]`, and |
| bf629a2 | 28 | # `List[T: ToStr]` already requires its own `T` to support `toStr` — |
| bf629a2 | 29 | # so a `Pair` used as a `List` element always has `K`/`V: ToStr` too, |
| a271f34 | 30 | # and `List`'s own eagerly-compiled `join`/`toStr` (which call `.toStr()` on |
| a271f34 | 31 | # every element) need this to exist for every `Map` specialization anyway. |
| 5f2f962 | 32 | enum Pair[K, V](ToStr) = |
| 5f2f962 | 33 | | Pair(key: K, val: V) |
| 5f5d99d | 34 | |
| a271f34 | 35 | fun toStr(self) -> Str = |
| a271f34 | 36 | return "{self.key.toStr()}: {self.val.toStr()}" |
| a271f34 | 37 | |
| ea9b8c1 | 38 | # Number of buckets every `Map` allocates at construction — fixed for the |
| ea9b8c1 | 39 | # map's whole lifetime, no rehashing/resizing as it grows. A real hash table |
| ea9b8c1 | 40 | # would grow this as `size` outpaces it (keeping average bucket length, and |
| ea9b8c1 | 41 | # so average `get`/`set` cost, roughly constant); that's future work; for now |
| ea9b8c1 | 42 | # a big-enough fixed count keeps small-to-medium maps fast in practice. |
| ea9b8c1 | 43 | BUCKET_COUNT = 16 |
| ea9b8c1 | 44 | |
| ea9b8c1 | 45 | # A Map is a hash table: `BUCKET_COUNT` buckets, each an association-list |
| ea9b8c1 | 46 | # chain (`List[Pair[K, V]]`) of every key hashing into it — O(1) average |
| ea9b8c1 | 47 | # `get`/`set`/`remove` (versus the single, whole-map linear scan a plain |
| ea9b8c1 | 48 | # association list costs), degrading to O(n) only if many keys collide into |
| ea9b8c1 | 49 | # the same bucket. |
| 5f2f962 | 50 | enum Map[K: Hashable, V] = |
| 5f2f962 | 51 | | Map(buckets: Array[List[Pair[K, V]]], size: Int) |
| ea9b8c1 | 52 | |
| ea9b8c1 | 53 | # `Map[Str, Int]()` — explicit generics required (nothing in a no-arg call |
| ea9b8c1 | 54 | # pins down `K`/`V`), builds `BUCKET_COUNT` empty bucket lists up front via |
| ea9b8c1 | 55 | # `Array`'s own `push`-to-grow (see `libs/std/array.plum`'s header comment |
| ea9b8c1 | 56 | # on why `Array` itself has no direct "make me length N" constructor). |
| ea9b8c1 | 57 | fun init() -> Map[K, V] = |
| ea9b8c1 | 58 | b := Array[List[Pair[K, V]]]() |
| ea9b8c1 | 59 | i := 0 |
| ea9b8c1 | 60 | while i < BUCKET_COUNT |
| ea9b8c1 | 61 | b = b.push(List[Pair[K, V]]()) |
| ea9b8c1 | 62 | i = i + 1 |
| ea9b8c1 | 63 | return Map(buckets: b, size: 0) |
| ea9b8c1 | 64 | |
| ea9b8c1 | 65 | # `k`'s bucket index — `hash`'s sign is unconstrained (a plain `%` on a |
| ea9b8c1 | 66 | # negative `Int` stays negative in Plum, matching wasm's signed |
| ea9b8c1 | 67 | # `i64.rem_s`), so this folds a negative result back into `[0, |
| ea9b8c1 | 68 | # BUCKET_COUNT)` rather than indexing `buckets` out of range. |
| ea9b8c1 | 69 | fun bucketIndex(self, k: K) -> Int = |
| ea9b8c1 | 70 | m := k.hash() % BUCKET_COUNT |
| ea9b8c1 | 71 | return m < 0 ? m + BUCKET_COUNT : m |
| ea9b8c1 | 72 | |
| ea9b8c1 | 73 | # Gets the value for `k`, or `None` if absent. |
| 58b01fc | 74 | fun get(self, k: K) -> Option[V] = |
| ea9b8c1 | 75 | current := self.buckets.get(self.bucketIndex(k)).head |
| a271f34 | 76 | while current != None |
| a271f34 | 77 | match current |
| 3a2119e | 78 | Some(Node(Pair(key, val), _, next)) => |
| 3a2119e | 79 | if key == k |
| 3a2119e | 80 | return Some(val) |
| 3a2119e | 81 | current = next |
| a271f34 | 82 | None => |
| a271f34 | 83 | break |
| a271f34 | 84 | return None |
| 58b01fc | 85 | |
| a271f34 | 86 | fun has(self, k: K) -> Bool = |
| a271f34 | 87 | return self.get(k) != None |
| a271f34 | 88 | |
| a271f34 | 89 | # Sets `k`'s value to `v` — replacing an existing entry for `k` in place |
| a271f34 | 90 | # (mutating the stored `Pair`'s `val` field) rather than appending a |
| ea9b8c1 | 91 | # duplicate, so a repeated `set` on the same key doesn't grow its bucket |
| ea9b8c1 | 92 | # unboundedly or leave a stale value reachable by `get`. |
| a271f34 | 93 | fun set(self, k: K, v: V) -> Unit = |
| ea9b8c1 | 94 | bucket := self.buckets.get(self.bucketIndex(k)) |
| ea9b8c1 | 95 | current := bucket.head |
| a271f34 | 96 | while current != None |
| a271f34 | 97 | match current |
| 3a2119e | 98 | Some(Node(pair, _, next)) => |
| 3a2119e | 99 | if pair.key == k |
| 3a2119e | 100 | pair.val = v |
| a271f34 | 101 | return |
| 3a2119e | 102 | current = next |
| a271f34 | 103 | None => |
| a271f34 | 104 | break |
| ea9b8c1 | 105 | bucket.add(Pair(key: k, val: v)) |
| ea9b8c1 | 106 | self.size = self.size + 1 |
| 58b01fc | 107 | |
| a271f34 | 108 | # Sets `k`'s value to `v` only if `k` isn't already present. |
| a271f34 | 109 | fun putIfAbsent(self, k: K, v: V) -> Unit = |
| a271f34 | 110 | if !self.has(k) |
| a271f34 | 111 | self.set(k, v) |
| a271f34 | 112 | |
| ea9b8c1 | 113 | # Removes `k`'s entry, if present. |
| a271f34 | 114 | fun remove(self, k: K) -> Unit = |
| ea9b8c1 | 115 | bucket := self.buckets.get(self.bucketIndex(k)) |
| ea9b8c1 | 116 | current := bucket.head |
| a271f34 | 117 | while current != None |
| a271f34 | 118 | match current |
| a271f34 | 119 | Some(node) => |
| a271f34 | 120 | if node.value.key == k |
| ea9b8c1 | 121 | bucket.unlink(node) |
| ea9b8c1 | 122 | self.size = self.size - 1 |
| a271f34 | 123 | return |
| a271f34 | 124 | current = node.next |
| a271f34 | 125 | None => |
| a271f34 | 126 | break |
| a271f34 | 127 | |
| a271f34 | 128 | fun length(self) -> Int = |
| ea9b8c1 | 129 | return self.size |
| a271f34 | 130 | |
| a271f34 | 131 | fun isEmpty(self) -> Bool = |
| ea9b8c1 | 132 | return self.size == 0 |
| a271f34 | 133 | |
| a271f34 | 134 | fun clear(self) -> Unit = |
| ea9b8c1 | 135 | i := 0 |
| ea9b8c1 | 136 | while i < self.buckets.length() |
| ea9b8c1 | 137 | self.buckets.get(i).clear() |
| ea9b8c1 | 138 | i = i + 1 |
| ea9b8c1 | 139 | self.size = 0 |
| a271f34 | 140 | |
| ea9b8c1 | 141 | # Calls `cb` with each key and value in the map. Order is bucket order, |
| ea9b8c1 | 142 | # then insertion order within a bucket — NOT overall insertion order (that |
| ea9b8c1 | 143 | # would need a separate side list to track, which nothing here needs today). |
| a271f34 | 144 | fun each(self, cb: fn(K, V)) -> Unit = |
| ea9b8c1 | 145 | i := 0 |
| ea9b8c1 | 146 | while i < self.buckets.length() |
| ea9b8c1 | 147 | current := self.buckets.get(i).head |
| ea9b8c1 | 148 | while current != None |
| ea9b8c1 | 149 | match current |
| ea9b8c1 | 150 | Some(Node(Pair(key, val), _, next)) => |
| ea9b8c1 | 151 | cb(key, val) |
| ea9b8c1 | 152 | current = next |
| ea9b8c1 | 153 | None => |
| ea9b8c1 | 154 | break |
| ea9b8c1 | 155 | i = i + 1 |
| a271f34 | 156 | |
| a271f34 | 157 | fun keys(self) -> List[K] = |
| a271f34 | 158 | result := List(head: None, tail: None, size: 0) |
| ea9b8c1 | 159 | self.each(|k, v| result.add(k)) |
| a271f34 | 160 | return result |
| a271f34 | 161 | |
| a271f34 | 162 | fun values(self) -> List[V] = |
| a271f34 | 163 | result := List(head: None, tail: None, size: 0) |
| ea9b8c1 | 164 | self.each(|k, v| result.add(v)) |
| a271f34 | 165 | return result |
| 58b01fc | 166 | |
| accb2b1 | 167 | fun map(self, cb: fn(K, V) -> Pair[X, Y]) -> Map[X, Y] = |
| ea9b8c1 | 168 | b := Array[List[Pair[X, Y]]]() |
| ea9b8c1 | 169 | i := 0 |
| ea9b8c1 | 170 | while i < BUCKET_COUNT |
| ea9b8c1 | 171 | b = b.push(List[Pair[X, Y]]()) |
| ea9b8c1 | 172 | i = i + 1 |
| ea9b8c1 | 173 | result := Map(buckets: b, size: 0) |
| ea9b8c1 | 174 | j := 0 |
| ea9b8c1 | 175 | while j < self.buckets.length() |
| ea9b8c1 | 176 | current := self.buckets.get(j).head |
| ea9b8c1 | 177 | while current != None |
| ea9b8c1 | 178 | match current |
| ea9b8c1 | 179 | Some(Node(Pair(key, val), _, next)) => |
| ea9b8c1 | 180 | pair := cb(key, val) |
| ea9b8c1 | 181 | result.set(pair.key, pair.val) |
| ea9b8c1 | 182 | current = next |
| ea9b8c1 | 183 | None => |
| ea9b8c1 | 184 | break |
| ea9b8c1 | 185 | j = j + 1 |
| accb2b1 | 186 | return result |
| c4ee6ec | 187 | |
| c4ee6ec | 188 | test "get/set/has round-trip and set replaces an existing key in place" |
| ea9b8c1 | 189 | m := Map[Str, Int]() |
| a9a0147 | 190 | assert m.has("a") == False |
| c4ee6ec | 191 | m.set("a", 1) |
| a9a0147 | 192 | assert m.has("a") == True |
| a9a0147 | 193 | assert m.get("a").unwrap() == 1 |
| a9a0147 | 194 | assert m.length() == 1 |
| c4ee6ec | 195 | m.set("a", 2) |
| a9a0147 | 196 | assert m.get("a").unwrap() == 2 |
| a9a0147 | 197 | assert m.length() == 1 |
| c4ee6ec | 198 | |
| c4ee6ec | 199 | test "putIfAbsent only sets a key that isn't already present" |
| ea9b8c1 | 200 | m := Map[Str, Int]() |
| c4ee6ec | 201 | m.putIfAbsent("a", 1) |
| c4ee6ec | 202 | m.putIfAbsent("a", 2) |
| a9a0147 | 203 | assert m.get("a").unwrap() == 1 |
| c4ee6ec | 204 | |
| c4ee6ec | 205 | test "remove deletes an entry and clear empties the map" |
| ea9b8c1 | 206 | m := Map[Str, Int]() |
| c4ee6ec | 207 | m.set("a", 1) |
| c4ee6ec | 208 | m.set("b", 2) |
| c4ee6ec | 209 | m.remove("a") |
| a9a0147 | 210 | assert m.has("a") == False |
| a9a0147 | 211 | assert m.length() == 1 |
| c4ee6ec | 212 | m.clear() |
| a9a0147 | 213 | assert m.isEmpty() == True |
| c4ee6ec | 214 | |
| ea9b8c1 | 215 | test "keys/values list every entry's key/value" |
| ea9b8c1 | 216 | m := Map[Str, Int]() |
| c4ee6ec | 217 | m.set("a", 1) |
| c4ee6ec | 218 | m.set("b", 2) |
| ea9b8c1 | 219 | assert m.length() == 2 |
| ea9b8c1 | 220 | assert m.keys().contains("a") |
| ea9b8c1 | 221 | assert m.keys().contains("b") |
| ea9b8c1 | 222 | assert m.values().contains(1) |
| ea9b8c1 | 223 | assert m.values().contains(2) |
| c4ee6ec | 224 | |
| c4ee6ec | 225 | test "each calls the callback with every key and value" |
| ea9b8c1 | 226 | m := Map[Str, Int]() |
| c4ee6ec | 227 | m.set("a", 1) |
| c4ee6ec | 228 | m.set("b", 2) |
| c4ee6ec | 229 | # A closure captures its outer variables BY VALUE at creation time, so |
| c4ee6ec | 230 | # reassigning a captured `Int` inside the callback wouldn't be visible out |
| c4ee6ec | 231 | # here — mutate a captured `List` (a class, reference semantics) instead. |
| 4a2384c | 232 | seen := List[Int]() |
| c4ee6ec | 233 | m.each(|k, v| seen.add(v)) |
| ea9b8c1 | 234 | assert seen.length() == 2 |
| ea9b8c1 | 235 | assert seen.contains(1) |
| ea9b8c1 | 236 | assert seen.contains(2) |
| accb2b1 | 237 | |
| accb2b1 | 238 | test "map transforms every key/value pair into a new map, changing the value type" |
| ea9b8c1 | 239 | m := Map[Str, Int]() |
| accb2b1 | 240 | m.set("a", 1) |
| accb2b1 | 241 | m.set("b", 2) |
| accb2b1 | 242 | stringified := m.map(|k, v| Pair(key: k, val: v.toStr())) |
| a9a0147 | 243 | assert stringified.get("a").unwrap() == "1" |
| a9a0147 | 244 | assert stringified.get("b").unwrap() == "2" |
| a9a0147 | 245 | assert stringified.length() == 2 |
| ea9b8c1 | 246 | |
| ea9b8c1 | 247 | test "many keys landing in the same bucket still resolve correctly (collision chaining)" |
| ea9b8c1 | 248 | m := Map[Int, Int]() |
| ea9b8c1 | 249 | i := 0 |
| ea9b8c1 | 250 | while i < 40 |
| ea9b8c1 | 251 | m.set(i, i * i) |
| ea9b8c1 | 252 | i = i + 1 |
| ea9b8c1 | 253 | assert m.length() == 40 |
| ea9b8c1 | 254 | j := 0 |
| ea9b8c1 | 255 | while j < 40 |
| ea9b8c1 | 256 | assert m.get(j).unwrap() == j * j |
| ea9b8c1 | 257 | j = j + 1 |
| ea9b8c1 | 258 | m.remove(5) |
| ea9b8c1 | 259 | assert m.has(5) == False |
| ea9b8c1 | 260 | assert m.get(4).unwrap() == 16 |
| ea9b8c1 | 261 | assert m.get(21).unwrap() == 441 |
| ea9b8c1 | 262 | assert m.length() == 39 |