1/**
2 * Stores unique values in a mutable hash set.
3 *
4 * `MutableHashSet` updates the same collection in place and supports fast
5 * membership checks, insertion, removal, clearing, and iteration. It is built
6 * on `MutableHashMap`: each set value is stored as a map key, so uniqueness
7 * follows the same hashing and equality rules as the underlying mutable hash
8 * map.
9 *
10 * @since 2.0.0
11 */
12import { format } from "./Formatter.ts"
13import * as Dual from "./Function.ts"
14import { type Inspectable, NodeInspectSymbol, toJson } from "./Inspectable.ts"
15import * as MutableHashMap from "./MutableHashMap.ts"
16import type { Pipeable } from "./Pipeable.ts"
17import { pipeArguments } from "./Pipeable.ts"
18import { hasProperty } from "./Predicate.ts"
19
20const TypeId = "~effect/MutableHashSet"
21
22/**
23 * A mutable hash set for storing unique values with Effect structural equality
24 * support.
25 *
26 * **When to use**
27 *
28 * Use to store and mutate a collection of unique values with Effect hashing and
29 * equality semantics.
30 *
31 * **Details**
32 *
33 * Operations mutate the set in place. Values that implement `Equal` / `Hash`
34 * can be de-duplicated structurally; other values use normal JavaScript
35 * reference or primitive equality.
36 *
37 * **Example** (Using a mutable hash set)
38 *
39 * ```ts import.meta.vitest
40 * import { MutableHashSet } from "effect"
41 *
42 * // Create a mutable hash set
43 * const set: MutableHashSet.MutableHashSet<string> = MutableHashSet.make(
44 * "apple",
45 * "banana"
46 * )
47 *
48 * // Add elements
49 * MutableHashSet.add(set, "cherry")
50 *
51 * // Check if elements exist
52 * MutableHashSet.has(set, "apple") // => true
53 * MutableHashSet.has(set, "grape") // => false
54 *
55 * // Collect the iterator values
56 * Array.from(set) // => ["apple", "banana", "cherry"]
57 *
58 * // Get size
59 * MutableHashSet.size(set) // => 3
60 * ```
61 *
62 * @category models
63 * @since 2.0.0
64 */
65export interface MutableHashSet<out V> extends Iterable<V>, Pipeable, Inspectable {
66 readonly [TypeId]: typeof TypeId
67 readonly keyMap: MutableHashMap.MutableHashMap<V, boolean>
68}
69
70/**
71 * Checks whether the specified value is a `MutableHashSet`, `false` otherwise.
72 *
73 * **When to use**
74 *
75 * Use to narrow an unknown value before treating it as a mutable hash set.
76 *
77 * **Details**
78 *
79 * The check looks for the `MutableHashSet` runtime marker.
80 *
81 * **Gotchas**
82 *
83 * Native `Set` values do not satisfy this check.
84 *
85 * @see {@link MutableHashSet} for the mutable hash set interface
86 *
87 * @category guards
88 * @since 4.0.0
89 */
90export const isMutableHashSet = <V>(value: unknown): value is MutableHashSet<V> => hasProperty(value, TypeId)
91
92const MutableHashSetProto: Omit<MutableHashSet<unknown>, "keyMap"> = {
93 [TypeId]: TypeId,
94 [Symbol.iterator](this: MutableHashSet<unknown>): Iterator<unknown> {
95 return Array.from(this.keyMap).map(([_]) => _)[Symbol.iterator]()
96 },
97 toString() {
98 return `MutableHashSet(${format(Array.from(this))})`
99 },
100 toJSON() {
101 return {
102 _id: "MutableHashSet",
103 values: toJson(Array.from(this))
104 }
105 },
106 [NodeInspectSymbol]() {
107 return this.toJSON()
108 },
109 pipe() {
110 return pipeArguments(this, arguments)
111 }
112}
113
114const fromHashMap = <V>(keyMap: MutableHashMap.MutableHashMap<V, boolean>): MutableHashSet<V> => {
115 const set = Object.create(MutableHashSetProto)
116 set.keyMap = keyMap
117 return set
118}
119
120/**
121 * Creates an empty MutableHashSet.
122 *
123 * **When to use**
124 *
125 * Use to create a fresh mutable set before adding values over time.
126 *
127 * **Details**
128 *
129 * Each call returns a new empty set backed by an empty `MutableHashMap`.
130 *
131 * **Example** (Creating an empty set)
132 *
133 * ```ts import.meta.vitest
134 * import { MutableHashSet } from "effect"
135 *
136 * const set = MutableHashSet.empty<string>()
137 *
138 * // Add some values
139 * MutableHashSet.add(set, "apple")
140 * MutableHashSet.add(set, "banana")
141 * MutableHashSet.add(set, "apple") // Duplicate, no effect
142 *
143 * MutableHashSet.size(set) // => 2
144 * Array.from(set) // => ["apple", "banana"]
145 * ```
146 *
147 * @see {@link make} for creating a set from explicit values
148 * @see {@link fromIterable} for creating a set from an iterable of values
149 * @see {@link clear} for emptying an existing mutable set
150 *
151 * @category constructors
152 * @since 2.0.0
153 */
154export const empty = <K = never>(): MutableHashSet<K> => fromHashMap(MutableHashMap.empty())
155
156/**
157 * Creates a MutableHashSet from an iterable collection of values.
158 * Duplicates are automatically removed.
159 *
160 * **When to use**
161 *
162 * Use to build a mutable hash set from any iterable of values.
163 *
164 * **Example** (Creating a set from an iterable)
165 *
166 * ```ts import.meta.vitest
167 * import { MutableHashSet } from "effect"
168 *
169 * const values = ["apple", "banana", "apple", "cherry", "banana"]
170 * const set = MutableHashSet.fromIterable(values)
171 *
172 * MutableHashSet.size(set) // => 3
173 * Array.from(set) // => ["apple", "banana", "cherry"]
174 *
175 * // Works with any iterable
176 * MutableHashSet.size(MutableHashSet.fromIterable(new Set([1, 2, 3]))) // => 3
177 *
178 * // From string characters
179 * Array.from(MutableHashSet.fromIterable("hello")) // => ["h", "e", "l", "o"]
180 * ```
181 *
182 * @category constructors
183 * @since 2.0.0
184 */
185export const fromIterable = <K = never>(keys: Iterable<K>): MutableHashSet<K> =>
186 fromHashMap(MutableHashMap.fromIterable(Array.from(keys).map((k) => [k, true])))
187
188/**
189 * Creates a MutableHashSet from a variable number of values.
190 * Duplicates are automatically removed.
191 *
192 * **When to use**
193 *
194 * Use to build a mutable hash set from explicit values.
195 *
196 * **Example** (Creating a set from values)
197 *
198 * ```ts import.meta.vitest
199 * import { MutableHashSet } from "effect"
200 *
201 * const set = MutableHashSet.make("apple", "banana", "apple", "cherry")
202 *
203 * MutableHashSet.size(set) // => 3
204 * Array.from(set) // => ["apple", "banana", "cherry"]
205 *
206 * // With numbers
207 * const numbers = MutableHashSet.make(1, 2, 3, 2, 1)
208 * MutableHashSet.size(numbers) // => 3
209 * Array.from(numbers) // => [1, 2, 3]
210 *
211 * // Mixed types
212 * MutableHashSet.size(MutableHashSet.make("hello", 42, true, "hello")) // => 3
213 * ```
214 *
215 * @category constructors
216 * @since 2.0.0
217 */
218export const make = <Keys extends ReadonlyArray<unknown>>(
219 ...keys: Keys
220): MutableHashSet<Keys[number]> => fromIterable(keys)
221
222/**
223 * Adds a value to the MutableHashSet, mutating the set in place.
224 * If the value already exists, the set remains unchanged.
225 *
226 * **When to use**
227 *
228 * Use to insert a value into a mutable set while keeping uniqueness.
229 *
230 * **Example** (Adding values)
231 *
232 * ```ts import.meta.vitest
233 * import { MutableHashSet } from "effect"
234 *
235 * const set = MutableHashSet.empty<string>()
236 *
237 * // Add new values
238 * MutableHashSet.add(set, "apple")
239 * MutableHashSet.add(set, "banana")
240 *
241 * MutableHashSet.size(set) // => 2
242 * MutableHashSet.has(set, "apple") // => true
243 *
244 * // Add duplicate (no effect)
245 * MutableHashSet.add(set, "apple")
246 * MutableHashSet.size(set) // => 2
247 *
248 * // Pipe-able version
249 * const addFruit = MutableHashSet.add("cherry")
250 * addFruit(set)
251 * MutableHashSet.size(set) // => 3
252 * ```
253 *
254 * @category mutations
255 * @since 2.0.0
256 */
257export const add: {
258 <V>(key: V): (self: MutableHashSet<V>) => MutableHashSet<V>
259 <V>(self: MutableHashSet<V>, key: V): MutableHashSet<V>
260} = Dual.dual<
261 <V>(key: V) => (self: MutableHashSet<V>) => MutableHashSet<V>,
262 <V>(self: MutableHashSet<V>, key: V) => MutableHashSet<V>
263>(2, (self, key) => (MutableHashMap.set(self.keyMap, key, true), self))
264
265/**
266 * Checks whether the MutableHashSet contains the specified value.
267 *
268 * **When to use**
269 *
270 * Use to test whether a mutable set currently contains a value.
271 *
272 * **Details**
273 *
274 * Membership follows the same hashing and equality rules as the underlying
275 * `MutableHashMap`.
276 *
277 * **Example** (Checking for a value)
278 *
279 * ```ts import.meta.vitest
280 * import { MutableHashSet } from "effect"
281 *
282 * const set = MutableHashSet.make("apple", "banana", "cherry")
283 *
284 * MutableHashSet.has(set, "apple") // => true
285 * MutableHashSet.has(set, "grape") // => false
286 *
287 * // Pipe-able version
288 * const hasApple = MutableHashSet.has("apple")
289 * hasApple(set) // => true
290 *
291 * // Check after adding
292 * MutableHashSet.add(set, "grape")
293 * MutableHashSet.has(set, "grape") // => true
294 * ```
295 *
296 * @see {@link add} for adding a value to the set
297 * @see {@link remove} for removing a value from the set
298 *
299 * @category predicates
300 * @since 2.0.0
301 */
302export const has: {
303 <V>(key: V): (self: MutableHashSet<V>) => boolean
304 <V>(self: MutableHashSet<V>, key: V): boolean
305} = Dual.dual<
306 <V>(key: V) => (self: MutableHashSet<V>) => boolean,
307 <V>(self: MutableHashSet<V>, key: V) => boolean
308>(2, (self, key) => MutableHashMap.has(self.keyMap, key))
309
310/**
311 * Removes the specified value from the MutableHashSet, mutating the set in place.
312 * If the value doesn't exist, the set remains unchanged.
313 *
314 * **When to use**
315 *
316 * Use to delete a value from a mutable set if it is present.
317 *
318 * **Example** (Removing a value)
319 *
320 * ```ts import.meta.vitest
321 * import { MutableHashSet } from "effect"
322 *
323 * const set = MutableHashSet.make("apple", "banana", "cherry")
324 *
325 * MutableHashSet.size(set) // => 3
326 *
327 * // Remove existing value
328 * MutableHashSet.remove(set, "banana")
329 * MutableHashSet.size(set) // => 2
330 * MutableHashSet.has(set, "banana") // => false
331 *
332 * // Remove non-existent value (no effect)
333 * MutableHashSet.remove(set, "grape")
334 * MutableHashSet.size(set) // => 2
335 *
336 * // Pipe-able version
337 * const removeFruit = MutableHashSet.remove("apple")
338 * removeFruit(set)
339 * MutableHashSet.size(set) // => 1
340 * ```
341 *
342 * @category mutations
343 * @since 2.0.0
344 */
345export const remove: {
346 <V>(key: V): (self: MutableHashSet<V>) => MutableHashSet<V>
347 <V>(self: MutableHashSet<V>, key: V): MutableHashSet<V>
348} = Dual.dual<
349 <V>(key: V) => (self: MutableHashSet<V>) => MutableHashSet<V>,
350 <V>(self: MutableHashSet<V>, key: V) => MutableHashSet<V>
351>(2, (self, key) => (MutableHashMap.remove(self.keyMap, key), self))
352
353/**
354 * Returns the number of unique values in the MutableHashSet.
355 *
356 * **When to use**
357 *
358 * Use to read how many unique values are currently stored in the set.
359 *
360 * **Example** (Checking set size)
361 *
362 * ```ts import.meta.vitest
363 * import { MutableHashSet } from "effect"
364 *
365 * const set = MutableHashSet.empty<string>()
366 * MutableHashSet.size(set) // => 0
367 *
368 * MutableHashSet.add(set, "apple")
369 * MutableHashSet.add(set, "banana")
370 * MutableHashSet.add(set, "apple") // Duplicate
371 * MutableHashSet.size(set) // => 2
372 *
373 * MutableHashSet.remove(set, "apple")
374 * MutableHashSet.size(set) // => 1
375 *
376 * MutableHashSet.clear(set)
377 * MutableHashSet.size(set) // => 0
378 * ```
379 *
380 * @category getters
381 * @since 2.0.0
382 */
383export const size = <V>(self: MutableHashSet<V>): number => MutableHashMap.size(self.keyMap)
384
385/**
386 * Removes all values from the MutableHashSet, mutating the set in place.
387 * The set becomes empty after this operation.
388 *
389 * **When to use**
390 *
391 * Use to empty a mutable set while keeping the same set instance.
392 *
393 * **Example** (Clearing all values)
394 *
395 * ```ts import.meta.vitest
396 * import { MutableHashSet } from "effect"
397 *
398 * const set = MutableHashSet.make("apple", "banana", "cherry")
399 *
400 * MutableHashSet.size(set) // => 3
401 *
402 * // Clear all values
403 * MutableHashSet.clear(set)
404 *
405 * MutableHashSet.size(set) // => 0
406 * MutableHashSet.has(set, "apple") // => false
407 * Array.from(set) // => []
408 *
409 * // Can still add new values after clearing
410 * MutableHashSet.add(set, "new")
411 * MutableHashSet.size(set) // => 1
412 * ```
413 *
414 * @category mutations
415 * @since 2.0.0
416 */
417export const clear = <V>(self: MutableHashSet<V>): MutableHashSet<V> => (MutableHashMap.clear(self.keyMap), self)