Skip to content

Commit 1e7faef

Browse files
cknittclaude
andcommitted
Add getWithDefault and getOrThrow to Belt.HashMap
Belt.HashMap, Belt.HashMap.Int and Belt.HashMap.String gain getWithDefault and getOrThrow, the same lookups Belt.Map and Belt.MutableMap already have. They walk the bucket directly, so a hit allocates no option; getOrThrow throws Not_found like the other Belt maps. Example (rescript): let m = Belt.HashMap.Int.fromArray([(1, "a")]) m->Belt.HashMap.Int.getWithDefault(2, "z") == "z" m->Belt.HashMap.Int.getOrThrow(1) == "a" Review: Why getWithDefault and not getOr, as the issue title says? Belt names this lookup getWithDefault everywhere (Option, Map, MutableMap and their Int/String variants) and has no getOr; the new functions follow Belt. getOrThrow is the name Belt already uses, without a deprecated getExn alias for a new API. Area: runtime tests Output: changed tests/belt_tests/src/belt_hashmap_test.mjs (new tests); no existing program compiles differently Tests: ~tests/belt_tests/src/belt_hashmap_test.res plus docstring examples in Belt_HashMap.resi, Belt_HashMapInt.resi, Belt_HashMapString.resi Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kr4cjoUi2f5VpMvE6wT9bo Signed-off-by: Christoph Knittel <ck@cca.io>
1 parent 58da277 commit 1e7faef

9 files changed

Lines changed: 313 additions & 0 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Add `getWithDefault` and `getOrThrow` to `Belt.HashMap`, `Belt.HashMap.Int` and `Belt.HashMap.String`.

‎packages/@rescript/belt/src/Belt_HashMap.res‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,40 @@ let get = (h, key) => {
170170
}
171171
}
172172

173+
let rec getWithDefaultAux = (~eq, key, buckets, default) =>
174+
switch C.toOpt(buckets) {
175+
| None => default
176+
| Some(cell) =>
177+
if eq(key, cell.N.key) {
178+
cell.N.value
179+
} else {
180+
getWithDefaultAux(~eq, key, cell.N.next, default)
181+
}
182+
}
183+
184+
let getWithDefault = (h, key, default) => {
185+
let h_buckets = h.C.buckets
186+
let nid = Int.bitwiseAnd(Belt_Id.getHashInternal(h.C.hash)(key), A.length(h_buckets) - 1)
187+
getWithDefaultAux(~eq=Belt_Id.getEqInternal(h.C.eq), key, A.getUnsafe(h_buckets, nid), default)
188+
}
189+
190+
let rec getOrThrowAux = (~eq, key, buckets) =>
191+
switch C.toOpt(buckets) {
192+
| None => throw(Not_found)
193+
| Some(cell) =>
194+
if eq(key, cell.N.key) {
195+
cell.N.value
196+
} else {
197+
getOrThrowAux(~eq, key, cell.N.next)
198+
}
199+
}
200+
201+
let getOrThrow = (h, key) => {
202+
let h_buckets = h.C.buckets
203+
let nid = Int.bitwiseAnd(Belt_Id.getHashInternal(h.C.hash)(key), A.length(h_buckets) - 1)
204+
getOrThrowAux(~eq=Belt_Id.getEqInternal(h.C.eq), key, A.getUnsafe(h_buckets, nid))
205+
}
206+
173207
let rec memInBucket = (key, cell, ~eq) =>
174208
eq(cell.N.key, key) ||
175209
switch C.toOpt(cell.N.next) {

‎packages/@rescript/belt/src/Belt_HashMap.resi‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,52 @@ Belt.HashMap.has(s0, 2) == false
203203
*/
204204
let has: (t<'key, 'value, 'id>, 'key) => bool
205205

206+
/**
207+
`getWithDefault(tbl, k, default)` returns the value bound to `k`, or `default` if `k` is not bound.
208+
209+
## Examples
210+
211+
```rescript
212+
module IntHash = Belt.Id.MakeHashable({
213+
type t = int
214+
let hash = a => a
215+
let eq = (a, b) => a == b
216+
})
217+
218+
let s0 = Belt.HashMap.make(~hintSize=10, ~id=module(IntHash))
219+
Belt.HashMap.set(s0, 1, "value1")
220+
221+
Belt.HashMap.getWithDefault(s0, 1, "default") == "value1"
222+
Belt.HashMap.getWithDefault(s0, 2, "default") == "default"
223+
```
224+
*/
225+
let getWithDefault: (t<'key, 'value, 'id>, 'key, 'value) => 'value
226+
227+
/**
228+
`getOrThrow(tbl, k)` returns the value bound to `k`. Throws `Not_found` if `k` is not bound.
229+
230+
## Examples
231+
232+
```rescript
233+
module IntHash = Belt.Id.MakeHashable({
234+
type t = int
235+
let hash = a => a
236+
let eq = (a, b) => a == b
237+
})
238+
239+
let s0 = Belt.HashMap.make(~hintSize=10, ~id=module(IntHash))
240+
Belt.HashMap.set(s0, 1, "value1")
241+
242+
Belt.HashMap.getOrThrow(s0, 1) == "value1"
243+
244+
switch Belt.HashMap.getOrThrow(s0, 2) {
245+
| exception Not_found => assert(true)
246+
| _ => assert(false)
247+
}
248+
```
249+
*/
250+
let getOrThrow: (t<'key, 'value, 'id>, 'key) => 'value
251+
206252
/**
207253
If bound exists, removes it from the hash map.
208254

‎packages/@rescript/belt/src/Belt_HashMapInt.res‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,40 @@ let get = (h, key: key) => {
154154
}
155155
}
156156

157+
let rec getWithDefaultAux = (key: key, buckets, default) =>
158+
switch C.toOpt(buckets) {
159+
| None => default
160+
| Some(cell) =>
161+
if key == cell.N.key {
162+
cell.N.value
163+
} else {
164+
getWithDefaultAux(key, cell.N.next, default)
165+
}
166+
}
167+
168+
let getWithDefault = (h, key: key, default) => {
169+
let h_buckets = h.C.buckets
170+
let nid = Int.bitwiseAnd(hash(key), A.length(h_buckets) - 1)
171+
getWithDefaultAux(key, A.getUnsafe(h_buckets, nid), default)
172+
}
173+
174+
let rec getOrThrowAux = (key: key, buckets) =>
175+
switch C.toOpt(buckets) {
176+
| None => throw(Not_found)
177+
| Some(cell) =>
178+
if key == cell.N.key {
179+
cell.N.value
180+
} else {
181+
getOrThrowAux(key, cell.N.next)
182+
}
183+
}
184+
185+
let getOrThrow = (h, key: key) => {
186+
let h_buckets = h.C.buckets
187+
let nid = Int.bitwiseAnd(hash(key), A.length(h_buckets) - 1)
188+
getOrThrowAux(key, A.getUnsafe(h_buckets, nid))
189+
}
190+
157191
let rec memInBucket = (key: key, cell) =>
158192
cell.N.key == key ||
159193
switch C.toOpt(cell.next) {

‎packages/@rescript/belt/src/Belt_HashMapInt.resi‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,38 @@ let get: (t<'a>, key) => option<'a>
1919

2020
let has: (t<'b>, key) => bool
2121

22+
/**
23+
`getWithDefault(tbl, k, default)` returns the value bound to `k`, or `default` if `k` is not bound.
24+
25+
## Examples
26+
27+
```rescript
28+
let hMap = Belt.HashMap.Int.fromArray([(1, "value1")])
29+
30+
Belt.HashMap.Int.getWithDefault(hMap, 1, "default") == "value1"
31+
Belt.HashMap.Int.getWithDefault(hMap, 2, "default") == "default"
32+
```
33+
*/
34+
let getWithDefault: (t<'a>, key, 'a) => 'a
35+
36+
/**
37+
`getOrThrow(tbl, k)` returns the value bound to `k`. Throws `Not_found` if `k` is not bound.
38+
39+
## Examples
40+
41+
```rescript
42+
let hMap = Belt.HashMap.Int.fromArray([(1, "value1")])
43+
44+
Belt.HashMap.Int.getOrThrow(hMap, 1) == "value1"
45+
46+
switch Belt.HashMap.Int.getOrThrow(hMap, 2) {
47+
| exception Not_found => assert(true)
48+
| _ => assert(false)
49+
}
50+
```
51+
*/
52+
let getOrThrow: (t<'a>, key) => 'a
53+
2254
let remove: (t<'a>, key) => unit
2355

2456
@deprecated("Use `forEach` instead")

‎packages/@rescript/belt/src/Belt_HashMapString.res‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,40 @@ let get = (h, key: key) => {
154154
}
155155
}
156156

157+
let rec getWithDefaultAux = (key: key, buckets, default) =>
158+
switch C.toOpt(buckets) {
159+
| None => default
160+
| Some(cell) =>
161+
if key == cell.N.key {
162+
cell.N.value
163+
} else {
164+
getWithDefaultAux(key, cell.N.next, default)
165+
}
166+
}
167+
168+
let getWithDefault = (h, key: key, default) => {
169+
let h_buckets = h.C.buckets
170+
let nid = Int.bitwiseAnd(hash(key), A.length(h_buckets) - 1)
171+
getWithDefaultAux(key, A.getUnsafe(h_buckets, nid), default)
172+
}
173+
174+
let rec getOrThrowAux = (key: key, buckets) =>
175+
switch C.toOpt(buckets) {
176+
| None => throw(Not_found)
177+
| Some(cell) =>
178+
if key == cell.N.key {
179+
cell.N.value
180+
} else {
181+
getOrThrowAux(key, cell.N.next)
182+
}
183+
}
184+
185+
let getOrThrow = (h, key: key) => {
186+
let h_buckets = h.C.buckets
187+
let nid = Int.bitwiseAnd(hash(key), A.length(h_buckets) - 1)
188+
getOrThrowAux(key, A.getUnsafe(h_buckets, nid))
189+
}
190+
157191
let rec memInBucket = (key: key, cell) =>
158192
cell.N.key == key ||
159193
switch C.toOpt(cell.next) {

‎packages/@rescript/belt/src/Belt_HashMapString.resi‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,38 @@ let get: (t<'a>, key) => option<'a>
1919

2020
let has: (t<'b>, key) => bool
2121

22+
/**
23+
`getWithDefault(tbl, k, default)` returns the value bound to `k`, or `default` if `k` is not bound.
24+
25+
## Examples
26+
27+
```rescript
28+
let hMap = Belt.HashMap.String.fromArray([("a", "value1")])
29+
30+
Belt.HashMap.String.getWithDefault(hMap, "a", "default") == "value1"
31+
Belt.HashMap.String.getWithDefault(hMap, "b", "default") == "default"
32+
```
33+
*/
34+
let getWithDefault: (t<'a>, key, 'a) => 'a
35+
36+
/**
37+
`getOrThrow(tbl, k)` returns the value bound to `k`. Throws `Not_found` if `k` is not bound.
38+
39+
## Examples
40+
41+
```rescript
42+
let hMap = Belt.HashMap.String.fromArray([("a", "value1")])
43+
44+
Belt.HashMap.String.getOrThrow(hMap, "a") == "value1"
45+
46+
switch Belt.HashMap.String.getOrThrow(hMap, "b") {
47+
| exception Not_found => assert(true)
48+
| _ => assert(false)
49+
}
50+
```
51+
*/
52+
let getOrThrow: (t<'a>, key) => 'a
53+
2254
let remove: (t<'a>, key) => unit
2355

2456
@deprecated("Use `forEach` instead")

‎tests/belt_tests/src/belt_hashmap_test.mjs‎

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import * as Belt_HashMap from "@rescript/belt/src/Belt_HashMap.mjs";
99
import * as Primitive_int from "@rescript/runtime/lib/es6/Primitive_int.mjs";
1010
import * as Belt_SortArray from "@rescript/belt/src/Belt_SortArray.mjs";
1111
import * as Array_data_util from "./array_data_util.mjs";
12+
import * as Belt_HashMapInt from "@rescript/belt/src/Belt_HashMapInt.mjs";
13+
import * as Belt_HashMapString from "@rescript/belt/src/Belt_HashMapString.mjs";
1214

1315
function intEq(x, y) {
1416
return x === y;
@@ -67,6 +69,71 @@ Mocha.describe("Belt_hashmap_test", () => {
6769
Test_utils.eq("File \"belt_hashmap_test.res\", line 43, characters 7-14", Belt_HashMap.size(v), 98000);
6870
Test_utils.ok("File \"belt_hashmap_test.res\", line 44, characters 7-14", Belt_Array.every(Array_data_util.range(2001, 100000), x => Belt_HashMap.has(v, x)));
6971
});
72+
Mocha.test("getWithDefault and getOrThrow", () => {
73+
let v = Belt_HashMap.fromArray([
74+
[
75+
1,
76+
10
77+
],
78+
[
79+
2,
80+
20
81+
]
82+
], Y);
83+
Test_utils.eq("File \"belt_hashmap_test.res\", line 49, characters 7-14", Belt_HashMap.getWithDefault(v, 1, 0), 10);
84+
Test_utils.eq("File \"belt_hashmap_test.res\", line 50, characters 7-14", Belt_HashMap.getWithDefault(v, 3, 0), 0);
85+
Test_utils.eq("File \"belt_hashmap_test.res\", line 51, characters 7-14", Belt_HashMap.getOrThrow(v, 2), 20);
86+
Test_utils.throws("File \"belt_hashmap_test.res\", line 52, characters 11-18", () => {
87+
Belt_HashMap.getOrThrow(v, 3);
88+
});
89+
});
90+
Mocha.test("getWithDefault and getOrThrow with colliding keys", () => {
91+
let Collide = Belt_Id.hashable(param => 0, intEq);
92+
let v = Belt_HashMap.make(4, Collide);
93+
for (let i = 0; i <= 9; ++i) {
94+
Belt_HashMap.set(v, i, i * 10 | 0);
95+
}
96+
Test_utils.ok("File \"belt_hashmap_test.res\", line 61, characters 7-14", Belt_Array.every(Array_data_util.range(0, 9), i => Belt_HashMap.getWithDefault(v, i, -1) === (i * 10 | 0)));
97+
Test_utils.ok("File \"belt_hashmap_test.res\", line 62, characters 7-14", Belt_Array.every(Array_data_util.range(0, 9), i => Belt_HashMap.getOrThrow(v, i) === (i * 10 | 0)));
98+
Test_utils.eq("File \"belt_hashmap_test.res\", line 63, characters 7-14", Belt_HashMap.getWithDefault(v, 10, -1), -1);
99+
Test_utils.throws("File \"belt_hashmap_test.res\", line 64, characters 11-18", () => {
100+
Belt_HashMap.getOrThrow(v, 10);
101+
});
102+
});
103+
Mocha.test("Int and String getWithDefault and getOrThrow", () => {
104+
let i = Belt_HashMapInt.fromArray([
105+
[
106+
1,
107+
"a"
108+
],
109+
[
110+
2,
111+
"b"
112+
]
113+
]);
114+
Test_utils.eq("File \"belt_hashmap_test.res\", line 69, characters 7-14", Belt_HashMapInt.getWithDefault(i, 2, "z"), "b");
115+
Test_utils.eq("File \"belt_hashmap_test.res\", line 70, characters 7-14", Belt_HashMapInt.getWithDefault(i, 3, "z"), "z");
116+
Test_utils.eq("File \"belt_hashmap_test.res\", line 71, characters 7-14", Belt_HashMapInt.getOrThrow(i, 1), "a");
117+
Test_utils.throws("File \"belt_hashmap_test.res\", line 72, characters 11-18", () => {
118+
Belt_HashMapInt.getOrThrow(i, 3);
119+
});
120+
let s = Belt_HashMapString.fromArray([
121+
[
122+
"a",
123+
1
124+
],
125+
[
126+
"b",
127+
2
128+
]
129+
]);
130+
Test_utils.eq("File \"belt_hashmap_test.res\", line 74, characters 7-14", Belt_HashMapString.getWithDefault(s, "b", 0), 2);
131+
Test_utils.eq("File \"belt_hashmap_test.res\", line 75, characters 7-14", Belt_HashMapString.getWithDefault(s, "c", 0), 0);
132+
Test_utils.eq("File \"belt_hashmap_test.res\", line 76, characters 7-14", Belt_HashMapString.getOrThrow(s, "a"), 1);
133+
Test_utils.throws("File \"belt_hashmap_test.res\", line 77, characters 11-18", () => {
134+
Belt_HashMapString.getOrThrow(s, "c");
135+
});
136+
});
70137
});
71138

72139
let N;

‎tests/belt_tests/src/belt_hashmap_test.res‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,37 @@ describe(__MODULE__, () => {
4343
eq(__LOC__, N.size(v), 98_000)
4444
ok(__LOC__, A.every(I.range(2_001, 100_000), x => N.has(v, x)))
4545
})
46+
47+
test("getWithDefault and getOrThrow", () => {
48+
let v = N.fromArray(~id=module(Y), [(1, 10), (2, 20)])
49+
eq(__LOC__, N.getWithDefault(v, 1, 0), 10)
50+
eq(__LOC__, N.getWithDefault(v, 3, 0), 0)
51+
eq(__LOC__, N.getOrThrow(v, 2), 20)
52+
throws(__LOC__, () => N.getOrThrow(v, 3)->ignore)
53+
})
54+
55+
test("getWithDefault and getOrThrow with colliding keys", () => {
56+
module Collide = unpack(Belt.Id.hashable(~eq=intEq, ~hash=(_: int) => 0))
57+
let v = N.make(~id=module(Collide), ~hintSize=4)
58+
for i in 0 to 9 {
59+
N.set(v, i, i * 10)
60+
}
61+
ok(__LOC__, A.every(I.range(0, 9), i => N.getWithDefault(v, i, -1) == i * 10))
62+
ok(__LOC__, A.every(I.range(0, 9), i => N.getOrThrow(v, i) == i * 10))
63+
eq(__LOC__, N.getWithDefault(v, 10, -1), -1)
64+
throws(__LOC__, () => N.getOrThrow(v, 10)->ignore)
65+
})
66+
67+
test("Int and String getWithDefault and getOrThrow", () => {
68+
let i = Belt.HashMap.Int.fromArray([(1, "a"), (2, "b")])
69+
eq(__LOC__, Belt.HashMap.Int.getWithDefault(i, 2, "z"), "b")
70+
eq(__LOC__, Belt.HashMap.Int.getWithDefault(i, 3, "z"), "z")
71+
eq(__LOC__, Belt.HashMap.Int.getOrThrow(i, 1), "a")
72+
throws(__LOC__, () => Belt.HashMap.Int.getOrThrow(i, 3)->ignore)
73+
let s = Belt.HashMap.String.fromArray([("a", 1), ("b", 2)])
74+
eq(__LOC__, Belt.HashMap.String.getWithDefault(s, "b", 0), 2)
75+
eq(__LOC__, Belt.HashMap.String.getWithDefault(s, "c", 0), 0)
76+
eq(__LOC__, Belt.HashMap.String.getOrThrow(s, "a"), 1)
77+
throws(__LOC__, () => Belt.HashMap.String.getOrThrow(s, "c")->ignore)
78+
})
4679
})

0 commit comments

Comments
 (0)