From 22bd38e05d9cc92d6f8bade64bf174064994687a Mon Sep 17 00:00:00 2001 From: Matthew Hammer Date: Wed, 3 Nov 2021 11:53:37 -0600 Subject: [PATCH 1/5] a stable hash map --- src/StableHashMap.mo | 215 ++++++++++++++++++++++++++++++++++++++ test/stableHashMapTest.mo | 148 ++++++++++++++++++++++++++ 2 files changed, 363 insertions(+) create mode 100644 src/StableHashMap.mo create mode 100644 test/stableHashMapTest.mo diff --git a/src/StableHashMap.mo b/src/StableHashMap.mo new file mode 100644 index 00000000..fd16164f --- /dev/null +++ b/src/StableHashMap.mo @@ -0,0 +1,215 @@ +/// Mutable hash map (aka Hashtable) +/// +/// This module defines an imperative hash map (hash table), with a general key and value type. +/// +/// It is like HashMap, but eschews classes and objects so that its type is `stable`. +/// + +import Prim "mo:⛔"; +import P "Prelude"; +import A "Array"; +import Hash "Hash"; +import Iter "Iter"; +import AssocList "AssocList"; + +module { + // Type parameters that we need for all operations: + // - K = key type + // - V = value type + + // func parameters that we need for some operations: + // - keyEq : (K, K) -> Bool, + // - keyHash : K -> Hash.Hash + + // key-val list + public type KVs = AssocList.AssocList; + + // representation of hash table + public type HashMap = { + var table : [var KVs]; + var count : Nat; + }; + + /// Returns the number of entries in this HashMap. + public func size(h : HashMap) : Nat = h.count; + +/* + /// Deletes the entry with the key `k`. Doesn't do anything if the key doesn't + /// exist. + public func delete(k : K) = ignore remove(k); + + /// Removes the entry with the key `k` and returns the associated value if it + /// existed or `null` otherwise. + public func remove(k : K) : ?V { + let m = table.size(); + if (m > 0) { + let h = Prim.nat32ToNat(keyHash(k)); + let pos = h % m; + let (kvs2, ov) = AssocList.replace(table[pos], k, keyEq, null); + table[pos] := kvs2; + switch(ov){ + case null { }; + case _ { _count -= 1; } + }; + ov + } else { + null + }; + }; + + /// Gets the entry with the key `k` and returns its associated value if it + /// existed or `null` otherwise. + public func get(k : K) : ?V { + let h = Prim.nat32ToNat(keyHash(k)); + let m = table.size(); + let v = if (m > 0) { + AssocList.find(table[h % m], k, keyEq) + } else { + null + }; + }; + + /// Insert the value `v` at key `k`. Overwrites an existing entry with key `k` + public func put(k : K, v : V) = ignore replace(k, v); + + /// Insert the value `v` at key `k` and returns the previous value stored at + /// `k` or `null` if it didn't exist. + public func replace(k : K, v : V) : ?V { + if (_count >= table.size()) { + let size = + if (_count == 0) { + 1 + } else { + table.size() * 2; + }; + let table2 = A.init>(size, null); + for (i in table.keys()) { + var kvs = table[i]; + label moveKeyVals : () + loop { + switch kvs { + case null { break moveKeyVals }; + case (?((k, v), kvsTail)) { + let h = Prim.nat32ToNat(keyHash(k)); + let pos2 = h % table2.size(); + table2[pos2] := ?((k,v), table2[pos2]); + kvs := kvsTail; + }; + } + }; + }; + table := table2; + }; + let h = Prim.nat32ToNat(keyHash(k)); + let pos = h % table.size(); + let (kvs2, ov) = AssocList.replace(table[pos], k, keyEq, ?v); + table[pos] := kvs2; + switch(ov){ + case null { _count += 1 }; + case _ {} + }; + ov + }; + + /// An `Iter` over the keys. + public func keys() : Iter.Iter + { Iter.map(entries(), func (kv : (K, V)) : K { kv.0 }) }; + + /// An `Iter` over the values. + public func vals() : Iter.Iter + { Iter.map(entries(), func (kv : (K, V)) : V { kv.1 }) }; + + /// Returns an iterator over the key value pairs in this + /// `HashMap`. Does _not_ modify the `HashMap`. + public func entries() : Iter.Iter<(K, V)> { + if (table.size() == 0) { + object { public func next() : ?(K, V) { null } } + } + else { + object { + var kvs = table[0]; + var nextTablePos = 1; + public func next () : ?(K, V) { + switch kvs { + case (?(kv, kvs2)) { + kvs := kvs2; + ?kv + }; + case null { + if (nextTablePos < table.size()) { + kvs := table[nextTablePos]; + nextTablePos += 1; + next() + } else { + null + } + } + } + } + } + } + }; + + }; + + /// clone cannot be an efficient object method, + /// ...but is still useful in tests, and beyond. + public func clone ( + h : HashMap, + keyEq : (K, K) -> Bool, + keyHash : K -> Hash.Hash + ) : HashMap { + let h2 = HashMap(h.size(), keyEq, keyHash); + for ((k,v) in h.entries()) { + h2.put(k,v); + }; + h2 + }; + + /// Clone from any iterator of key-value pairs + public func fromIter( + iter : Iter.Iter<(K, V)>, + initCapacity : Nat, + keyEq : (K, K) -> Bool, + keyHash : K -> Hash.Hash + ) : HashMap { + let h = HashMap(initCapacity, keyEq, keyHash); + for ((k, v) in iter) { + h.put(k, v); + }; + h + }; + + public func map( + h : HashMap, + keyEq : (K, K) -> Bool, + keyHash : K -> Hash.Hash, + mapFn : (K, V1) -> V2, + ) : HashMap { + let h2 = HashMap(h.size(), keyEq, keyHash); + for ((k, v1) in h.entries()) { + let v2 = mapFn(k, v1); + h2.put(k, v2); + }; + h2 + }; + + public func mapFilter( + h : HashMap, + keyEq : (K, K) -> Bool, + keyHash : K -> Hash.Hash, + mapFn : (K, V1) -> ?V2, + ) : HashMap { + let h2 = HashMap(h.size(), keyEq, keyHash); + for ((k, v1) in h.entries()) { + switch (mapFn(k, v1)) { + case null { }; + case (?v2) { + h2.put(k, v2); + }; + } + }; + h2 + }; +*/ +} diff --git a/test/stableHashMapTest.mo b/test/stableHashMapTest.mo new file mode 100644 index 00000000..41a0635f --- /dev/null +++ b/test/stableHashMapTest.mo @@ -0,0 +1,148 @@ +import Prim "mo:⛔"; +import H "mo:base/StableHashMap"; +import Hash "mo:base/Hash"; +import Text "mo:base/Text"; + +/* +debug { + let a = H.HashMap(3, Text.equal, Text.hash); + + a.put("apple", 1); + a.put("banana", 2); + a.put("pear", 3); + a.put("avocado", 4); + a.put("Apple", 11); + a.put("Banana", 22); + a.put("Pear", 33); + a.put("Avocado", 44); + a.put("ApplE", 111); + a.put("BananA", 222); + a.put("PeaR", 333); + a.put("AvocadO", 444); + + // need to resupply the constructor args; they are private to the object; but, should they be? + let b = H.clone(a, Text.equal, Text.hash); + + // ensure clone has each key-value pair present in original + for ((k,v) in a.entries()) { + Prim.debugPrint(debug_show (k,v)); + switch (b.get(k)) { + case null { assert false }; + case (?w) { assert v == w }; + }; + }; + + // ensure original has each key-value pair present in clone + for ((k,v) in b.entries()) { + Prim.debugPrint(debug_show (k,v)); + switch (a.get(k)) { + case null { assert false }; + case (?w) { assert v == w }; + }; + }; + + // ensure clone has each key present in original + for (k in a.keys()) { + switch (b.get(k)) { + case null { assert false }; + case (?_) { }; + }; + }; + + // ensure clone has each value present in original + for (v in a.vals()) { + var foundMatch = false; + for (w in b.vals()) { + if (v == w) { foundMatch := true } + }; + assert foundMatch + }; + + // do some more operations: + a.put("apple", 1111); + a.put("banana", 2222); + switch( a.remove("pear")) { + case null { assert false }; + case (?three) { assert three == 3 }; + }; + a.delete("avocado"); + + // check them: + switch (a.get("apple")) { + case (?1111) { }; + case _ { assert false }; + }; + switch (a.get("banana")) { + case (?2222) { }; + case _ { assert false }; + }; + switch (a.get("pear")) { + case null { }; + case (?_) { assert false }; + }; + switch (a.get("avocado")) { + case null { }; + case (?_) { assert false }; + }; + + // undo operations above: + a.put("apple", 1); + // .. and test that replace works + switch (a.replace("apple", 666)) { + case null { assert false }; + case (?one) { assert one == 1; // ...and revert + a.put("apple", 1) + }; + }; + a.put("banana", 2); + a.put("pear", 3); + a.put("avocado", 4); + + // ensure clone has each key-value pair present in original + for ((k,v) in a.entries()) { + Prim.debugPrint(debug_show (k,v)); + switch (b.get(k)) { + case null { assert false }; + case (?w) { assert v == w }; + }; + }; + + // ensure original has each key-value pair present in clone + for ((k,v) in b.entries()) { + Prim.debugPrint(debug_show (k,v)); + switch (a.get(k)) { + case null { assert false }; + case (?w) { assert v == w }; + }; + }; + + + // test fromIter method + let c = H.fromIter(b.entries(), 0, Text.equal, Text.hash); + + // c agrees with each entry of b + for ((k,v) in b.entries()) { + Prim.debugPrint(debug_show (k,v)); + switch (c.get(k)) { + case null { assert false }; + case (?w) { assert v == w }; + }; + }; + + // b agrees with each entry of c + for ((k,v) in c.entries()) { + Prim.debugPrint(debug_show (k,v)); + switch (b.get(k)) { + case null { assert false }; + case (?w) { assert v == w }; + }; + }; + + // Issue #228 + let d = H.HashMap(50, Text.equal, Text.hash); + switch(d.remove("test")) { + case null { }; + case (?_) { assert false }; + }; +}; +*/ From e1afaba540a0f4c12c113698cddfb3de3d8f57b2 Mon Sep 17 00:00:00 2001 From: Matthew Hammer Date: Wed, 3 Nov 2021 13:54:55 -0600 Subject: [PATCH 2/5] refactor out OO style --- src/StableHashMap.mo | 302 +++++++++++++++++++++++-------------------- 1 file changed, 165 insertions(+), 137 deletions(-) diff --git a/src/StableHashMap.mo b/src/StableHashMap.mo index fd16164f..68ae90b9 100644 --- a/src/StableHashMap.mo +++ b/src/StableHashMap.mo @@ -17,153 +17,185 @@ module { // - K = key type // - V = value type - // func parameters that we need for some operations: - // - keyEq : (K, K) -> Bool, - // - keyHash : K -> Hash.Hash // key-val list public type KVs = AssocList.AssocList; - // representation of hash table + // representation of (the stable types of) the hash map state. public type HashMap = { var table : [var KVs]; var count : Nat; }; + // representation of hash map including key operations. + // unlike HashMap, this type is not stable, but is required for + // some operations (keyEq and keyHash are functions). + public type HashMap_ = { + keyEq : (K, K) -> Bool; + keyHash : K -> Hash.Hash; + // to do -- use union type operation to compress this definition + initCapacity : Nat; + var table : [var KVs]; + var count : Nat; + }; + + public func empty() : HashMap { + { var table = [var]; + var count = 0; + } + }; + + public func empty_( + initCapacity : Nat, + keyEq : (K, K) -> Bool, + keyHash : K -> Hash.Hash + ) : HashMap_ + { + { var table = [var]; + var count = 0; + initCapacity; + keyEq; + keyHash; + } + }; + /// Returns the number of entries in this HashMap. - public func size(h : HashMap) : Nat = h.count; - -/* - /// Deletes the entry with the key `k`. Doesn't do anything if the key doesn't - /// exist. - public func delete(k : K) = ignore remove(k); - - /// Removes the entry with the key `k` and returns the associated value if it - /// existed or `null` otherwise. - public func remove(k : K) : ?V { - let m = table.size(); - if (m > 0) { - let h = Prim.nat32ToNat(keyHash(k)); - let pos = h % m; - let (kvs2, ov) = AssocList.replace(table[pos], k, keyEq, null); - table[pos] := kvs2; - switch(ov){ - case null { }; - case _ { _count -= 1; } - }; - ov - } else { - null + public func size(self : HashMap) : Nat = self.count; + + /// Deletes the entry with the key `k`. Doesn't do anything if the key doesn't + /// exist. + public func delete(self : HashMap_, k : K) = ignore remove(self, k); + + /// Removes the entry with the key `k` and returns the associated value if it + /// existed or `null` otherwise. + public func remove(self : HashMap_, k : K) : ?V { + let m = self.table.size(); + if (m > 0) { + let h = Prim.nat32ToNat(self.keyHash(k)); + let pos = h % m; + let (kvs2, ov) = AssocList.replace(self.table[pos], k, self.keyEq, null); + self.table[pos] := kvs2; + switch(ov){ + case null { }; + case _ { self.count -= 1; } }; + ov + } else { + null }; + }; - /// Gets the entry with the key `k` and returns its associated value if it - /// existed or `null` otherwise. - public func get(k : K) : ?V { - let h = Prim.nat32ToNat(keyHash(k)); - let m = table.size(); - let v = if (m > 0) { - AssocList.find(table[h % m], k, keyEq) - } else { - null - }; + /// Gets the entry with the key `k` and returns its associated value if it + /// existed or `null` otherwise. + public func get(self : HashMap_, k : K) : ?V { + let h = Prim.nat32ToNat(self.keyHash(k)); + let m = self.table.size(); + let v = if (m > 0) { + AssocList.find(self.table[h % m], k, self.keyEq) + } else { + null }; + }; - /// Insert the value `v` at key `k`. Overwrites an existing entry with key `k` - public func put(k : K, v : V) = ignore replace(k, v); - - /// Insert the value `v` at key `k` and returns the previous value stored at - /// `k` or `null` if it didn't exist. - public func replace(k : K, v : V) : ?V { - if (_count >= table.size()) { - let size = - if (_count == 0) { - 1 + /// Insert the value `v` at key `k`. Overwrites an existing entry with key `k` + public func put(self : HashMap_, k : K, v : V) = + ignore replace(self, k, v); + + /// Insert the value `v` at key `k` and returns the previous value stored at + /// `k` or `null` if it didn't exist. + public func replace(self : HashMap_, k : K, v : V) : ?V { + if (self.count >= self.table.size()) { + let size = + if (self.count == 0) { + if (self.initCapacity > 0) { + self.initCapacity } else { - table.size() * 2; - }; - let table2 = A.init>(size, null); - for (i in table.keys()) { - var kvs = table[i]; - label moveKeyVals : () - loop { - switch kvs { - case null { break moveKeyVals }; - case (?((k, v), kvsTail)) { - let h = Prim.nat32ToNat(keyHash(k)); - let pos2 = h % table2.size(); - table2[pos2] := ?((k,v), table2[pos2]); - kvs := kvsTail; - }; - } - }; + 1 + } + } else { + self.table.size() * 2; + }; + let table2 = A.init>(size, null); + for (i in self.table.keys()) { + var kvs = self.table[i]; + label moveKeyVals : () + loop { + switch kvs { + case null { break moveKeyVals }; + case (?((k, v), kvsTail)) { + let h = Prim.nat32ToNat(self.keyHash(k)); + let pos2 = h % table2.size(); + table2[pos2] := ?((k,v), table2[pos2]); + kvs := kvsTail; + }; + } }; - table := table2; - }; - let h = Prim.nat32ToNat(keyHash(k)); - let pos = h % table.size(); - let (kvs2, ov) = AssocList.replace(table[pos], k, keyEq, ?v); - table[pos] := kvs2; - switch(ov){ - case null { _count += 1 }; - case _ {} }; - ov + self.table := table2; }; + let h = Prim.nat32ToNat(self.keyHash(k)); + let pos = h % self.table.size(); + let (kvs2, ov) = AssocList.replace(self.table[pos], k, self.keyEq, ?v); + self.table[pos] := kvs2; + switch(ov){ + case null { self.count += 1 }; + case _ {} + }; + ov + }; - /// An `Iter` over the keys. - public func keys() : Iter.Iter - { Iter.map(entries(), func (kv : (K, V)) : K { kv.0 }) }; - - /// An `Iter` over the values. - public func vals() : Iter.Iter - { Iter.map(entries(), func (kv : (K, V)) : V { kv.1 }) }; - - /// Returns an iterator over the key value pairs in this - /// `HashMap`. Does _not_ modify the `HashMap`. - public func entries() : Iter.Iter<(K, V)> { - if (table.size() == 0) { - object { public func next() : ?(K, V) { null } } - } - else { - object { - var kvs = table[0]; - var nextTablePos = 1; - public func next () : ?(K, V) { - switch kvs { - case (?(kv, kvs2)) { - kvs := kvs2; - ?kv - }; - case null { - if (nextTablePos < table.size()) { - kvs := table[nextTablePos]; - nextTablePos += 1; - next() - } else { - null - } - } - } + /// An `Iter` over the keys. + public func keys(self : HashMap) : Iter.Iter + { Iter.map(entries(self), func (kv : (K, V)) : K { kv.0 }) }; + + /// An `Iter` over the values. + public func vals(self : HashMap) : Iter.Iter + { Iter.map(entries(self), func (kv : (K, V)) : V { kv.1 }) }; + + /// Returns an iterator over the key value pairs in this + /// `HashMap`. Does _not_ modify the `HashMap`. + public func entries(self : HashMap) : Iter.Iter<(K, V)> { + if (self.table.size() == 0) { + object { public func next() : ?(K, V) { null } } + } + else { + object { + var kvs = self.table[0]; + var nextTablePos = 1; + public func next () : ?(K, V) { + switch kvs { + case (?(kv, kvs2)) { + kvs := kvs2; + ?kv + }; + case null { + if (nextTablePos < self.table.size()) { + kvs := self.table[nextTablePos]; + nextTablePos += 1; + next() + } else { + null + } + } } } } - }; + } + }; + public func clone (h : HashMap) : HashMap { + { var table = A.tabulateVar(h.table.size(), func (i : Nat) : KVs { h.table[i] }); + var count = h.count ; + } }; - /// clone cannot be an efficient object method, - /// ...but is still useful in tests, and beyond. - public func clone ( - h : HashMap, - keyEq : (K, K) -> Bool, - keyHash : K -> Hash.Hash - ) : HashMap { - let h2 = HashMap(h.size(), keyEq, keyHash); - for ((k,v) in h.entries()) { - h2.put(k,v); - }; - h2 + public func clone_ (h : HashMap_) : HashMap_ { + { keyEq = h.keyEq ; + keyHash = h.keyHash ; + initCapacity = h.initCapacity ; + var table = A.tabulateVar(h.table.size(), func (i : Nat) : KVs { h.table[i] }); + var count = h.count ; + } }; /// Clone from any iterator of key-value pairs @@ -172,44 +204,40 @@ module { initCapacity : Nat, keyEq : (K, K) -> Bool, keyHash : K -> Hash.Hash - ) : HashMap { - let h = HashMap(initCapacity, keyEq, keyHash); + ) : HashMap_ { + let h = empty_(initCapacity, keyEq, keyHash); for ((k, v) in iter) { - h.put(k, v); + put(h, k, v); }; h }; public func map( - h : HashMap, - keyEq : (K, K) -> Bool, - keyHash : K -> Hash.Hash, + h : HashMap_, mapFn : (K, V1) -> V2, ) : HashMap { - let h2 = HashMap(h.size(), keyEq, keyHash); - for ((k, v1) in h.entries()) { + let h2 = empty_(h.table.size(), h.keyEq, h.keyHash); + for ((k, v1) in entries(h)) { let v2 = mapFn(k, v1); - h2.put(k, v2); + put(h2, k, v2); }; h2 }; public func mapFilter( - h : HashMap, - keyEq : (K, K) -> Bool, - keyHash : K -> Hash.Hash, + h : HashMap_, mapFn : (K, V1) -> ?V2, ) : HashMap { - let h2 = HashMap(h.size(), keyEq, keyHash); - for ((k, v1) in h.entries()) { + let h2 = empty_(h.table.size(), h.keyEq, h.keyHash); + for ((k, v1) in entries(h)) { switch (mapFn(k, v1)) { case null { }; case (?v2) { - h2.put(k, v2); + put(h2, k, v2); }; } }; h2 }; -*/ + } From 0ca1ec51ecbd8305a6fb9ac3735939410282d699 Mon Sep 17 00:00:00 2001 From: Matthew Hammer Date: Wed, 3 Nov 2021 14:17:20 -0600 Subject: [PATCH 3/5] adapted tests from HashMap --- test/stableHashMapTest.mo | 99 +++++++++++++++++++-------------------- 1 file changed, 49 insertions(+), 50 deletions(-) diff --git a/test/stableHashMapTest.mo b/test/stableHashMapTest.mo index 41a0635f..7098767c 100644 --- a/test/stableHashMapTest.mo +++ b/test/stableHashMapTest.mo @@ -3,114 +3,113 @@ import H "mo:base/StableHashMap"; import Hash "mo:base/Hash"; import Text "mo:base/Text"; -/* debug { - let a = H.HashMap(3, Text.equal, Text.hash); - - a.put("apple", 1); - a.put("banana", 2); - a.put("pear", 3); - a.put("avocado", 4); - a.put("Apple", 11); - a.put("Banana", 22); - a.put("Pear", 33); - a.put("Avocado", 44); - a.put("ApplE", 111); - a.put("BananA", 222); - a.put("PeaR", 333); - a.put("AvocadO", 444); + let a = H.empty_(3, Text.equal, Text.hash); + + H.put(a, "apple", 1); + H.put(a, "banana", 2); + H.put(a, "pear", 3); + H.put(a, "avocado", 4); + H.put(a, "Apple", 11); + H.put(a, "Banana", 22); + H.put(a, "Pear", 33); + H.put(a, "Avocado", 44); + H.put(a, "ApplE", 111); + H.put(a, "BananA", 222); + H.put(a, "PeaR", 333); + H.put(a, "AvocadO", 444); // need to resupply the constructor args; they are private to the object; but, should they be? - let b = H.clone(a, Text.equal, Text.hash); + let b = H.clone_(a); // ensure clone has each key-value pair present in original - for ((k,v) in a.entries()) { + for ((k,v) in H.entries(a)) { Prim.debugPrint(debug_show (k,v)); - switch (b.get(k)) { + switch (H.get(b, k)) { case null { assert false }; case (?w) { assert v == w }; }; }; // ensure original has each key-value pair present in clone - for ((k,v) in b.entries()) { + for ((k,v) in H.entries(b)) { Prim.debugPrint(debug_show (k,v)); - switch (a.get(k)) { + switch (H.get(a, k)) { case null { assert false }; case (?w) { assert v == w }; }; }; // ensure clone has each key present in original - for (k in a.keys()) { - switch (b.get(k)) { + for (k in H.keys(a)) { + switch (H.get(b, k)) { case null { assert false }; case (?_) { }; }; }; // ensure clone has each value present in original - for (v in a.vals()) { + for (v in H.vals(a)) { var foundMatch = false; - for (w in b.vals()) { + for (w in H.vals(b)) { if (v == w) { foundMatch := true } }; assert foundMatch }; // do some more operations: - a.put("apple", 1111); - a.put("banana", 2222); - switch( a.remove("pear")) { + H.put(a, "apple", 1111); + H.put(a, "banana", 2222); + switch( H.remove(a, "pear")) { case null { assert false }; case (?three) { assert three == 3 }; }; - a.delete("avocado"); + H.delete(a, "avocado"); // check them: - switch (a.get("apple")) { + switch (H.get(a, "apple")) { case (?1111) { }; case _ { assert false }; }; - switch (a.get("banana")) { + switch (H.get(a, "banana")) { case (?2222) { }; case _ { assert false }; }; - switch (a.get("pear")) { + switch (H.get(a, "pear")) { case null { }; case (?_) { assert false }; }; - switch (a.get("avocado")) { + switch (H.get(a, "avocado")) { case null { }; case (?_) { assert false }; }; // undo operations above: - a.put("apple", 1); + H.put(a, "apple", 1); // .. and test that replace works - switch (a.replace("apple", 666)) { + switch (H.replace(a, "apple", 666)) { case null { assert false }; case (?one) { assert one == 1; // ...and revert - a.put("apple", 1) + H.put(a, "apple", 1) }; }; - a.put("banana", 2); - a.put("pear", 3); - a.put("avocado", 4); + H.put(a, "banana", 2); + H.put(a, "pear", 3); + H.put(a, "avocado", 4); // ensure clone has each key-value pair present in original - for ((k,v) in a.entries()) { + for ((k,v) in H.entries(a)) { Prim.debugPrint(debug_show (k,v)); - switch (b.get(k)) { + switch (H.get(b, k)) { case null { assert false }; case (?w) { assert v == w }; }; }; // ensure original has each key-value pair present in clone - for ((k,v) in b.entries()) { + for ((k,v) in H.entries(b)) { Prim.debugPrint(debug_show (k,v)); - switch (a.get(k)) { + switch (H.get(a, k)) { case null { assert false }; case (?w) { assert v == w }; }; @@ -118,31 +117,31 @@ debug { // test fromIter method - let c = H.fromIter(b.entries(), 0, Text.equal, Text.hash); + let c = H.fromIter(H.entries(b), 0, Text.equal, Text.hash); // c agrees with each entry of b - for ((k,v) in b.entries()) { + for ((k,v) in H.entries(b)) { Prim.debugPrint(debug_show (k,v)); - switch (c.get(k)) { + switch (H.get(c, k)) { case null { assert false }; case (?w) { assert v == w }; }; }; // b agrees with each entry of c - for ((k,v) in c.entries()) { + for ((k,v) in H.entries(c)) { Prim.debugPrint(debug_show (k,v)); - switch (b.get(k)) { + switch (H.get(b, k)) { case null { assert false }; case (?w) { assert v == w }; }; }; // Issue #228 - let d = H.HashMap(50, Text.equal, Text.hash); - switch(d.remove("test")) { + let d = H.empty_(50, Text.equal, Text.hash); + switch(H.remove(d, "test")) { case null { }; case (?_) { assert false }; }; }; -*/ + From e7a2787c66f132237bb3f4cfac9a40e3db0ee2fb Mon Sep 17 00:00:00 2001 From: Matthew Hammer Date: Tue, 9 Nov 2021 15:16:16 -0700 Subject: [PATCH 4/5] more useful type definitions. --- src/StableHashMap.mo | 54 ++++++++++++++++++++---------------------- test/package-set.dhall | 10 ++++---- 2 files changed, 30 insertions(+), 34 deletions(-) diff --git a/src/StableHashMap.mo b/src/StableHashMap.mo index 68ae90b9..9b02778f 100644 --- a/src/StableHashMap.mo +++ b/src/StableHashMap.mo @@ -35,8 +35,7 @@ module { keyHash : K -> Hash.Hash; // to do -- use union type operation to compress this definition initCapacity : Nat; - var table : [var KVs]; - var count : Nat; + var hashMap : HashMap; }; public func empty() : HashMap { @@ -51,8 +50,8 @@ module { keyHash : K -> Hash.Hash ) : HashMap_ { - { var table = [var]; - var count = 0; + { var hashMap = { var table = [var]; + var count = 0 }; initCapacity; keyEq; keyHash; @@ -69,15 +68,15 @@ module { /// Removes the entry with the key `k` and returns the associated value if it /// existed or `null` otherwise. public func remove(self : HashMap_, k : K) : ?V { - let m = self.table.size(); + let m = self.hashMap.table.size(); if (m > 0) { let h = Prim.nat32ToNat(self.keyHash(k)); let pos = h % m; - let (kvs2, ov) = AssocList.replace(self.table[pos], k, self.keyEq, null); - self.table[pos] := kvs2; + let (kvs2, ov) = AssocList.replace(self.hashMap.table[pos], k, self.keyEq, null); + self.hashMap.table[pos] := kvs2; switch(ov){ case null { }; - case _ { self.count -= 1; } + case _ { self.hashMap.count -= 1; } }; ov } else { @@ -89,9 +88,9 @@ module { /// existed or `null` otherwise. public func get(self : HashMap_, k : K) : ?V { let h = Prim.nat32ToNat(self.keyHash(k)); - let m = self.table.size(); + let m = self.hashMap.table.size(); let v = if (m > 0) { - AssocList.find(self.table[h % m], k, self.keyEq) + AssocList.find(self.hashMap.table[h % m], k, self.keyEq) } else { null }; @@ -104,20 +103,20 @@ module { /// Insert the value `v` at key `k` and returns the previous value stored at /// `k` or `null` if it didn't exist. public func replace(self : HashMap_, k : K, v : V) : ?V { - if (self.count >= self.table.size()) { + if (self.hashMap.count >= self.hashMap.table.size()) { let size = - if (self.count == 0) { + if (self.hashMap.count == 0) { if (self.initCapacity > 0) { self.initCapacity } else { 1 } } else { - self.table.size() * 2; + self.hashMap.table.size() * 2; }; let table2 = A.init>(size, null); - for (i in self.table.keys()) { - var kvs = self.table[i]; + for (i in self.hashMap.table.keys()) { + var kvs = self.hashMap.table[i]; label moveKeyVals : () loop { switch kvs { @@ -131,14 +130,14 @@ module { } }; }; - self.table := table2; + self.hashMap.table := table2; }; let h = Prim.nat32ToNat(self.keyHash(k)); - let pos = h % self.table.size(); - let (kvs2, ov) = AssocList.replace(self.table[pos], k, self.keyEq, ?v); - self.table[pos] := kvs2; + let pos = h % self.hashMap.table.size(); + let (kvs2, ov) = AssocList.replace(self.hashMap.table[pos], k, self.keyEq, ?v); + self.hashMap.table[pos] := kvs2; switch(ov){ - case null { self.count += 1 }; + case null { self.hashMap.count += 1 }; case _ {} }; ov @@ -193,8 +192,7 @@ module { { keyEq = h.keyEq ; keyHash = h.keyHash ; initCapacity = h.initCapacity ; - var table = A.tabulateVar(h.table.size(), func (i : Nat) : KVs { h.table[i] }); - var count = h.count ; + var hashMap = clone(h.hashMap) ; } }; @@ -215,9 +213,9 @@ module { public func map( h : HashMap_, mapFn : (K, V1) -> V2, - ) : HashMap { - let h2 = empty_(h.table.size(), h.keyEq, h.keyHash); - for ((k, v1) in entries(h)) { + ) : HashMap_ { + let h2 = empty_(h.hashMap.table.size(), h.keyEq, h.keyHash); + for ((k, v1) in entries(h.hashMap)) { let v2 = mapFn(k, v1); put(h2, k, v2); }; @@ -227,9 +225,9 @@ module { public func mapFilter( h : HashMap_, mapFn : (K, V1) -> ?V2, - ) : HashMap { - let h2 = empty_(h.table.size(), h.keyEq, h.keyHash); - for ((k, v1) in entries(h)) { + ) : HashMap_ { + let h2 = empty_(h.hashMap.table.size(), h.keyEq, h.keyHash); + for ((k, v1) in entries(h.hashMap)) { switch (mapFn(k, v1)) { case null { }; case (?v2) { diff --git a/test/package-set.dhall b/test/package-set.dhall index 7c2671fa..ebfad9fb 100644 --- a/test/package-set.dhall +++ b/test/package-set.dhall @@ -1,8 +1,6 @@ -[ - { - name = "matchers", - repo = "https://github.com/kritzcreek/motoko-matchers.git", - version = "v1.1.0", - dependencies = [] : List Text +[ { name = "matchers" + , repo = "https://github.com/kritzcreek/motoko-matchers.git" + , version = "v1.1.0" + , dependencies = [] : List Text } ] From 6f6b9da06c068b74ed2e929f7c8d111ce9214da9 Mon Sep 17 00:00:00 2001 From: Matthew Hammer Date: Tue, 9 Nov 2021 15:24:25 -0700 Subject: [PATCH 5/5] nits --- src/StableHashMap.mo | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/StableHashMap.mo b/src/StableHashMap.mo index 9b02778f..3daa636e 100644 --- a/src/StableHashMap.mo +++ b/src/StableHashMap.mo @@ -22,6 +22,7 @@ module { public type KVs = AssocList.AssocList; // representation of (the stable types of) the hash map state. + // can be stored in a stable variable. public type HashMap = { var table : [var KVs]; var count : Nat; @@ -30,10 +31,10 @@ module { // representation of hash map including key operations. // unlike HashMap, this type is not stable, but is required for // some operations (keyEq and keyHash are functions). + // to use, initialize `hashMap` to be your `stable var` hashmap. public type HashMap_ = { keyEq : (K, K) -> Bool; keyHash : K -> Hash.Hash; - // to do -- use union type operation to compress this definition initCapacity : Nat; var hashMap : HashMap; };