io.lacuna.bifurcan.Map Maven / Gradle / Ivy
package io.lacuna.bifurcan;
import io.lacuna.bifurcan.nodes.MapNodes;
import io.lacuna.bifurcan.nodes.MapNodes.Node;
import java.util.Collection;
import java.util.Iterator;
import java.util.Objects;
import java.util.function.*;
/**
* An implementation of an immutable hash-map based on the general approach described by Steindorfer and Vinju in
* this paper. It allows for customized hashing
* and equality semantics, and due to its default reliance on Java's semantics, it is significantly faster than
* Clojure's {@code PersistentHashMap} for lookups and construction for collections smaller than 100k entries, and has
* equivalent performance for larger collections.
*
* By ensuring that equivalent maps always have equivalent layout in memory, it can perform equality checks and set
* operations (union, difference, intersection) significantly faster than a more naive implementation.. By keeping the
* memory layout of each node more compact, iteration is at least 2x faster than Clojure's map.
*
* @author ztellman
*/
public class Map implements IMap, Cloneable {
private static final Object DEFAULT_VALUE = new Object();
private final BiPredicate equalsFn;
private final ToIntFunction hashFn;
public Node root;
private int hash = -1;
final Object editor;
///
/**
* @param map another map
* @return an equivalent forked map, with the same equality semantics
*/
public static Map from(IMap map) {
if (map instanceof Map) {
return (Map) map.forked();
} else {
Map result = new Map(map.keyHash(), map.keyEquality()).linear();
map.forEach(e -> result.put(e.key(), e.value()));
return result.forked();
}
}
/**
* @param map a {@code java.util.Map}
* @return a forked map with the same entries
*/
public static Map from(java.util.Map map) {
return from(map.entrySet());
}
/**
* @param entries an sequence of {@code IEntry} objects
* @return a forked map containing theentries
*/
public static Map from(Iterable> entries) {
return from(entries.iterator());
}
/**
* @param entries an iterator of {@code IEntry} objects
* @return a forked map containing the remaining entries
*/
public static Map from(Iterator> entries) {
Map m = new Map().linear();
entries.forEachRemaining(e -> m.put(e.key(), e.value()));
return m.forked();
}
/**
* @param entries a list of {@code IEntry} objects
* @return a forked map containing these entries
*/
public static Map from(IList> entries) {
return entries.stream().collect(Maps.collector(IEntry::key, IEntry::value));
}
/**
* @param entries a collection of {@code java.util.Map.Entry} objects
* @return a forked map containing these entries
*/
public static Map from(Collection> entries) {
return entries.stream().collect(Maps.collector(java.util.Map.Entry::getKey, java.util.Map.Entry::getValue));
}
/**
* Creates a map.
*
* @param hashFn a function which yields the hash value of keys
* @param equalsFn a function which checks equality of keys
*/
public Map(ToIntFunction hashFn, BiPredicate equalsFn) {
this(Node.EMPTY, hashFn, equalsFn, false);
}
public Map() {
this(Node.EMPTY, Objects::hashCode, Objects::equals, false);
}
private Map(Node root, ToIntFunction hashFn, BiPredicate equalsFn, boolean linear) {
this.root = root;
this.hashFn = hashFn;
this.equalsFn = equalsFn;
this.editor = linear ? new Object() : null;
}
///
@Override
public Set keys() {
return new Set((Map) this);
}
@Override
public ToIntFunction keyHash() {
return hashFn;
}
@Override
public BiPredicate keyEquality() {
return equalsFn;
}
@Override
public V get(K key, V defaultValue) {
Object val = MapNodes.get(root, 0, keyHash(key), key, equalsFn, DEFAULT_VALUE);
return val == DEFAULT_VALUE ? defaultValue : (V) val;
}
@Override
public Map put(K key, V value) {
return put(key, value, (BinaryOperator) Maps.MERGE_LAST_WRITE_WINS);
}
@Override
public Map put(K key, V value, BinaryOperator merge) {
return put(key, value, merge, isLinear() ? editor : new Object());
}
public Map put(K key, V value, BinaryOperator merge, Object editor) {
Node rootPrime = root.put(0, editor, keyHash(key), key, value, equalsFn, merge);
if (isLinear() && editor == this.editor) {
root = rootPrime;
hash = -1;
return this;
} else {
return new Map(rootPrime, hashFn, equalsFn, false);
}
}
@Override
public Map update(K key, UnaryOperator update) {
return update(key, update, isLinear() ? editor : new Object());
}
public Map update(K key, UnaryOperator update, Object editor) {
return put(key, update.apply(get(key, null)), (BinaryOperator) Maps.MERGE_LAST_WRITE_WINS, editor);
}
@Override
public Map remove(K key) {
return remove(key, isLinear() ? editor : new Object());
}
public Map remove(K key, Object editor) {
Node rootPrime = (Node) root.remove(0, editor, keyHash(key), key, equalsFn);
if (isLinear() && editor == this.editor) {
root = rootPrime;
hash = -1;
return this;
} else {
return new Map(rootPrime, hashFn, equalsFn, false);
}
}
@Override
public boolean contains(K key) {
return MapNodes.contains(root, 0, keyHash(key), key, equalsFn);
}
@Override
public long indexOf(K key) {
return root.indexOf(0, keyHash(key), key, keyEquality());
}
@Override
public IEntry nth(long index) {
if (index < 0 || index >= size()) {
throw new IndexOutOfBoundsException();
}
return root.nth(index);
}
@Override
public Map forked() {
if (isLinear()) {
return new Map<>(root, hashFn, equalsFn, false);
} else {
return this;
}
}
@Override
public Map linear() {
if (isLinear()) {
return this;
} else {
return new Map<>(root, hashFn, equalsFn, true);
}
}
@Override
public Map mapValues(BiFunction f) {
return new Map(root.mapVals(new Object(), f), hashFn, equalsFn, isLinear());
}
@Override
public List