Skip to content

Add set operations to @immut/hash{map, set} and @internal/sparse_array Summary #2145

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

Open
wants to merge 24 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a9b3888
feat: add four new functions to HAMT and their tests
Asterless May 21, 2025
09b6a99
fix(union_with): fix union_with for HAMT,now it can handle branch
Asterless May 21, 2025
1b84483
feat(sparse_array): add intersection and difference methods
Asterless May 21, 2025
0cdca5b
fix(HAMT): fix union_with, intersection, intersection_with and differ…
Asterless May 21, 2025
44c0a79
feat(hashset): add intersection and difference methods to hashset
Asterless May 21, 2025
f80c9f2
commit other files
Asterless May 21, 2025
7f2524c
feat: add four new functions to HAMT and their tests
Asterless May 21, 2025
fc71f34
fix(union_with): fix union_with for HAMT,now it can handle branch
Asterless May 21, 2025
5adb5ce
feat(sparse_array): add intersection and difference methods
Asterless May 21, 2025
8d06b82
fix(HAMT): fix union_with, intersection, intersection_with and differ…
Asterless May 21, 2025
eb6ef82
feat(hashset): add intersection and difference methods to hashset
Asterless May 21, 2025
61ca5f0
Merge branch 'feature/20250522_HAMT' of github.com:Asterless/core int…
Asterless May 22, 2025
a0d7669
style: change the position of some function declarations
Asterless May 22, 2025
29c51a5
fix: fix formatting of the code
Asterless May 22, 2025
4b46d45
Merge pull request #1 from Asterless/feature/20250522_HAMT
Asterless May 22, 2025
256557a
Merge branch 'main' of github.com:Asterless/core
Asterless May 22, 2025
dab49f5
feat: Update the function declarations of hash tables and sparse arrays
Asterless May 22, 2025
779268b
Merge branch 'main' into feature/20250522_HAMT
Lampese May 23, 2025
3f120f1
Merge branch 'main' into feature/20250522_HAMT
Asterless May 23, 2025
34bce14
Merge branch 'main' into feature/20250522_HAMT
Asterless May 23, 2025
3b99be9
Merge branch 'main' into feature/20250522_HAMT
Asterless May 23, 2025
139cda1
Merge branch 'main' into feature/20250522_HAMT
Lampese May 23, 2025
0f648d5
Merge branch 'main' into feature/20250522_HAMT
Asterless May 23, 2025
d72a3c0
Merge branch 'main' into feature/20250522_HAMT
Lampese May 25, 2025
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
127 changes: 127 additions & 0 deletions immut/hashmap/HAMT.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,133 @@ pub fn[K : Eq + Hash, V] union(self : T[K, V], other : T[K, V]) -> T[K, V] {
}
}

///|
/// Union two hashmaps with a function
pub fn[K : Eq + Hash, V] union_with(
self : T[K, V],
other : T[K, V],
f : (K, V, V) -> V
) -> T[K, V] {
match (self, other) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we just make it a method pub fn[K : Eq + Hash, V] T::union_with(

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure!😊 It's just that I don't quite understand why this was done. When I added this method, I observed that other methods didn't make such modifications

(_, Empty) => self
(Empty, _) => other
(_, Leaf(k, v)) =>
match self.get(k) {
Some(v1) => self.add(k, f(k, v1, v))
None => self.add(k, v)
}
(Leaf(k, v), _) =>
match other.get(k) {
Some(v2) => other.add(k, f(k, v, v2))
None => other.add(k, v)
}
(Branch(sa1), Branch(sa2)) =>
Branch(sa1.union(sa2, fn(m1, m2) { m1.union_with(m2, f) }))
(_, _) =>
self
.iter()
.fold(init=other, fn(m, kv) {
match m.get(kv.0) {
Some(v2) => m.add(kv.0, f(kv.0, kv.1, v2))
None => m.add(kv.0, kv.1)
}
})
}
}

///|
/// Intersect two hashmaps
pub fn[K : Eq + Hash, V] intersection(
self : T[K, V],
other : T[K, V]
) -> T[K, V] {
match (self, other) {
(_, Empty) => Empty
(Empty, _) => Empty
(Leaf(k, v), _) =>
match other.get(k) {
Some(_) => Leaf(k, v)
None => Empty
}
(_, Leaf(k, _)) =>
match self.get(k) {
Some(v) => Leaf(k, v)
None => Empty
}
(Branch(sa1), Branch(sa2)) =>
Branch(sa1.intersection(sa2, fn(m1, m2) { m1.intersection(m2) }))
(_, _) =>
self
.iter()
.fold(init=Empty, fn(m, kv) {
if other.get(kv.0) is Some(_) {
m.add(kv.0, kv.1)
} else {
m
}
})
}
}

///|
/// Intersection two hashmaps with a function
pub fn[K : Eq + Hash, V] intersection_with(
self : T[K, V],
other : T[K, V],
f : (K, V, V) -> V
) -> T[K, V] {
match (self, other) {
(_, Empty) => Empty
(Empty, _) => Empty
(Leaf(k, v), _) =>
match other.get(k) {
Some(v2) => Leaf(k, f(k, v, v2))
None => Empty
}
(_, Leaf(k, v2)) =>
match self.get(k) {
Some(v1) => Leaf(k, f(k, v1, v2))
None => Empty
}
(Branch(sa1), Branch(sa2)) =>
Branch(sa1.intersection(sa2, fn(m1, m2) { m1.intersection_with(m2, f) }))
(_, _) =>
self
.iter()
.fold(init=Empty, fn(m, kv) {
match other.get(kv.0) {
Some(v2) => m.add(kv.0, f(kv.0, kv.1, v2))
None => m
}
})
}
}

///|
/// Difference of two hashmaps: elements in `self` but not in `other`
pub fn[K : Eq + Hash, V] difference(self : T[K, V], other : T[K, V]) -> T[K, V] {
match (self, other) {
(Empty, _) => Empty
(_, Empty) => self
(Leaf(k, v), _) =>
match other.get(k) {
Some(_) => Empty
None => Leaf(k, v)
}
(Branch(sa1), Branch(sa2)) => Branch(sa1.difference(sa2))
(_, _) =>
self
.iter()
.fold(init=Empty, fn(m, kv) {
if other.get(kv.0) is None {
m.add(kv.0, kv.1)
} else {
m
}
})
}
}

///|
/// Iterate through the elements in a hash map
pub fn[K, V] each(self : T[K, V], f : (K, V) -> Unit) -> Unit {
Expand Down
93 changes: 93 additions & 0 deletions immut/hashmap/HAMT_test.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,99 @@ test "HAMT::union leaf to non-overlapping map" {
assert_eq(u.get(2), Some(2))
}

///|
test "HAMT::intersection with empty" {
let m1 = @hashmap.of([(1, 1)])
let m2 = @hashmap.new()
assert_eq!(m1.intersection(m2).size(), 0)
assert_eq!(m2.intersection(m1).size(), 0)
}

///|
test "HAMT::intersection with leaf" {
let m1 = @hashmap.of([(1, 1), (2, 2)])
let m2 = @hashmap.singleton(2, 2)
let m3 = @hashmap.singleton(3, 3)
assert_eq!(m1.intersection(m2).get(2), Some(2))
assert_eq!(m1.intersection(m3).get(3), None)
assert_eq!(m2.intersection(m1).get(2), Some(2))
}

///|
test "HAMT::intersection with branch" {
let m1 = @hashmap.of([(1, 1), (2, 2), (3, 3)])
let m2 = @hashmap.of([(2, 2), (3, 30), (4, 4)])
let inter = m1.intersection(m2)
assert_eq!(inter.get(1), None)
assert_eq!(inter.get(2), Some(2))
assert_eq!(inter.get(3), Some(3))
assert_eq!(inter.get(4), None)
}

///|
test "HAMT::intersection_with with empty" {
let m1 = @hashmap.of([(1, 1)])
let m2 = @hashmap.new()
assert_eq!(m1.intersection_with(m2, fn(_k, v1, v2) { v1 + v2 }).size(), 0)
assert_eq!(m2.intersection_with(m1, fn(_k, v1, v2) { v1 + v2 }).size(), 0)
}

///|
test "HAMT::intersection_with with leaf" {
let m1 = @hashmap.of([(1, 1), (2, 2)])
let m2 = @hashmap.singleton(2, 20)
let m3 = @hashmap.singleton(3, 30)
assert_eq!(
m1.intersection_with(m2, fn(_k, v1, v2) { v1 + v2 }).get(2),
Some(22),
)
assert_eq!(m1.intersection_with(m3, fn(_k, v1, v2) { v1 + v2 }).get(3), None)
assert_eq!(
m2.intersection_with(m1, fn(_k, v1, v2) { v1 + v2 }).get(2),
Some(22),
)
}

///|
test "HAMT::intersection_with with branch" {
let m1 = @hashmap.of([(1, 1), (2, 2), (3, 3)])
let m2 = @hashmap.of([(2, 20), (3, 30), (4, 4)])
let inter = m1.intersection_with(m2, fn(_k, v1, v2) { v1 * v2 })
assert_eq!(inter.get(1), None)
assert_eq!(inter.get(2), Some(40))
assert_eq!(inter.get(3), Some(90))
assert_eq!(inter.get(4), None)
}

///|
test "HAMT::difference with empty" {
let m1 = @hashmap.of([(1, 1)])
let m2 = @hashmap.new()
assert_eq!(m1.difference(m2), m1)
assert_eq!(m2.difference(m1).size(), 0)
}

///|
test "HAMT::difference with leaf" {
let m1 = @hashmap.of([(1, 1), (2, 2)])
let m2 = @hashmap.singleton(2, 2)
let m3 = @hashmap.singleton(3, 3)
assert_eq!(m1.difference(m2).get(2), None)
assert_eq!(m1.difference(m3).get(1), Some(1))
assert_eq!(m2.difference(m1).size(), 0)
}

///|
test "HAMT::difference with branch" {
let m1 = @hashmap.of([(1, 1), (2, 2), (3, 3)])
let m2 = @hashmap.of([(2, 2), (3, 30), (4, 4)])
let diff = m1.difference(m2)
assert_eq!(diff.get(1), Some(1))
assert_eq!(diff.get(2), None)
assert_eq!(diff.get(3), None)
assert_eq!(diff.get(4), None)
}

///|
test "HAMT::each" {
let empty = @hashmap.new()
Expand Down
12 changes: 12 additions & 0 deletions immut/hashmap/hashmap.mbti
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ fn[K : Eq + Hash, V] add(T[K, V], K, V) -> T[K, V]

fn[K : Eq + Hash, V] contains(T[K, V], K) -> Bool

fn[K : Eq + Hash, V] difference(T[K, V], T[K, V]) -> T[K, V]

fn[K, V] each(T[K, V], (K, V) -> Unit) -> Unit

fn[K, V] elems(T[K, V]) -> Iter[V]
Expand All @@ -28,6 +30,10 @@ fn[K : Eq + Hash, V] from_iter(Iter[(K, V)]) -> T[K, V]

fn[K : Eq + Hash, V] get(T[K, V], K) -> V?

fn[K : Eq + Hash, V] intersection(T[K, V], T[K, V]) -> T[K, V]

fn[K : Eq + Hash, V] intersection_with(T[K, V], T[K, V], (K, V, V) -> V) -> T[K, V]

fn[K, V] iter(T[K, V]) -> Iter[(K, V)]

fn[K, V] iter2(T[K, V]) -> Iter2[K, V]
Expand Down Expand Up @@ -55,11 +61,14 @@ fn[K, V] to_array(T[K, V]) -> Array[(K, V)]

fn[K : Eq + Hash, V] union(T[K, V], T[K, V]) -> T[K, V]

fn[K : Eq + Hash, V] union_with(T[K, V], T[K, V], (K, V, V) -> V) -> T[K, V]

// Types and methods
type T[K, V]
impl T {
add[K : Eq + Hash, V](Self[K, V], K, V) -> Self[K, V]
contains[K : Eq + Hash, V](Self[K, V], K) -> Bool
difference[K : Eq + Hash, V](Self[K, V], Self[K, V]) -> Self[K, V]
each[K, V](Self[K, V], (K, V) -> Unit) -> Unit
elems[K, V](Self[K, V]) -> Iter[V]
filter[K : Eq + Hash, V](Self[K, V], (V) -> Bool) -> Self[K, V]
Expand All @@ -68,6 +77,8 @@ impl T {
fold[K, V, A](Self[K, V], init~ : A, (A, V) -> A) -> A
fold_with_key[K, V, A](Self[K, V], init~ : A, (A, K, V) -> A) -> A
get[K : Eq + Hash, V](Self[K, V], K) -> V?
intersection[K : Eq + Hash, V](Self[K, V], Self[K, V]) -> Self[K, V]
intersection_with[K : Eq + Hash, V](Self[K, V], Self[K, V], (K, V, V) -> V) -> Self[K, V]
iter[K, V](Self[K, V]) -> Iter[(K, V)]
iter2[K, V](Self[K, V]) -> Iter2[K, V]
keys[K, V](Self[K, V]) -> Iter[K]
Expand All @@ -79,6 +90,7 @@ impl T {
size[K, V](Self[K, V]) -> Int
to_array[K, V](Self[K, V]) -> Array[(K, V)]
union[K : Eq + Hash, V](Self[K, V], Self[K, V]) -> Self[K, V]
union_with[K : Eq + Hash, V](Self[K, V], Self[K, V], (K, V, V) -> V) -> Self[K, V]
}
impl[K : Eq + Hash, V : Eq] Eq for T[K, V]
impl[K : Hash, V : Hash] Hash for T[K, V]
Expand Down
50 changes: 50 additions & 0 deletions immut/hashset/HAMT.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,56 @@ pub fn[K : Eq + Hash] union(self : T[K], other : T[K]) -> T[K] {
}
}

///|
/// Intersect two hashsets
pub fn[K : Eq + Hash] intersection(self : T[K], other : T[K]) -> T[K] {
match (self, other) {
(_, Empty) => Empty
(Empty, _) => Empty
(Leaf(k), _) => if other.contains(k) { Leaf(k) } else { Empty }
(_, Leaf(k)) => if self.contains(k) { Leaf(k) } else { Empty }
(Branch(sa1), Branch(sa2)) => {
let res = sa1.intersection(sa2, fn(m1, m2) { m1.intersection(m2) })
if res.size() == 0 {
Empty
} else {
Branch(res)
}
}
(_, _) =>
self
.iter()
.fold(init=Empty, fn(m, k) {
if other.contains(k) {
m.add(k)
} else {
m
}
})
}
}

///|
/// Difference of two hashsets: elements in `self` but not in `other`
pub fn[K : Eq + Hash] difference(self : T[K], other : T[K]) -> T[K] {
match (self, other) {
(Empty, _) => Empty
(_, Empty) => self
(Leaf(k), _) => if other.contains(k) { Empty } else { Leaf(k) }
(Branch(sa1), Branch(sa2)) => Branch(sa1.difference(sa2))
(_, _) =>
self
.iter()
.fold(init=Empty, fn(m, k) {
if other.contains(k) {
m
} else {
m.add(k)
}
})
}
}

///|
/// Returns true if the hash set is empty.
pub fn[A] is_empty(self : T[A]) -> Bool {
Expand Down
40 changes: 40 additions & 0 deletions immut/hashset/HAMT_test.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -217,3 +217,43 @@ test "union 2 hashsets" {
let set4 = @hashset.of([1, 2, 3, 4, 5])
inspect(set3 == set4, content="true")
}

///|
test "@hashset.intersection with overlapping sets" {
let set1 = @hashset.of([1, 2, 3, 4])
let set2 = @hashset.of([3, 4, 5, 6])
let result = set1.intersection(set2)
inspect!(result, content="@immut/hashset.of([3, 4])")
}

///|
test "@hashset.intersection with disjoint sets" {
let set1 = @hashset.of([1, 2])
let set2 = @hashset.of([3, 4])
let result = set1.intersection(set2)
inspect!(result.is_empty(), content="true")
}

///|
test "@hashset.intersection with one empty set" {
let set1 = @hashset.of([1, 2, 3])
let set2 = @hashset.new()
let result = set1.intersection(set2)
inspect!(result.is_empty(), content="true")
}

///|
test "@hashset.intersection with identical sets" {
let set1 = @hashset.of([1, 2, 3])
let set2 = @hashset.of([1, 2, 3])
let result = set1.intersection(set2)
inspect!(result == set1, content="true")
}

///|
test "@hashset.intersection with subset" {
let set1 = @hashset.of([1, 2, 3, 4])
let set2 = @hashset.of([2, 3])
let result = set1.intersection(set2)
inspect!(result == @hashset.of([2, 3]), content="true")
}
Loading
Loading