diff --git a/persistent.carp b/persistent.carp index aef7850..2e7e01e 100644 --- a/persistent.carp +++ b/persistent.carp @@ -1788,6 +1788,34 @@ This generates: (defn values [map-ref] (reduce &(fn [acc p] (Array.push-back acc @(Pair.b &p))) [] map-ref)) + (doc merge + "Merge two maps. On key collision the second (right) map wins.") + (sig merge (Fn [(Ref %name a) (Ref %name b)] %name)) + (defn merge [a-ref b-ref] + (reduce + &(fn [acc p] (insert @(Pair.a &p) @(Pair.b &p) &acc)) + @a-ref + b-ref)) + + (doc merge-with + "Merge two maps with a conflict resolver. For keys in both maps, call the resolver with both values.") + (sig merge-with + (Fn + [(Ref (Fn [%value-type %value-type] %value-type) q) + (Ref %name a) + (Ref %name b)] + %name)) + (defn merge-with [f a-ref b-ref] + (reduce + &(fn [acc p] + (let [k @(Pair.a &p) + bv @(Pair.b &p)] + (match (get @&k &acc) + (Maybe.Nothing) (insert k bv &acc) + (Maybe.Just av) (insert k (~f av bv) &acc)))) + @a-ref + b-ref)) + (doc = "Structural equality: same size and pairwise equal entries in ascending key order.") (sig = (Fn [(Ref %name q) (Ref %name q)] Bool)) @@ -2404,6 +2432,34 @@ Example: (defn values [map-ref] (reduce &(fn [acc p] (Array.push-back acc @(Pair.b &p))) [] map-ref)) + (doc merge + "Merge two maps. On key collision the second (right) map wins.") + (sig merge (Fn [(Ref %name a) (Ref %name b)] %name)) + (defn merge [a-ref b-ref] + (reduce + &(fn [acc p] (insert @(Pair.a &p) @(Pair.b &p) &acc)) + @a-ref + b-ref)) + + (doc merge-with + "Merge two maps with a conflict resolver. For keys in both maps, call the resolver with both values.") + (sig merge-with + (Fn + [(Ref (Fn [%value-type %value-type] %value-type) q) + (Ref %name a) + (Ref %name b)] + %name)) + (defn merge-with [f a-ref b-ref] + (reduce + &(fn [acc p] + (let [k @(Pair.a &p) + bv @(Pair.b &p)] + (match (get &k &acc) + (Maybe.Nothing) (insert k bv &acc) + (Maybe.Just av) (insert k (~f av bv) &acc)))) + @a-ref + b-ref)) + (doc = "Structural equality: same size and same set of `(key, value)` entries.") (sig = (Fn [(Ref %name q) (Ref %name q)] Bool)) diff --git a/test/persistent_hash_map.carp b/test/persistent_hash_map.carp index 75ac9cb..47e77aa 100644 --- a/test/persistent_hash_map.carp +++ b/test/persistent_hash_map.carp @@ -188,6 +188,79 @@ (and (= &a &b) (not (= &a &c)))) "= is structural regardless of insertion order") + (assert-equal test + true + (let [a (IntIntMap.insert 1 10 &(IntIntMap.insert 2 20 &(IntIntMap.empty))) + b (IntIntMap.insert 3 30 &(IntIntMap.insert 4 40 &(IntIntMap.empty))) + m (IntIntMap.merge &a &b)] + (and (= (IntIntMap.length &m) 4l) + (let [k1 1 + k3 3] + (and + (match (IntIntMap.get &k1 &m) + (Maybe.Just v) (= v 10) + (Maybe.Nothing) false) + (match (IntIntMap.get &k3 &m) + (Maybe.Just v) (= v 30) + (Maybe.Nothing) false))))) + "merge of disjoint maps contains all entries") + + (assert-equal test + true + (let [a (IntIntMap.insert 1 10 &(IntIntMap.insert 2 20 &(IntIntMap.empty))) + b (IntIntMap.insert 2 99 &(IntIntMap.insert 3 30 &(IntIntMap.empty))) + m (IntIntMap.merge &a &b) + k 2] + (and (= (IntIntMap.length &m) 3l) + (match (IntIntMap.get &k &m) + (Maybe.Just v) (= v 99) + (Maybe.Nothing) false))) + "merge right wins on key collision") + + (assert-equal test + true + (let [a (IntIntMap.insert 1 10 &(IntIntMap.insert 2 20 &(IntIntMap.empty))) + e (IntIntMap.empty) + m1 (IntIntMap.merge &a &e) + m2 (IntIntMap.merge &e &a)] + (and (= &m1 &a) (= &m2 &a))) + "merge with empty is identity") + + (assert-equal test + true + (let [a (IntIntMap.insert 1 10 &(IntIntMap.insert 2 20 &(IntIntMap.empty))) + b (IntIntMap.insert 2 3 &(IntIntMap.insert 3 30 &(IntIntMap.empty))) + m (IntIntMap.merge-with &(fn [x y] (+ x y)) &a &b) + k1 1 + k2 2 + k3 3] + (and (= (IntIntMap.length &m) 3l) + (match (IntIntMap.get &k1 &m) + (Maybe.Just v) (= v 10) + (Maybe.Nothing) false) + (match (IntIntMap.get &k2 &m) + (Maybe.Just v) (= v 23) + (Maybe.Nothing) false) + (match (IntIntMap.get &k3 &m) + (Maybe.Just v) (= v 30) + (Maybe.Nothing) false))) + "merge-with addition combiner on overlapping keys") + + (assert-equal test + true + (let [a (IntIntMap.insert 1 10 &(IntIntMap.empty)) + b (IntIntMap.insert 2 20 &(IntIntMap.empty)) + m (IntIntMap.merge-with &(fn [x y] (+ x y)) &a &b) + k1 1 + k2 2] + (and (= (IntIntMap.length &m) 2l) + (match (IntIntMap.get &k1 &m) + (Maybe.Just v) (= v 10) + (Maybe.Nothing) false) + (match (IntIntMap.get &k2 &m) + (Maybe.Just v) (= v 20) + (Maybe.Nothing) false))) + "merge-with on disjoint maps just takes values") (assert-memory-balance test map-branch-lifecycle 0l diff --git a/test/persistent_ord_map.carp b/test/persistent_ord_map.carp index 888ec86..62f3d0b 100644 --- a/test/persistent_ord_map.carp +++ b/test/persistent_ord_map.carp @@ -204,6 +204,83 @@ (= &filtered &m2)) "filter with always-true preserves all entries") + (assert-equal test + true + (let [a (IntIntOMap.insert 1 + 10 + &(IntIntOMap.insert 2 20 &(IntIntOMap.empty))) + b (IntIntOMap.insert 3 + 30 + &(IntIntOMap.insert 4 40 &(IntIntOMap.empty))) + m (IntIntOMap.merge &a &b)] + (and (= (IntIntOMap.length &m) 4l) + (match (IntIntOMap.get 1 &m) + (Maybe.Just v) (= v 10) + (Maybe.Nothing) false) + (match (IntIntOMap.get 3 &m) + (Maybe.Just v) (= v 30) + (Maybe.Nothing) false))) + "merge of disjoint maps contains all entries") + + (assert-equal test + true + (let [a (IntIntOMap.insert 1 + 10 + &(IntIntOMap.insert 2 20 &(IntIntOMap.empty))) + b (IntIntOMap.insert 2 + 99 + &(IntIntOMap.insert 3 30 &(IntIntOMap.empty))) + m (IntIntOMap.merge &a &b)] + (and (= (IntIntOMap.length &m) 3l) + (match (IntIntOMap.get 2 &m) + (Maybe.Just v) (= v 99) + (Maybe.Nothing) false))) + "merge right wins on key collision") + + (assert-equal test + true + (let [a (IntIntOMap.insert 1 + 10 + &(IntIntOMap.insert 2 20 &(IntIntOMap.empty))) + e (IntIntOMap.empty) + m1 (IntIntOMap.merge &a &e) + m2 (IntIntOMap.merge &e &a)] + (and (= &m1 &a) (= &m2 &a))) + "merge with empty is identity") + + (assert-equal test + true + (let [a (IntIntOMap.insert 1 + 10 + &(IntIntOMap.insert 2 20 &(IntIntOMap.empty))) + b (IntIntOMap.insert 2 3 &(IntIntOMap.insert 3 30 &(IntIntOMap.empty))) + m (IntIntOMap.merge-with &(fn [x y] (+ x y)) &a &b)] + (and (= (IntIntOMap.length &m) 3l) + (match (IntIntOMap.get 1 &m) + (Maybe.Just v) (= v 10) + (Maybe.Nothing) false) + (match (IntIntOMap.get 2 &m) + (Maybe.Just v) (= v 23) + (Maybe.Nothing) false) + (match (IntIntOMap.get 3 &m) + (Maybe.Just v) (= v 30) + (Maybe.Nothing) false))) + "merge-with addition combiner on overlapping keys") + + (assert-equal test + true + (let [a (IntIntOMap.insert 1 10 &(IntIntOMap.empty)) + b (IntIntOMap.insert 2 20 &(IntIntOMap.empty)) + m (IntIntOMap.merge-with &(fn [x y] (+ x y)) &a &b)] + (and (= (IntIntOMap.length &m) 2l) + (match (IntIntOMap.get 1 &m) + (Maybe.Just v) (= v 10) + (Maybe.Nothing) false) + (match (IntIntOMap.get 2 &m) + (Maybe.Just v) (= v 20) + (Maybe.Nothing) false))) + "merge-with on disjoint maps just takes values") + (assert-memory-balance test map-branch-lifecycle 0l