#Immutable HashSet Package Documentation

    This package provides an immutable hash-based set implementation using Hash Array Mapped Trie (HAMT) data structure. Unlike the mutable Set type, this provides persistent data structures that create new versions when modified while sharing structure efficiently.

    #Creating Immutable Sets

    Create immutable sets using various methods:

    ///|
    test "creating immutable sets" {
    // Empty set
    let empty : @hashset.HashSet[Int] = @hashset.new()
    inspect(empty.length(), content="0")
    inspect(empty.is_empty(), content="true")

    // From array
    let from_array_result = @hashset.HashSet([1, 2, 3, 2, 1]) // Duplicates removed
    inspect(from_array_result.length(), content="3")

    // From an array of distinct values
    let from_fixed = @hashset.HashSet([10, 20, 30])
    inspect(from_fixed.length(), content="3")

    // From iterator
    let from_iter = @hashset.from_iter(40, 50, 60)
    inspect(from_iter.length(), content="3")
    }

    #Immutable Operations

    All operations return new sets without modifying the original:

    ///|
    test "immutable operations" {
    let original = @hashset.HashSet([1, 2, 3])

    // Add element - returns new set
    let with_four = original.add(4)
    inspect(original.length(), content="3") // Original unchanged
    inspect(with_four.length(), content="4") // New set has additional element
    inspect(with_four.contains(4), content="true")

    // Remove element - returns new set
    let without_two = original.remove(2)
    inspect(original.length(), content="3") // Original unchanged
    inspect(without_two.length(), content="2") // New set missing element
    inspect(without_two.contains(2), content="false")

    // Original set remains unmodified
    inspect(original.contains(2), content="true")
    }

    #Set Operations

    Perform mathematical set operations immutably:

    ///|
    test "set operations" {
    let set1 = @hashset.HashSet([1, 2, 3, 4])
    let set2 = @hashset.HashSet([3, 4, 5, 6])

    // Union - all elements from both sets
    let union_set = set1.union(set2)
    inspect(union_set.length(), content="6") // [1, 2, 3, 4, 5, 6]

    // Intersection - common elements only
    let intersection_set = set1.intersection(set2)
    inspect(intersection_set.length(), content="2") // [3, 4]

    // Difference - elements in first but not second
    let difference_set = set1.difference(set2)
    inspect(difference_set.length(), content="2") // [1, 2]

    // All original sets remain unchanged
    inspect(set1.length(), content="4")
    inspect(set2.length(), content="4")
    }

    #Equality and Hashing

    == is content equality: two sets are equal exactly when they hold the same elements, no matter which sequence of operations produced them. Hash agrees with ==, so equal sets always hash equally.

    ///|
    test "equality is content equality" {
    // Insertion order never matters
    let a = @hashset.new().add("x").add("y").add("z")
    let b = @hashset.new().add("z").add("y").add("x")
    assert_true(a == b)
    // Bulk construction and incremental adds agree
    let c : @hashset.HashSet[String] = HashSet(["x", "y", "z"])
    assert_true(c == a)
    // Operation history is invisible: add/remove round-trips restore
    // equality
    assert_true(a.add("w").remove("w") == a)
    // Equal sets hash equally
    let h1 = Hasher()
    h1.combine(a)
    let h2 = Hasher()
    h2.combine(b)
    assert_eq(h1.finalize(), h2.finalize())
    }

    This holds even for elements whose hashes collide:

    ///|
    /// Every element hashes to the same value: all of them share one bucket.
    priv struct OneBucketElem(Int) derive(Eq)

    ///|
    impl Hash for OneBucketElem with fn hash(_) {
    7
    }

    ///|
    impl Hash for OneBucketElem with fn hash_combine(_, hasher) {
    hasher.combine_int(7)
    }

    ///|
    test "collisions preserve content equality" {
    let a = @hashset.new().add(OneBucketElem(1)).add(OneBucketElem(2))
    let b = @hashset.new().add(OneBucketElem(2)).add(OneBucketElem(1))
    assert_true(a == b)
    // Shrinking a bucket back to one element restores the exact
    // single-element shape
    assert_true(a.remove(OneBucketElem(2)) == HashSet([OneBucketElem(1)]))
    }

    One boundary to be aware of: iteration order is unspecified. == and Hash are order-blind by design, but iter/each visit elements in an internal order that is not part of the contract — in particular, the relative order of elements sharing a collision bucket reflects insertion history. Sort the elements if you need a deterministic order.

    HashSet shares its HAMT representation — and the canonical form invariant that makes structural equality a correct implementation of content equality — with immut/hashmap: a subtree holding one element is always a Flat node, collision buckets exist only at maximum trie depth with at least two elements, and every shrinking operation collapses back to the shape a fresh construction would build. See immut/hashmap/README.mbt.md for the full internals story (path bit layout, node shapes, and collapse diagrams); this package's canonical-structure and QuickCheck property tests pin the same invariant for sets.

    #Membership and Queries

    Test membership and query the set:

    ///|
    test "membership and queries" {
    let numbers = @hashset.HashSet([10, 20, 30, 40, 50])

    // Membership testing
    inspect(numbers.contains(30), content="true")
    inspect(numbers.contains(35), content="false")

    // Size and emptiness
    inspect(numbers.length(), content="5")
    inspect(numbers.is_empty(), content="false")

    // Iterate over elements
    let mut sum = 0
    numbers.each(fn(x) { sum = sum + x })
    inspect(sum, content="150") // 10+20+30+40+50
    }

    #Iterator

    iter() returns an Iter over the set's elements.

    ///|
    test "iter" {
    let set = @hashset.HashSet([1, 2, 3])
    let arr = set.iter().to_array()
    arr.sort()
    @test.assert_eq(arr, [1, 2, 3])
    }

    #Structural Sharing

    Immutable sets share structure efficiently:

    ///|
    test "structural sharing" {
    let base_set = @hashset.HashSet([1, 2, 3, 4, 5])

    // Adding elements creates new sets that share structure
    let set_with_six = base_set.add(6)
    let set_with_seven = base_set.add(7)
    let set_with_eight = base_set.add(8)

    // All sets share the common structure [1, 2, 3, 4, 5]
    inspect(base_set.length(), content="5")
    inspect(set_with_six.length(), content="6")
    inspect(set_with_seven.length(), content="6")
    inspect(set_with_eight.length(), content="6")

    // Each retains the base elements
    inspect(set_with_six.contains(3), content="true")
    inspect(set_with_seven.contains(3), content="true")
    inspect(set_with_eight.contains(3), content="true")
    }

    #Filtering and Transformation

    Transform sets while maintaining immutability:

    ///|
    test "filtering and transformation" {
    let numbers = @hashset.HashSet([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])

    // Create filtered sets manually (no built-in filter/map)
    let evens = @hashset.HashSet([2, 4, 6, 8, 10])
    inspect(evens.length(), content="5")
    let doubled = @hashset.HashSet([2, 4, 6, 8, 10, 12, 14, 16, 18, 20])
    inspect(doubled.length(), content="10")
    inspect(doubled.contains(6), content="true") // 3 * 2 = 6

    // Original set unchanged
    inspect(numbers.length(), content="10")
    inspect(numbers.contains(3), content="true")
    }

    #Combining Sets

    Build complex sets from simpler ones:

    ///|
    test "combining sets" {
    let small_primes = @hashset.HashSet([2, 3, 5, 7])
    let small_evens = @hashset.HashSet([2, 4, 6, 8])
    let small_odds = @hashset.HashSet([1, 3, 5, 7, 9])

    // Combine multiple sets
    let all_small = small_primes.union(small_evens).union(small_odds)
    inspect(all_small.length(), content="9") // [1, 2, 3, 4, 5, 6, 7, 8, 9]

    // Find intersection of primes and odds
    let odd_primes = small_primes.intersection(small_odds)
    inspect(odd_primes.length(), content="3") // [3, 5, 7]

    // All original sets remain unchanged
    inspect(small_primes.length(), content="4")
    inspect(small_evens.length(), content="4")
    inspect(small_odds.length(), content="5")
    }

    #Comparison with Mutable Sets

    Key differences from mutable sets:

    ///|
    test "immutable vs mutable comparison" {
    // Immutable set - creates new instances
    let immut_set = @hashset.HashSet([1, 2, 3])
    let immut_with_four = immut_set.add(4)

    // Both sets exist independently
    inspect(immut_set.contains(4), content="false") // Original doesn't have 4
    inspect(immut_with_four.contains(4), content="true") // New one has 4

    // This demonstrates the immutable nature - both sets exist
    inspect(immut_set.length(), content="3")
    inspect(immut_with_four.length(), content="4")
    }

    #Advanced Operations

    More complex set operations:

    ///|
    test "advanced operations" {
    let set1 = @hashset.HashSet([1, 2, 3, 4, 5])
    let set2 = @hashset.HashSet([4, 5, 6, 7, 8])

    // Symmetric difference (elements in either but not both)
    let sym_diff = set1.difference(set2).union(set2.difference(set1))
    inspect(sym_diff.length(), content="6") // [1, 2, 3, 6, 7, 8]

    // Test intersection
    let intersection = set1.intersection(set2)
    inspect(intersection.length(), content="2") // [4, 5]

    // Test difference
    let diff = set1.difference(set2)
    inspect(diff.length(), content="3") // [1, 2, 3]
    }

    #Performance Benefits

    Immutable sets provide several performance advantages:

    ///|
    test "performance benefits" {
    let base = @hashset.HashSet([1, 2, 3, 4, 5])

    // Multiple derived sets share structure
    let derived1 = base.add(6)
    let derived2 = base.add(7)
    let derived3 = base.remove(1)

    // Efficient operations due to structural sharing
    inspect(derived1.length(), content="6")
    inspect(derived2.length(), content="6")
    inspect(derived3.length(), content="4")

    // Union of derived sets is efficient
    let combined = derived1.union(derived2)
    inspect(combined.length(), content="7") // [1, 2, 3, 4, 5, 6, 7]
    }

    #Use Cases

    Immutable sets are particularly useful for:

    1. Functional programming: Pure functions that don't modify data
    2. Concurrent programming: Safe sharing between threads
    3. Undo/redo systems: Keep history of set states
    4. Caching: Cache intermediate results without fear of modification
    5. Configuration management: Immutable configuration sets

    #Best Practices

    #1. Prefer Immutable for Functional Code

    ///|
    test "functional programming style" {
    fn process_numbers(
    _numbers : @hashset.HashSet[Int],
    ) -> @hashset.HashSet[Int] {
    // Manually create processed set (no built-in filter/map)
    let positive_squares = @hashset.HashSet([1, 4, 9]) // Squares of 1, 2, 3
    positive_squares.add(1) // Add the number 1 (though 1 already exists)
    }

    let input = @hashset.HashSet([-2, -1, 0, 1, 2, 3])
    let result = process_numbers(input)

    // Input unchanged, result is new set
    inspect(input.length(), content="6")
    inspect(result.contains(1), content="true") // Has 1
    inspect(result.contains(4), content="true") // Has 4
    inspect(result.contains(9), content="true") // Has 9
    }

    #2. Use for Configuration and State

    ///|
    test "configuration usage" {
    let base_config = @hashset.HashSet(["feature1", "feature2", "feature3"])
    fn enable_feature(
    config : @hashset.HashSet[String],
    feature : String,
    ) -> @hashset.HashSet[String] {
    config.add(feature)
    }

    fn disable_feature(
    config : @hashset.HashSet[String],
    feature : String,
    ) -> @hashset.HashSet[String] {
    config.remove(feature)
    }

    // Create different configurations
    let dev_config = enable_feature(base_config, "debug_mode")
    let prod_config = disable_feature(base_config, "feature3")
    inspect(base_config.length(), content="3") // Base unchanged
    inspect(dev_config.length(), content="4") // Has debug_mode
    inspect(prod_config.length(), content="2") // Missing feature3
    }

    #Memory Efficiency

    The HAMT structure provides:

    • Logarithmic depth: O(log n) operations
    • Structural sharing: Common subtrees shared between versions
    • Compact representation: Efficient memory usage
    • Cache-friendly access patterns: Good locality of reference

    The immutable hashset package provides efficient, thread-safe, and functionally pure set operations for MoonBit applications requiring persistent data structures.

    HashSet

    type HashSet[A] derive(Eq)

    impl Hash for HashSet[A]
    impl Show for HashSet[A]

    HashSet::HashSet

    fn[A : Eq + Hash] HashSet::HashSet(arr : ArrayView[A]) -> HashSet[A]

    Creates a hash set from an array of values.

    Example

    test {
    let set = @hashset.HashSet([3, 1, 2, 3])
    @test.assert_eq(set.contains(1), true)
    @test.assert_eq(set.contains(4), false)
    }

    HashSet::add

    fn[A : Eq + Hash] HashSet::add(self : HashSet[A], key : A) -> HashSet[A]

    Add a key to the hashset.

    HashSet::contains

    fn[A : Eq + Hash] HashSet::contains(self : HashSet[A], key : A) -> Bool

    Lookup a value from the hash set

    HashSet::difference

    fn[K : Eq] HashSet::difference(self : HashSet[K], other : HashSet[K]) -> HashSet[K]

    Difference of two hashsets: elements in self but not in other

    HashSet::each

    fn[A] HashSet::each(self : HashSet[A], f : (A) -> Unit raise?) -> Unit raise?

    Iterate through the elements in a hash set

    HashSet::equal

    fn[A : Eq] HashSet::equal(HashSet[A], HashSet[A]) -> Bool

    HashSet::from_array

    #deprecated("Use @immut/hashset.HashSet([...]) instead")
    #as_free_fn(of, deprecated="Use @immut/hashset.HashSet([...]) instead")
    #alias(of, deprecated="Use @immut/hashset.HashSet([...]) instead")
    #as_free_fn(deprecated="Use @immut/hashset.HashSet([...]) instead")
    fn[A : Eq + Hash] HashSet::from_array(arr : ArrayView[A]) -> HashSet[A]

    Creates a hash set from an array of values.

    HashSet::from_iter

    #as_free_fn(from_iterator, deprecated="Use HashSet::from_iter instead.")
    #alias(from_iterator, deprecated="`from_iterator` is deprecated, use `from_iter` instead")
    #as_free_fn
    fn[A : Eq + Hash] HashSet::from_iter(iter : Iter[A]) -> HashSet[A]

    Creates a hash set from an iterator of values.

    HashSet::hash

    fn[A : Hash] HashSet::hash(self : HashSet[A]) -> Int

    HashSet::intersection

    fn[K : Eq] HashSet::intersection(self : HashSet[K], other : HashSet[K]) -> HashSet[K]

    Intersect two hashsets

    HashSet::is_empty

    fn[A] HashSet::is_empty(self : HashSet[A]) -> Bool

    Returns true if the hash set is empty.

    HashSet::iter

    #alias(iterator, deprecated="`iterator` is deprecated, use `iter` instead")
    fn[A] HashSet::iter(self : HashSet[A]) -> Iter[A]

    Converted to Iter

    HashSet::length

    #alias(size, deprecated="`size` is deprecated, use `length` instead")
    fn[A] HashSet::length(self : HashSet[A]) -> Int

    Calculate the size of a set.

    WARNING: this operation is O(N) in set size

    HashSet::new

    #as_free_fn
    fn[A] HashSet::new() -> HashSet[A]

    Create a new instance.

    HashSet::remove

    fn[A : Eq + Hash] HashSet::remove(self : HashSet[A], key : A) -> HashSet[A]

    Remove an element from a set

    HashSet::union

    fn[K : Eq] HashSet::union(self : HashSet[K], other : HashSet[K]) -> HashSet[K]

    Union two hashsets