Immutable HashMap in Typescript
class HashMap<K extends HashKey, V>
Immutable Hash Map
The HashMap<K, V>
class stores key-value pairs where the keys have type K
and the values type V
. Keys can be numbers, strings, booleans, dates, or
custom objects which implement the HashableObj and ComparableObj interfaces.
HashMap
implements the typescript-builtin ReadonlyMap
interface (which
consists of the read-only methods of the JS builtin
Map).
The HashMap is immutable, which means that no changes or mutations are allowed directly to the HashMap. Instead, modification operations such as HashMap.delete return a new HashMap which contains the result of the modification. The original HashMap is unchanged and can continue to be accessed and used. The HashMap implements this efficiently using structural sharing and does not require a full copy.
Creating Hash Maps
Static method to create a new empty HashMap
The key type must extend HashKey, which consists of strings,
numbers, dates, booleans, or a custom user-defined object which implements
the HashableObj and ComparableObj interfaces.
These interfaces allows you to create complex keys which are made up of multiple properties. Values can
have any type but can not contain undefined
. The value type can include
null
if you wish to represent missing or empty values.
While you can start with an empty HashMap
and then use HashMap.set
to add entries, it is more efficient to create the HashMap in bulk using
either the static HashMap.from or HashMap.build or using
various methods on LazySeq to convert a LazySeq
to a HashMap
.
Example
import { HashMap } from "@seedtactics/immutable-collections";
const hEmpty = HashMap.empty<string, number>();
const h = hEmpty.set("one", 1).set("two", 2);
for (const [k, v] of h) {
console.log("key " + k + ": " + v.toString());
}
static from<K extends HashKey, V extends NotUndefined>(
items: Iterable<readonly [K, V]>,
merge?: (v1: V, v2: V) => V
): HashMap<K, V>
Efficiently create a new HashMap from key-value pairs
from
efficiently creates a HashMap from a sequence of key-value pairs. An optional merge
function
can be provided. When from
detects a duplicate key, the merge function is called to determine
the value associated to the key. The first parameter v1
to the merge function is the existing value
and the second parameter v2
is the new value just recieved from the sequence. The return value from the
merge function is the value associated to the key. If no merge function is provided, the second value v2
is used, overwriting the first value v1
.
If you have a LazySeq, the LazySeq.LazySeq.toHashMap method is an easy way to call from
.
Example
import { HashMap } from "@seedtactics/immutable-collections";
const h = HashMap.from(
[["one", 1], ["two", 2], ["one", 3]]
);
console.log(h.get("one")); // prints 3 because 3 overwrites the 1
console.log(h.get("two")); // prints 2
Example
import { HashMap } from "@seedtactics/immutable-collections";
const h = HashMap.from(
[["one", 1], ["two", 2], ["one", 3]],
(v1, v2) => v1 + v2 + 100
);
console.log(h.get("one")); // prints 104 because merge is called with 1 and 3
console.log(h.get("two")); // prints 2
static build<K extends HashKey, V extends NotUndefined>(
items: Iterable<V>,
key: (v: V) => K
): HashMap<K, V>
Efficently create a new HashMap
build
efficiently creates a HashMap from a sequence of values and a key extraction function. If a
duplicate key is found, the later value is used and the earlier value is overwritten. If this is
not desired, use the more generalized version of build
which also provides a value extraction function.
static build<T, K extends HashKey, V extends NotUndefined>(
items: Iterable<T>,
key: (v: T) => K,
val: (old: V | undefined, t: T) => V
): HashMap<K, V>
Efficently create a new HashMap
build
efficiently creates a HashMap from a sequence of items, a key extraction function, and a value extraction
function. The sequence of initial items can have any type T
, and for each item the key is extracted. If the key does not
yet exist, the val
extraction function is called with undefined
to retrieve the value associated to the key.
If the key already exists in the HashMap, the val
extraction function is called with the old
value to
merge the new item t
into the existing value old
.
IReadOnlyMap interface
size is a readonly property containing the number of entries in the HashMap.
Looks up the value associated to the given key. Returns undefined if the key is not found.
Checks if the key exists in the HashMap. Returns true if found, otherwise false
Iterates the keys and values in the HashMap
This is the default iteration when using for .. of
directly on the HashMap
. It iterates
all keys and values. The order of iteration is an implementation detail and cannot be relied upon,
it depends on the hashes and how the internal data is organized.
Example
import { HashMap } from "@seedtactics/immutable-collections";
const h = HashMap.from([["one", 1], ["two", 2], ["three", 3]]);
for (const [k, v] of h) {
console.log("key " + k + ": " + v.toString());
}
// will print
// key one: 1
// key two: 2
// key three: 3
Iterates the keys and values in the HashMap
This provides an iterator for all the entries in the map. The order of iteration is an implementation detail and cannot be relied upon, it depends on the hashes and how the internal data is organized. Similar to the builtin Map.entries, it can only be iterated once. Use HashMap.toLazySeq to create an iterable that can be iterated more than once.
Iterates the keys in the HashMap
This provides an iterator for all the keys in the map. The order of iteration is an implementation detail and cannot be relied upon, it depends on the hashes and how the internal data is organized. Similar to the builtin Map.keys, it can only be iterated once. Use HashMap.keysToLazySeq to create an iterable that can be iterated more than once.
Iterates the values in the HashMap
This provides an iterator for all the values in the map. The order of iteration is an implementation detail and cannot be relied upon, it depends on the hashes and how the internal data is organized. Similar to the builtin Map.values, it can only be iterated once. Use HashMap.valuesToLazySeq to create an iterable that can be iterated more than once.
Applys a function to each entry in the HashMap
This applies the function f
to each value and key in the hashmap. The order of iteration is an
implementation detail and cannot be relied upon, it depends on the hashes and how the internal
data is organized.
Other Read Methods
Reduce all the entries in the HashMap to a single value
Creates a LazySeq which iterates all the entries in the HashMap
Creates a LazySeq which iterates all the keys in the HashMap
Creates a LazySeq which iterates all the values in the HashMap
Creates a HashSet which contains all the keys in the HashMap
This function is O(1) and very fast because the backing data structure is reused. Essentially, the HashMap and HashSet classes are just two different APIs against the same underlying tree. Since both HashSet and HashMap are immutable, they can both share the same underlying tree without problems.
Modification
Return a new HashMap with the given key set to the given value
If the key already exists and the value is ===
to the existing value, then the HashMap
object instance is returned unchanged.
Example
import { HashMap } from "@seedtactics/immutable-collections";
const h = HashMap.from([["one", 1], ["two", 2], ["three", 3]]);
const h2 = h.set("one", 1);
console.log(h === h2); // prints true
const h3 = h.set("one", 50);
console.log(h === h3); // prints false
console.log(h.get("one")); // prints 1
console.log(h3.get("one")); // prints 50
Return a new HashMap with the value at a key modified using a function
The modify function is a more efficient combination of HashMap.get and HashMap.set. modify
first
looks for the key in the map. If the key is found, the function f
is applied to the
existing value and the result is used to set the new value. If the key is not found, the
function f
is applied to undefined
and the result is used to set the new value.
This allows you to either insert ot modify the value at a key.
If the key already exists and the returned value is ===
to the existing value, then the HashMap
object instance is returned unchanged.
Return a new HashMap with the given key removed (if it exists)
If the key does not exist, then the HashMap object instance is returned unchanged.
Return a new HashMap by inserting, modifying, or deleting the value at a given key
alter
is a generalization of HashMap.get, HashMap.set, HashMap.modify,
and HashMap.delete. It can be used to insert a new entry, modify an existing entry, or
delete an existing entry. alter
first looks for the key in the map. The function f
is then
applied to the existing value if the key was found and undefined
if the key does not exist.
If the function f
returns undefined
, the entry is deleted and if f
returns a value, the
entry is updated to use the new value.
If the key is not found and f
returns undefined or the key exists and the function f
returns
a value ===
to the existing value, then the HashMap object instance is returned unchanged.
Transformation
Transform the values in the HashMap using a function
mapValues
applies the function f
to each value and key in the HashMap and returns a new HashMap
with the same keys but the values adjusted to the result of the function f
. This can be done efficiently because
the keys are unchanged the arrangement of the data structure is unchanged. If you wish to transform
both the keys and the values, use HashMap.toLazySeq, map the lazy sequence, and then convert the
lazy sequence back to a HashMap. (This is the most efficient way to transform both the keys and values, since
if the keys change the entire data structure needs to be rebuilt anyway.)
mapValues
guarantees that if no values are changed, then the HashMap object instance is returned
unchanged.
𝑜𝑏𝑗.collectValues<V2 extends NotUndefined>(
f: (v: V, k: K) => V2 | null | undefined
): HashMap<K, V2>
Transform or delete the values in the HashMap using a function
collectValues
applies the function f
to each value and key in the HashMap. If f
returns null or undefined,
the key and value is removed. Otherwise, the returned value from f
is used as the new value associated to the key k.
This can be done efficiently because the keys are unchanged the arrangement of the data
structure is unchanged. If you wish to transform both the keys and the values, use HashMap.toLazySeq,
map the lazy sequence, and then convert the lazy sequence back to a HashMap. (This is the most efficient
way to transform both the keys and values, since if the keys change the entire data structure needs to be
rebuilt anyway.)
collectValues
guarantees that if no values are changed, then the HashMap object instance is returned
unchanged.
Remove entries from the HashMap that return false from a predicate
filter
applies the function f
to each value and key in the HashMap. If f
returns false, the
key is removed.
filter
guarantees that if no values are removed, then the HashMap object instance is returned
unchanged.
Apply a function to the HashMap
Applies the provided function f
to this
and returns the result. This is a convenience function
which allows you to continue to chain operations without having to create a new
temporary variable.
Bulk Modification
Returns a new HashMap which combines all entries in two HashMaps
union
produces a new HashMap which contains all the entries in both HashMaps. If a
key appears in only one of the two maps, the value from the map is used. If a key appears
in both maps, the provided merge function is used to determine the value. If the merge function
is not specified, the value from the other
HashMap provided as an argument is used and the
value from this
is ignored.
union
guarantees that if the resulting HashMap is equal to this
, then the HashMap object
instance is returned unchanged.
static union<K extends HashKey, V extends NotUndefined>(
merge: (v1: V, v2: V, k: K) => V,
...maps: readonly HashMap<K, V>[]
): HashMap<K, V>
Create a new HashMap which combines all entries in a sequence of HashMaps
HashMap.union
is the static version of HashMap.union and allows unioning more than two HashMaps
at once. It produces a new HashMap which contains all the entries in all the HashMaps. If a
key appears in only one of the maps, the value from that map is used. If a key appears
in multiple maps, the provided merge function is used to determine the value. The order of merging
is equivalent to the order of maps in the sequence.
union
guarantees that if the resulting HashMap is equal to the first non-empty HashMap in the sequence,
then the HashMap object instance is returned unchanged.
Return a new HashMap which adds the entries.
append
is just a shorthand for a combination of HashMap.from and HashMap.union. union
is very efficient at combining data structures, so the fastest way to bulk-add entries is to first create
a data structure of the entries to add and then union them into the existing data structure. Thus, if you
already have a HashMap or HashMap.build is more ergonomic, you should just directly use HashMap.union.
𝑜𝑏𝑗.intersection(
other: HashMap<K, V>,
merge?: (vThis: V, vOther: V, k: K) => V
): HashMap<K, V>
Returns a new HashMap which contains only entries whose keys are in both HashMaps
intersection
produces a new HashMap which contains all the entries which have keys in
both HashMaps. For each such entry, the merge function is used to determine the resulting value.
If the merge function is not specified, the value from the other
HashMap provided as an argument
is used and the value from this
is ignored.
intersection
guarantees that if the resulting HashMap is equal to this
, then the HashMap object
instance is returned unchanged.
static intersection<K extends HashKey, V extends NotUndefined>(
merge: (v1: V, v2: V, k: K) => V,
...maps: readonly HashMap<K, V>[]
): HashMap<K, V>
Returns a new HashMap which contains only entries whose keys are in all HashMaps
HashMap.intersection
is a static version of HashMap.intersection, and produces a new HashMap
which contains the entries which have keys in all specified HashMaps. For each such entry, the merge
function is used to determine the resulting value.
intersection
guarantees that if the resulting HashMap is equal to the first non-empty HashMap, then the HashMap object
instance is returned unchanged.
Returns a new HashMap which contains only keys which do not appear in the provided HashMap
difference
produces a new HashMap which contains all the entries in this
where the key does
not exist in the provided other
HashMap. Can think of this as this - other
where the subtraction
is removing all the keys in other
from this
. The values of the other
HashMap are ignored and
can be any value V2
.
difference
guarantees that if no entries are removed from this
, then the HashMap object
instance is returned unchanged.
Returns a new HashMap which contains only entries whose key appear in exactly one of the two maps
symmetricDifference produces a new HashMap which contains all the entries whose keys appear in exactly one of this and other. If other is empty, this is returned unchanged.
Returns a new HashMap which contains only keys which do not appear in the provided HashSet
withoutKeys
produces a new HashMap which contains all the entries in this
where the key does
not exist in the provided keys
HashSet. withoutKeys
guarantees that if no entries are
removed from this
, then the HashMap object instance is returned unchanged.
𝑜𝑏𝑗.adjust<V2>(
keysToAdjust: HashMap<K, V2>,
adjustVal: (existingVal: V | undefined, helperVal: V2, k: K) => V | undefined
): HashMap<K, V>
Return a new HashMap which adjusts all the provided keys with a specified modification function.
adjust
is passed a HashMap of keys to adjust associated to helper values of type V2
(the type V2
can be
anything and does not need to be related V
). For each key to modify, adjust
then calls the adjustVal
function with the current existing
value in the HashMap (or undefined
if the key does not exist) and the helper value associated with the key.
The return value is set as the new value for the key, or removed if the return value is undefined
.
adjust
is equivalent to the following code, but is much more efficient since adjust
can perform the operation
in a single pass through the tree.
const m = this;
for (const [k, v2] of keysToAdjust) {
const v = m.get(k);
const newV = adjustVal(v, v2, k);
if (newV === undefined) {
m = m.delete(k);
} else {
m = m.set(k, newV);
}
}
return m;
If the keys to adjust are only available in an array or some other data structure,
it is still very fast to use HashMap.from or HashMap.build to create the keysToAdjust
map and
then pass it to adjust
. adjust
is very efficient because it can overlap the structure of the two trees and
perform the merge in a single pass through both trees.
adjust
guarantees that if no entries are added, removed, or modified from this
, then the HashMap object
is returned unchanged.