diff --git a/src/StableHashMap.mo b/src/StableHashMap.mo new file mode 100644 index 00000000..3daa636e --- /dev/null +++ b/src/StableHashMap.mo @@ -0,0 +1,242 @@ +/// 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 + + + // key-val list + 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; + }; + + // 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; + initCapacity : Nat; + var hashMap : HashMap; + }; + + 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 hashMap = { var table = [var]; + var count = 0 }; + initCapacity; + keyEq; + keyHash; + } + }; + + /// Returns the number of entries in this HashMap. + 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.hashMap.table.size(); + if (m > 0) { + let h = Prim.nat32ToNat(self.keyHash(k)); + let pos = h % m; + let (kvs2, ov) = AssocList.replace(self.hashMap.table[pos], k, self.keyEq, null); + self.hashMap.table[pos] := kvs2; + switch(ov){ + case null { }; + case _ { self.hashMap.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(self : HashMap_, k : K) : ?V { + let h = Prim.nat32ToNat(self.keyHash(k)); + let m = self.hashMap.table.size(); + let v = if (m > 0) { + AssocList.find(self.hashMap.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(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.hashMap.count >= self.hashMap.table.size()) { + let size = + if (self.hashMap.count == 0) { + if (self.initCapacity > 0) { + self.initCapacity + } else { + 1 + } + } else { + self.hashMap.table.size() * 2; + }; + let table2 = A.init>(size, null); + for (i in self.hashMap.table.keys()) { + var kvs = self.hashMap.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; + }; + } + }; + }; + self.hashMap.table := table2; + }; + let h = Prim.nat32ToNat(self.keyHash(k)); + 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.hashMap.count += 1 }; + case _ {} + }; + ov + }; + + /// 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 ; + } + }; + + public func clone_ (h : HashMap_) : HashMap_ { + { keyEq = h.keyEq ; + keyHash = h.keyHash ; + initCapacity = h.initCapacity ; + var hashMap = clone(h.hashMap) ; + } + }; + + /// 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 = empty_(initCapacity, keyEq, keyHash); + for ((k, v) in iter) { + put(h, k, v); + }; + h + }; + + public func map( + h : HashMap_, + mapFn : (K, V1) -> V2, + ) : 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); + }; + h2 + }; + + public func mapFilter( + h : HashMap_, + mapFn : (K, V1) -> ?V2, + ) : 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) { + put(h2, k, v2); + }; + } + }; + h2 + }; + +} 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 } ] diff --git a/test/stableHashMapTest.mo b/test/stableHashMapTest.mo new file mode 100644 index 00000000..7098767c --- /dev/null +++ b/test/stableHashMapTest.mo @@ -0,0 +1,147 @@ +import Prim "mo:⛔"; +import H "mo:base/StableHashMap"; +import Hash "mo:base/Hash"; +import Text "mo:base/Text"; + +debug { + 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); + + // ensure clone has each key-value pair present in original + for ((k,v) in H.entries(a)) { + Prim.debugPrint(debug_show (k,v)); + 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 H.entries(b)) { + Prim.debugPrint(debug_show (k,v)); + 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 H.keys(a)) { + switch (H.get(b, k)) { + case null { assert false }; + case (?_) { }; + }; + }; + + // ensure clone has each value present in original + for (v in H.vals(a)) { + var foundMatch = false; + for (w in H.vals(b)) { + if (v == w) { foundMatch := true } + }; + assert foundMatch + }; + + // do some more operations: + H.put(a, "apple", 1111); + H.put(a, "banana", 2222); + switch( H.remove(a, "pear")) { + case null { assert false }; + case (?three) { assert three == 3 }; + }; + H.delete(a, "avocado"); + + // check them: + switch (H.get(a, "apple")) { + case (?1111) { }; + case _ { assert false }; + }; + switch (H.get(a, "banana")) { + case (?2222) { }; + case _ { assert false }; + }; + switch (H.get(a, "pear")) { + case null { }; + case (?_) { assert false }; + }; + switch (H.get(a, "avocado")) { + case null { }; + case (?_) { assert false }; + }; + + // undo operations above: + H.put(a, "apple", 1); + // .. and test that replace works + switch (H.replace(a, "apple", 666)) { + case null { assert false }; + case (?one) { assert one == 1; // ...and revert + H.put(a, "apple", 1) + }; + }; + 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 H.entries(a)) { + Prim.debugPrint(debug_show (k,v)); + 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 H.entries(b)) { + Prim.debugPrint(debug_show (k,v)); + switch (H.get(a, k)) { + case null { assert false }; + case (?w) { assert v == w }; + }; + }; + + + // test fromIter method + let c = H.fromIter(H.entries(b), 0, Text.equal, Text.hash); + + // c agrees with each entry of b + for ((k,v) in H.entries(b)) { + Prim.debugPrint(debug_show (k,v)); + 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 H.entries(c)) { + Prim.debugPrint(debug_show (k,v)); + switch (H.get(b, k)) { + case null { assert false }; + case (?w) { assert v == w }; + }; + }; + + // Issue #228 + let d = H.empty_(50, Text.equal, Text.hash); + switch(H.remove(d, "test")) { + case null { }; + case (?_) { assert false }; + }; +}; +