Skip to content

Commit e851c09

Browse files
committed
Demo: Generalizing the HashMap constructor, extract stable store
this is to demonstrate what I meant in dfinity#300 (comment) and dfinity#299 (comment), and how to introduce this without breaking changes (although it’s kinda ugly) What I would _want_ here is to introduce a second, more general, constructor for the given class, but Motoko does not allow me to do that easily. But I can hack around that by * Creating a new class, not public `HashMap_` with the constructor I want * In `HashMap`’s constructor, call the `HashMap_` constructor to create an inner object (`i`) * In `HashMap`, simply copy all the fields from the inner objects to the outer object. * A public module-level function (here `wrapS`) exposes the new constructor. With this (generic, ugly) trick I can suppor the idiom ``` stable var userS : HashMap.S <UserId,UserData> = newS(); let user : HashMap.HashMap<UserId,UserData> = HashMap.wrapS(10, Nat.eq, Nat.hash, userS) ``` without changing the API. But it is ugly, and the effect on documentation generation is probably bad as well. So maybe a better course of action would be to have a midly breaking change where we only have the “new” constructor, and people will have to fix their code by passing `HashMap.newS()` as a new fourth argument if they want their old behavior. Probably better than piling up hacks like this. In that case, simply rename `class HashMap_` to `HashMap`, remove `wrapS` and the old `class HashMap`.
1 parent 37ef96a commit e851c09

File tree

1 file changed

+95
-27
lines changed

1 file changed

+95
-27
lines changed

src/HashMap.mo

Lines changed: 95 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,44 @@ module {
2424
// key-val list type
2525
type KVs<K, V> = AssocList.AssocList<K, V>;
2626

27-
/// An imperative HashMap with a minimal object-oriented interface.
28-
/// Maps keys of type `K` to values of type `V`.
29-
public class HashMap<K, V>(
27+
// The mutable bits of a HashMap, put in their own type
28+
type S<K,V> = {
29+
var table : [var KVs<K, V>];
30+
var _count : Nat;
31+
};
32+
33+
/// See `wrapS`
34+
func newS<K,V>() : S<K,V>{
35+
return {
36+
var table : [var KVs<K, V>] = [var];
37+
var _count : Nat = 0;
38+
};
39+
};
40+
41+
/// This is an alternative constructor for `HashMap` that allows the backing
42+
/// store for the HashMap to live in stable memory. Use it as follows:
43+
/// ```
44+
/// stable var userS : HashMap.S <UserId,UserData> = newS();
45+
/// let user : HashMap.HashMap<UserId,UserData> = HashMap.wrapS(10, Nat.eq, Nat.hash, userS)
46+
/// ```
47+
public func wrapS<K,V>(
3048
initCapacity : Nat,
3149
keyEq : (K, K) -> Bool,
32-
keyHash : K -> Hash.Hash) {
50+
keyHash : K -> Hash.Hash,
51+
s : S<K,V>) : HashMap<K,V> {
52+
HashMap_(initCapacity, keyEq, keyHash, s);
53+
};
3354

34-
var table : [var KVs<K, V>] = [var];
35-
var _count : Nat = 0;
55+
// not public, same type as HashMap
56+
// more general constructor than HashMap
57+
class HashMap_<K, V>(
58+
initCapacity : Nat,
59+
keyEq : (K, K) -> Bool,
60+
keyHash : K -> Hash.Hash,
61+
s : S<K,V>) {
3662

3763
/// Returns the number of entries in this HashMap.
38-
public func size() : Nat = _count;
64+
public func size() : Nat = s._count;
3965

4066
/// Deletes the entry with the key `k`. Doesn't do anything if the key doesn't
4167
/// exist.
@@ -44,15 +70,15 @@ module {
4470
/// Removes the entry with the key `k` and returns the associated value if it
4571
/// existed or `null` otherwise.
4672
public func remove(k : K) : ?V {
47-
let m = table.size();
73+
let m = s.table.size();
4874
if (m > 0) {
4975
let h = Prim.nat32ToNat(keyHash(k));
5076
let pos = h % m;
51-
let (kvs2, ov) = AssocList.replace<K, V>(table[pos], k, keyEq, null);
52-
table[pos] := kvs2;
77+
let (kvs2, ov) = AssocList.replace<K, V>(s.table[pos], k, keyEq, null);
78+
s.table[pos] := kvs2;
5379
switch(ov){
5480
case null { };
55-
case _ { _count -= 1; }
81+
case _ { s._count -= 1; }
5682
};
5783
ov
5884
} else {
@@ -64,9 +90,9 @@ module {
6490
/// existed or `null` otherwise.
6591
public func get(k : K) : ?V {
6692
let h = Prim.nat32ToNat(keyHash(k));
67-
let m = table.size();
93+
let m = s.table.size();
6894
let v = if (m > 0) {
69-
AssocList.find<K, V>(table[h % m], k, keyEq)
95+
AssocList.find<K, V>(s.table[h % m], k, keyEq)
7096
} else {
7197
null
7298
};
@@ -78,20 +104,20 @@ module {
78104
/// Insert the value `v` at key `k` and returns the previous value stored at
79105
/// `k` or `null` if it didn't exist.
80106
public func replace(k : K, v : V) : ?V {
81-
if (_count >= table.size()) {
107+
if (s._count >= s.table.size()) {
82108
let size =
83-
if (_count == 0) {
109+
if (s._count == 0) {
84110
if (initCapacity > 0) {
85111
initCapacity
86112
} else {
87113
1
88114
}
89115
} else {
90-
table.size() * 2;
116+
s.table.size() * 2;
91117
};
92118
let table2 = A.init<KVs<K, V>>(size, null);
93-
for (i in table.keys()) {
94-
var kvs = table[i];
119+
for (i in s.table.keys()) {
120+
var kvs = s.table[i];
95121
label moveKeyVals : ()
96122
loop {
97123
switch kvs {
@@ -105,14 +131,14 @@ module {
105131
}
106132
};
107133
};
108-
table := table2;
134+
s.table := table2;
109135
};
110136
let h = Prim.nat32ToNat(keyHash(k));
111-
let pos = h % table.size();
112-
let (kvs2, ov) = AssocList.replace<K, V>(table[pos], k, keyEq, ?v);
113-
table[pos] := kvs2;
137+
let pos = h % s.table.size();
138+
let (kvs2, ov) = AssocList.replace<K, V>(s.table[pos], k, keyEq, ?v);
139+
s.table[pos] := kvs2;
114140
switch(ov){
115-
case null { _count += 1 };
141+
case null { s._count += 1 };
116142
case _ {}
117143
};
118144
ov
@@ -129,12 +155,12 @@ module {
129155
/// Returns an iterator over the key value pairs in this
130156
/// `HashMap`. Does _not_ modify the `HashMap`.
131157
public func entries() : Iter.Iter<(K, V)> {
132-
if (table.size() == 0) {
158+
if (s.table.size() == 0) {
133159
object { public func next() : ?(K, V) { null } }
134160
}
135161
else {
136162
object {
137-
var kvs = table[0];
163+
var kvs = s.table[0];
138164
var nextTablePos = 1;
139165
public func next () : ?(K, V) {
140166
switch kvs {
@@ -143,8 +169,8 @@ module {
143169
?kv
144170
};
145171
case null {
146-
if (nextTablePos < table.size()) {
147-
kvs := table[nextTablePos];
172+
if (nextTablePos < s.table.size()) {
173+
kvs := s.table[nextTablePos];
148174
nextTablePos += 1;
149175
next()
150176
} else {
@@ -159,6 +185,48 @@ module {
159185

160186
};
161187

188+
/// An imperative HashMap with a minimal object-oriented interface.
189+
/// Maps keys of type `K` to values of type `V`.
190+
public class HashMap<K, V>(
191+
initCapacity : Nat,
192+
keyEq : (K, K) -> Bool,
193+
keyHash : K -> Hash.Hash) {
194+
195+
let i : HashMap_<K,V> = HashMap_(initCapacity, keyEq, keyHash, newS<K,V>());
196+
197+
/// Returns the number of entries in this HashMap.
198+
public let size = i.size;
199+
200+
/// Deletes the entry with the key `k`. Doesn't do anything if the key doesn't
201+
/// exist.
202+
public let delete = i.delete;
203+
204+
/// Removes the entry with the key `k` and returns the associated value if it
205+
/// existed or `null` otherwise.
206+
public let remove = i.remove;
207+
208+
/// Gets the entry with the key `k` and returns its associated value if it
209+
/// existed or `null` otherwise.
210+
public let get = i.get;
211+
212+
/// Insert the value `v` at key `k`. Overwrites an existing entry with key `k`
213+
public let put = i.put;
214+
215+
/// Insert the value `v` at key `k` and returns the previous value stored at
216+
/// `k` or `null` if it didn't exist.
217+
public let replace = i.replace;
218+
219+
/// An `Iter` over the keys.
220+
public let keys = i.keys;
221+
222+
/// An `Iter` over the values.
223+
public let vals = i.vals;
224+
225+
/// Returns an iterator over the key value pairs in this
226+
/// `HashMap`. Does _not_ modify the `HashMap`.
227+
public let entries = i.entries;
228+
};
229+
162230
/// clone cannot be an efficient object method,
163231
/// ...but is still useful in tests, and beyond.
164232
public func clone<K, V> (

0 commit comments

Comments
 (0)