HashSet
A HashSet represents an unordered collection of unique values with efficient lookup, insertion and removal operations.
The Effect library provides two versions of this structure:
- HashSet - Immutable
- MutableHashSet - Mutable
Both versions provide constant-time operations on average. The main difference is how they handle changes: one returns new sets, the other modifies the original.
Why use HashSet?
HashSet solves the problem of maintaining an unsorted collection where each value appears exactly once, with fast operations for checking membership and adding/removing values.
Some common use cases include:
- Tracking unique items (e.g., users who have completed an action)
- Efficiently testing for membership in a collection
- Performing set operations like union, intersection, and difference
- Eliminating duplicates from a collection
When to use HashSet Instead of other collections
Choose HashSet (either variant) over other collections when:
- You need to ensure elements are unique
- You frequently need to check if an element exists in the collection
- You need to perform set operations like union, intersection, and difference
- The order of elements doesn’t matter to your use case
Choose other collections when:
- You need to maintain insertion order (use
ListorArray) - You need key-value associations (use
HashMaporMutableHashMap) - You need to frequently access elements by index (use
Array)
Choosing between immutable and mutable variants
Effect offers both immutable and mutable versions to support different coding styles and performance needs.
HashSet
This version never modifies the original set. Instead, it returns a new set for every change.
Characteristics:
- Operations return new instances instead of modifying the original
- Previous states are preserved
- Thread-safe by design
- Ideal for functional programming patterns
- Suitable for sharing across different parts of your application
MutableHashSet
This version allows direct updates: adding and removing values changes the set in place.
Characteristics:
- Operations modify the original set directly
- More efficient when building sets incrementally
- Requires careful handling to avoid unexpected side effects
- Better performance in scenarios with many modifications
- Ideal for localized use where mutations won’t cause issues elsewhere
When to use each variant
Use HashSet when:
- You need predictable behavior with no side effects
- You want to preserve previous states of your data
- You’re sharing sets across different parts of your application
- You prefer functional programming patterns
- You need fiber safety in concurrent environments
Use MutableHashSet when:
- Performance is critical, and you need to avoid creating new instances
- You’re building a collection incrementally with many additions/removals
- You’re working in a controlled scope where mutation is safe
- You need to optimize memory usage in performance-critical code
Hybrid approach
You can apply multiple updates to a HashSet in a temporary mutable context using HashSet.mutate. This allows you to perform several changes at once without modifying the original set.
Example (Batching changes without mutating the original)
import { HashSet } from "effect"
// Create an immutable HashSetconst original = HashSet.make(1, 2, 3)
// Apply several updates inside a temporary mutable draftconst modified = HashSet.mutate(original, (draft) => { HashSet.add(draft, 4) HashSet.add(draft, 5) HashSet.remove(draft, 1)})
console.log(HashSet.toValues(original))// Output: [1, 2, 3] - original remains unchanged
console.log(HashSet.toValues(modified))// Output: [2, 3, 4, 5] - changes applied to a new versionPerformance characteristics
Both HashSet and MutableHashSet offer similar average-time performance for core operations:
| Operation | HashSet | MutableHashSet | Description |
|---|---|---|---|
| Lookup | O(1) average | O(1) average | Check if a value exists |
| Insertion | O(1) average | O(1) average | Add a value |
| Removal | O(1) average | O(1) average | Remove a value |
| Iteration | O(n) | O(n) | Iterate over all values |
| Set operations | O(n) | O(n) | Union, intersection, difference |
The main difference is how updates are handled:
- HashSet returns a new set for each change. This can be slower if many changes are made in a row.
- MutableHashSet updates the same set in place. This is usually faster when performing many changes.
Equality and uniqueness
Both HashSet and MutableHashSet use Effect’s Equal trait to determine if two elements are the same. This ensures that each value appears only once in the set.
- Primitive values (like numbers or strings) are compared by value, similar to the
===operator. - Objects and custom types must implement the
Equalinterface to define what it means for two instances to be equal. If no implementation is provided, equality falls back to reference comparison.
Example (Using custom equality and hashing)
import { Equal, Hash, HashSet } from "effect"
// Define a custom class that implements the Equal interfaceclass Person implements Equal.Equal { constructor( readonly id: number, readonly name: string, readonly age: number, ) {}
// Two Person instances are equal if their id, name, and age match [Equal.symbol](that: Equal.Equal): boolean { if (that instanceof Person) { return ( Equal.equals(this.id, that.id) && Equal.equals(this.name, that.name) && Equal.equals(this.age, that.age) ) } return false }
// Hash code is based on the id (must match the equality logic) [Hash.symbol](): number { return Hash.hash(this.id) }}
// Add two different instances with the same contentconst set = HashSet.empty().pipe( HashSet.add(new Person(1, "Alice", 30)), HashSet.add(new Person(1, "Alice", 30)),)
// Only one instance is keptconsole.log(HashSet.size(set))// Output: 1Simplifying Equality with Data and Schema
Effect’s Data and Schema.Data modules implement Equal for you automatically, based on structural equality.
Example (Using Data.struct)
import { Data, Equal, HashSet, pipe } from "effect"
// Define two records with the same contentconst person1 = Data.struct({ id: 1, name: "Alice", age: 30 })const person2 = Data.struct({ id: 1, name: "Alice", age: 30 })
// They are different object referencesconsole.log(Object.is(person1, person2))// Output: false
// But they are equal in value (based on content)console.log(Equal.equals(person1, person2))// Output: true
// Add both to a HashSet — only one will be storedconst set = pipe(HashSet.empty(), HashSet.add(person1), HashSet.add(person2))
console.log(HashSet.size(set))// Output: 1Example (Using Schema.Data)
import { Equal, MutableHashSet, Schema } from "effect"
// Define a schema that describes the structure of a Personconst PersonSchema = Schema.Data( Schema.Struct({ id: Schema.Number, name: Schema.String, age: Schema.Number, }),)
// Decode values from plain objectsconst Person = Schema.decodeSync(PersonSchema)
const person1 = Person({ id: 1, name: "Alice", age: 30 })const person2 = Person({ id: 1, name: "Alice", age: 30 })
// person1 and person2 are different instances but equal in valueconsole.log(Equal.equals(person1, person2))// Output: true
// Add both to a MutableHashSet — only one will be storedconst set = MutableHashSet.empty().pipe(MutableHashSet.add(person1), MutableHashSet.add(person2))
console.log(MutableHashSet.size(set))// Output: 1HashSet
A HashSet<A> is an immutable, unordered collection of unique values.
It guarantees that each value appears only once and supports fast operations like lookup, insertion, and removal.
Any operation that would modify the set (like adding or removing a value) returns a new HashSet, leaving the original unchanged.
Operations
| Category | Operation | Description | Time Complexity |
|---|---|---|---|
| constructors | empty | Creates an empty HashSet | O(1) |
| constructors | fromIterable | Creates a HashSet from an iterable | O(n) |
| constructors | make | Creates a HashSet from multiple values | O(n) |
| elements | has | Checks if a value exists in the set | O(1) avg |
| elements | some | Checks if any element satisfies a predicate | O(n) |
| elements | every | Checks if all elements satisfy a predicate | O(n) |
| elements | isSubset | Checks if a set is a subset of another | O(n) |
| getters | values | Gets an Iterator of all values |
O(1) |
| getters | toValues | Gets an Array of all values |
O(n) |
| getters | size | Gets the number of elements | O(1) |
| mutations | add | Adds a value to the set | O(1) avg |
| mutations | remove | Removes a value from the set | O(1) avg |
| mutations | toggle | Toggles a value’s presence | O(1) avg |
| operations | difference | Computes set difference (A - B) | O(n) |
| operations | intersection | Computes set intersection (A ∩ B) | O(n) |
| operations | union | Computes set union (A ∪ B) | O(n) |
| mapping | map | Transforms each element | O(n) |
| sequencing | flatMap | Transforms and flattens elements | O(n) |
| traversing | forEach | Applies a function to each element | O(n) |
| folding | reduce | Reduces the set to a single value | O(n) |
| filtering | filter | Keeps elements that satisfy a predicate | O(n) |
| partitioning | partition | Splits into two sets by a predicate | O(n) |
Example (Basic creation and operations)
import { HashSet } from "effect"
// Create an initial set with 3 valuesconst set1 = HashSet.make(1, 2, 3)
// Add a value (returns a new set)const set2 = HashSet.add(set1, 4)
// The original set is unchangedconsole.log(HashSet.toValues(set1))// Output: [1, 2, 3]
console.log(HashSet.toValues(set2))// Output: [1, 2, 3, 4]
// Perform set operations with another setconst set3 = HashSet.make(3, 4, 5)
// Combine both setsconst union = HashSet.union(set2, set3)
console.log(HashSet.toValues(union))// Output: [1, 2, 3, 4, 5]
// Shared valuesconst intersection = HashSet.intersection(set2, set3)
console.log(HashSet.toValues(intersection))// Output: [3, 4]
// Values only in set2const difference = HashSet.difference(set2, set3)
console.log(HashSet.toValues(difference))// Output: [1, 2]Example (Chaining with pipe)
import { HashSet, pipe } from "effect"
const result = pipe( // Duplicates are ignored HashSet.make(1, 2, 2, 3, 4, 5, 5), // Keep even numbers HashSet.filter((n) => n % 2 === 0), // Double each value HashSet.map((n) => n * 2), // Convert to array HashSet.toValues,)
console.log(result)// Output: [4, 8]MutableHashSet
A MutableHashSet<A> is a mutable, unordered collection of unique values.
Unlike HashSet, it allows direct modifications, operations like add, remove, and clear update the original set instead of returning a new one.
This mutability can improve performance when you need to build or update a set repeatedly, especially within a local or isolated scope.
Operations
| Category | Operation | Description | Complexity |
|---|---|---|---|
| constructors | empty | Creates an empty MutableHashSet | O(1) |
| constructors | fromIterable | Creates a set from an iterable | O(n) |
| constructors | make | Creates a set from multiple values | O(n) |
| elements | has | Checks if a value exists in the set | O(1) avg |
| elements | add | Adds a value to the set | O(1) avg |
| elements | remove | Removes a value from the set | O(1) avg |
| getters | size | Gets the number of elements | O(1) |
| mutations | clear | Removes all values from the set | O(1) |
Example (Working with a mutable set)
import { MutableHashSet } from "effect"
// Create a mutable set with initial valuesconst set = MutableHashSet.make(1, 2, 3)
// Add a new element (updates the set in place)MutableHashSet.add(set, 4)
// Check current contentsconsole.log([...set])// Output: [1, 2, 3, 4]
// Remove an element (modifies in place)MutableHashSet.remove(set, 1)
console.log([...set])// Output: [2, 3, 4]
// Clear the set entirelyMutableHashSet.clear(set)
console.log(MutableHashSet.size(set))// Output: 0Interoperability with JavaScript
Both HashSet and MutableHashSet implement the Iterable interface, so you can use them with JavaScript features like:
- the spread operator (
...) for...ofloopsArray.from
You can also extract values as an array using .toValues.
Example (Using HashSet values in JS-native ways)
import { HashSet, MutableHashSet } from "effect"
// Immutable HashSetconst hashSet = HashSet.make(1, 2, 3)
// Mutable variantconst mutableSet = MutableHashSet.make(4, 5, 6)
// Convert HashSet to an iterator//// ┌─── IterableIterator<number>// ▼const iterable = HashSet.values(hashSet)
// Spread into console.logconsole.log(...iterable)// Output: 1 2 3
// Use in a for...of loopfor (const value of mutableSet) { console.log(value)}// Output: 4 5 6
// Convert to array with Array.fromconsole.log(Array.from(mutableSet))// Output: [ 4, 5, 6 ]
// Convert immutable HashSet to array using toValues//// ┌─── Array<number>// ▼const array = HashSet.toValues(hashSet)
console.log(array)// Output: [ 1, 2, 3 ]