All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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> split(int parts) { List> list = new List>().linear(); MapNodes.split(new Object(), root, (int) Math.ceil(size() / (float) parts)) .stream() .map(n -> new Map(n, hashFn, equalsFn, false)) .forEach(list::addLast); return list.forked(); } @Override public Map union(IMap m) { return merge(m, Maps.MERGE_LAST_WRITE_WINS); } @Override public Map merge(IMap b, BinaryOperator mergeFn) { if (b instanceof Map) { Node rootPrime = MapNodes.merge(0, editor, root, ((Map) b).root, equalsFn, mergeFn); return new Map<>(rootPrime, hashFn, equalsFn, isLinear()); } else { return (Map) Maps.merge(this.clone(), b, mergeFn); } } @Override public Map difference(ISet keys) { if (keys instanceof Set) { return difference(((Set) keys).map); } else { return (Map) Maps.difference(this.clone(), keys); } } @Override public Map intersection(ISet keys) { if (keys instanceof Set) { return intersection(((Set) keys).map); } else { Map map = (Map) Maps.intersection(new Map().linear(), this, keys); return isLinear() ? map : map.forked(); } } @Override public Map difference(IMap m) { if (m instanceof Map) { Node rootPrime = MapNodes.difference(0, editor, root, ((Map) m).root, equalsFn); return new Map<>(rootPrime == null ? Node.EMPTY : rootPrime, hashFn, equalsFn, isLinear()); } else { return difference(m.keys()); } } @Override public Map intersection(IMap m) { if (m instanceof Map) { Node rootPrime = MapNodes.intersection(0, editor, root, ((Map) m).root, equalsFn); return new Map<>(rootPrime == null ? Node.EMPTY : rootPrime, hashFn, equalsFn, isLinear()); } else { return intersection(m.keys()); } } @Override public long size() { return root.size(); } @Override public boolean isLinear() { return editor != null; } @Override public Iterator> iterator() { return root.iterator(); } @Override public int hashCode() { if (hash == -1) { hash = (int) Maps.hash(this); } return hash; } @Override public boolean equals(IMap m, BiPredicate valEquals) { if (m instanceof Map) { return root.equals(((Map) m).root, equalsFn, valEquals); } else { return Maps.equals(this, m, valEquals); } } @Override public boolean equals(Object obj) { if (obj instanceof IMap) { return equals((IMap) obj, Objects::equals); } return false; } @Override public String toString() { return Maps.toString(this); } @Override public Map clone() { return isLinear() ? forked().linear() : this; } private int keyHash(K key) { int hash = hashFn.applyAsInt(key); // make sure we don't have too many collisions in the lower bits hash ^= (hash >>> 20) ^ (hash >>> 12); hash ^= (hash >>> 7) ^ (hash >>> 4); return hash; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy