Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

a StableHashMap module. #300

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
243 changes: 243 additions & 0 deletions src/StableHashMap.mo
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
/// 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<K, V> = AssocList.AssocList<K, V>;

// representation of (the stable types of) the hash map state.
public type HashMap<K, V> = {
var table : [var KVs<K, V>];
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_<K, V> = {
keyEq : (K, K) -> Bool;
keyHash : K -> Hash.Hash;
// to do -- use union type operation to compress this definition
matthewhammer marked this conversation as resolved.
Show resolved Hide resolved
initCapacity : Nat;
var table : [var KVs<K, V>];
var count : Nat;
};

public func empty<K, V>() : HashMap<K, V> {
{ var table = [var];
var count = 0;
}
};

public func empty_<K, V>(
initCapacity : Nat,
keyEq : (K, K) -> Bool,
keyHash : K -> Hash.Hash
) : HashMap_<K, V>
{
{ var table = [var];
var count = 0;
initCapacity;
keyEq;
keyHash;
}
};

/// Returns the number of entries in this HashMap.
public func size<K, V>(self : HashMap<K, V>) : Nat = self.count;

/// Deletes the entry with the key `k`. Doesn't do anything if the key doesn't
/// exist.
public func delete<K, V>(self : HashMap_<K, V>, 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<K, V>(self : HashMap_<K, V>, 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<K, V>(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, V>(self : HashMap_<K, V>, k : K) : ?V {
let h = Prim.nat32ToNat(self.keyHash(k));
let m = self.table.size();
let v = if (m > 0) {
AssocList.find<K, V>(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, V>(self : HashMap_<K, V>, 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<K, V>(self : HashMap_<K, V>, k : K, v : V) : ?V {
if (self.count >= self.table.size()) {
let size =
if (self.count == 0) {
if (self.initCapacity > 0) {
self.initCapacity
} else {
1
}
} else {
self.table.size() * 2;
};
let table2 = A.init<KVs<K, V>>(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;
};
}
};
};
self.table := table2;
};
let h = Prim.nat32ToNat(self.keyHash(k));
let pos = h % self.table.size();
let (kvs2, ov) = AssocList.replace<K, V>(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<K, V>(self : HashMap<K, V>) : Iter.Iter<K>
{ Iter.map(entries(self), func (kv : (K, V)) : K { kv.0 }) };

/// An `Iter` over the values.
public func vals<K, V>(self : HashMap<K, V>) : Iter.Iter<V>
{ 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<K, V>(self : HashMap<K, V>) : 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<K, V> (h : HashMap<K, V>) : HashMap<K, V> {
{ var table = A.tabulateVar(h.table.size(), func (i : Nat) : KVs<K, V> { h.table[i] });
var count = h.count ;
}
};

public func clone_<K, V> (h : HashMap_<K, V>) : HashMap_<K, V> {
{ keyEq = h.keyEq ;
keyHash = h.keyHash ;
initCapacity = h.initCapacity ;
var table = A.tabulateVar(h.table.size(), func (i : Nat) : KVs<K, V> { h.table[i] });
var count = h.count ;
}
};

/// Clone from any iterator of key-value pairs
public func fromIter<K, V>(
iter : Iter.Iter<(K, V)>,
initCapacity : Nat,
keyEq : (K, K) -> Bool,
keyHash : K -> Hash.Hash
) : HashMap_<K, V> {
let h = empty_<K, V>(initCapacity, keyEq, keyHash);
for ((k, v) in iter) {
put(h, k, v);
};
h
};

public func map<K, V1, V2>(
h : HashMap_<K, V1>,
mapFn : (K, V1) -> V2,
) : HashMap<K, V2> {
let h2 = empty_<K, V2>(h.table.size(), h.keyEq, h.keyHash);
for ((k, v1) in entries(h)) {
let v2 = mapFn(k, v1);
put(h2, k, v2);
};
h2
};

public func mapFilter<K, V1, V2>(
h : HashMap_<K, V1>,
mapFn : (K, V1) -> ?V2,
) : HashMap<K, V2> {
let h2 = empty_<K, V2>(h.table.size(), h.keyEq, h.keyHash);
for ((k, v1) in entries(h)) {
switch (mapFn(k, v1)) {
case null { };
case (?v2) {
put(h2, k, v2);
};
}
};
h2
};

}
147 changes: 147 additions & 0 deletions test/stableHashMapTest.mo
Original file line number Diff line number Diff line change
@@ -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_<Text, Nat>(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_<Text, Nat>(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<Text, Nat>(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_<Text, Nat>(50, Text.equal, Text.hash);
switch(H.remove(d, "test")) {
case null { };
case (?_) { assert false };
};
};