Repository navigation
feat: HashSet and ISet modules with the vocabulary of the FSharp.Core Set module - #29
xperiandri wants to merge 3 commits into
Conversation
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Copilot review overview
8 open issues
Functions that accept anIEqualityComparer<'T>don’t currently validatecomparerfor null, so… · New Functions that accept anIEqualityComparer<'T>don’t currently validatecomparerfor null, so… · New Functions that accept anIEqualityComparer<'T>don’t currently validatecomparerfor null, so… · New Functions that accept anIEqualityComparer<'T>don’t currently validatecomparerfor null, so… · New The primaryHashSetCE<'T>constructor accepts acomparerbut doesn’t validate it for null. If a… · New The complexity noteO(log n)is not generally true forIImmutableSet<'T>(it can be hash-based… · New These implementations allocate a newFunc<'T,bool>each call and route through LINQ… · New These implementations allocate a newFunc<'T,bool>each call and route through LINQ… · New
What changed in this PR
Adds new HashSet and ISet modules that mirror FSharp.Core Set semantics (argument order, names, exception behavior) and introduces hashSet { } / HashSetCE computation expression support, along with extensive shared contract tests and docs updates.
Changes:
- Introduces new
HashSetandISetAPIs (+ Option/Tuple “shadowing” namespaces) and ahashSetcomputation expression. - Adds shared set contract test infrastructure and per-module tests (comparers, null handling, builders, CE).
- Removes legacy
HashSet/ISetabbreviations fromSets.fsand updates docs/changelog accordingly.
| File | Description |
|---|---|
| tests/FSharp.Collections.Immutable.Tests/HashSet/ISetTupleTests.fs | Verifies Tuple-namespace ISet tuple-returning overloads shadow defaults correctly. |
| tests/FSharp.Collections.Immutable.Tests/HashSet/ISetTests.fs | Adds behavioral tests for ISet (implementation retention, comparer behavior, null rejection). |
| tests/FSharp.Collections.Immutable.Tests/HashSet/ISetOptionTests.fs | Verifies Option-namespace ISet option-returning overloads shadow defaults correctly. |
| tests/FSharp.Collections.Immutable.Tests/HashSet/ISetContractTests.fs | Wires ISet into shared reading/transform/mutation contracts. |
| tests/FSharp.Collections.Immutable.Tests/HashSet/HashSetTupleTests.fs | Verifies Tuple-namespace HashSet tuple-returning overloads shadow defaults correctly. |
| tests/FSharp.Collections.Immutable.Tests/HashSet/HashSetTests.fs | Adds behavioral tests for HashSet (comparers, sharing, null rejection, extras). |
| tests/FSharp.Collections.Immutable.Tests/HashSet/HashSetOptionTests.fs | Verifies Option-namespace HashSet option-returning overloads shadow defaults correctly. |
| tests/FSharp.Collections.Immutable.Tests/HashSet/HashSetOps.fs | Defines contract-test ops records for HashSet and ISet. |
| tests/FSharp.Collections.Immutable.Tests/HashSet/HashSetContractTests.fs | Wires HashSet into shared reading/transform/mutation/creation contracts. |
| tests/FSharp.Collections.Immutable.Tests/HashSet/HashSetComputationExpressionTests.fs | Tests hashSet { } and HashSetCE comparer { } behavior. |
| tests/FSharp.Collections.Immutable.Tests/HashSet/HashSetBuilderTests.fs | Tests builder functions (builder, builderWithComparer, ofBuilder, toBuilder). |
| tests/FSharp.Collections.Immutable.Tests/FSharp.Collections.Immutable.Tests.fsproj | Includes new set contract infra and HashSet/ISet tests in build. |
| tests/FSharp.Collections.Immutable.Tests/Contracts/SetTransformContract.fs | Adds shared transform-facet contract against FSharp.Core Set. |
| tests/FSharp.Collections.Immutable.Tests/Contracts/SetOps.fs | Adds ops record types and parameter-name mapping for set contracts. |
| tests/FSharp.Collections.Immutable.Tests/Contracts/SetMutationContract.fs | Adds shared mutation-facet contract for set modules. |
| tests/FSharp.Collections.Immutable.Tests/Contracts/SetCreationContract.fs | Adds shared creation-facet contract for concrete set modules. |
| tests/FSharp.Collections.Immutable.Tests/Contracts/SetContract.fs | Adds shared reading-facet contract base + shared inputs/helpers. |
| src/FSharp.Collections.Immutable/Sets.fs | Removes legacy HashSet module/abbreviations; keeps only SortedSet legacy types. |
| src/FSharp.Collections.Immutable/Helper.fs | Adds shared Sets.reduce and HashSets helpers for set modules. |
| src/FSharp.Collections.Immutable/HashSet/ISetTuple.fsi | Adds Tuple-namespace ISet signatures returning reference tuples. |
| src/FSharp.Collections.Immutable/HashSet/ISetTuple.fs | Implements Tuple-namespace ISet by repacking struct tuples. |
| src/FSharp.Collections.Immutable/HashSet/ISetOption.fsi | Adds Option-namespace ISet signatures returning/to-taking options. |
| src/FSharp.Collections.Immutable/HashSet/ISetOption.fs | Implements Option-namespace ISet via voption<->option conversion. |
| src/FSharp.Collections.Immutable/HashSet/ISet.fsi | Adds main ISet public API surface for IImmutableSet<'T>. |
| src/FSharp.Collections.Immutable/HashSet/ISet.fs | Implements ISet APIs over IImmutableSet<'T>. |
| src/FSharp.Collections.Immutable/HashSet/HashSetTuple.fsi | Adds Tuple-namespace HashSet signatures returning reference tuples. |
| src/FSharp.Collections.Immutable/HashSet/HashSetTuple.fs | Implements Tuple-namespace HashSet by repacking struct tuples. |
| src/FSharp.Collections.Immutable/HashSet/HashSetOption.fsi | Adds Option-namespace HashSet signatures returning/to-taking options. |
| src/FSharp.Collections.Immutable/HashSet/HashSetOption.fs | Implements Option-namespace HashSet via voption<->option conversion. |
| src/FSharp.Collections.Immutable/HashSet/HashSetCE.fs | Adds HashSetCE computation expression builder implementation. |
| src/FSharp.Collections.Immutable/HashSet/HashSet.fsi | Adds main HashSet public API surface mirroring FSharp.Core Set. |
| src/FSharp.Collections.Immutable/HashSet/HashSet.fs | Implements HashSet APIs over ImmutableHashSet<'T>. |
| src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj | Includes new HashSet/ISet sources in library build. |
| src/FSharp.Collections.Immutable/ComputationExpressions.fs | Adds exported hashSet<'T> CE value. |
| docsSrc/index.md | Documents HashSet CE and new ISet module. |
| docsSrc/Explanations/Background.md | Documents HashSet CE and new ISet module. |
| README.md | Documents HashSet CE and new ISet module. |
| CHANGELOG.md | Documents new modules/features and breaking changes/removals. |
Review effort: Lite
2aa035e to
b672ffa
Compare
6ebbce8 to
2b5179a
Compare
b672ffa to
9722553
Compare
2b5179a to
9d2526e
Compare
There was a problem hiding this comment.
🔵 Needs a closer look
The broad breaking API rewrite and deliberate semantic differences warrant final maintainer validation.
No open issues 🎉
8 resolved since last review
The complexity noteO(log n)is not generally true forIImmutableSet<'T>(it can be hash-based… The primaryHashSetCE<'T>constructor accepts acomparerbut doesn’t validate it for null. If a… Functions that accept anIEqualityComparer<'T>don’t currently validatecomparerfor null, so… Functions that accept anIEqualityComparer<'T>don’t currently validatecomparerfor null, so… Functions that accept anIEqualityComparer<'T>don’t currently validatecomparerfor null, so… Functions that accept anIEqualityComparer<'T>don’t currently validatecomparerfor null, so… These implementations allocate a newFunc<'T,bool>each call and route through LINQ… These implementations allocate a newFunc<'T,bool>each call and route through LINQ…
Review effort: Balanced
9d2526e to
047ee9b
Compare
…FSharp.Core `Set` module Follows the reworked ADR 0001 (D2, D5, D12) and the review of #29: - `HashSet`, `ISet` and their `Option` and `Tuple` modules are the root modules of their files. The `HashSet<'T>` abbreviation moves to `HashSet/HashSetTypes.fs`, which needs no signature file. The files that open `System.Collections.Generic` open `FSharp.Collections.Immutable` after it, so that `HashSet` names the abbreviation, not the mutable set of the BCL. - `CustomComparerHashSet` (D12) creates hash sets with an equality comparer: its members `empty`, `ofArray`, `ofList`, `ofSeq`, `singleton` and `builder` have the names and the compiled names of the module functions. `HashSet.forComparer` creates it. The struct has an explicit private field and constructor, because a primary constructor cannot check its argument: the constructor throws `ArgumentNullException` for a `null` comparer, and so does `forComparer`. The signature reveals the field, as F# requires of a struct (FS0314). The default value of the struct compares with `EqualityComparer<'T>.Default`. - `emptyWithComparer`, `ofSeqWithComparer`, `builderWithComparer` and `withComparer` are removed: `ofSeq` of the struct gives an existing hash set its comparer, and returns it when it has the comparer already. The creation functions of the module are the members of the struct with the default comparer, so that each has one implementation. - `HashSetCE` checks its comparer for `null` when it is created. - `HashSet.exists` and `forall` loop over the struct enumerator of the hash set and allocate nothing per call, where LINQ `Any` and `All` allocated 232 bytes, a delegate and the boxed enumerator. `ISet.exists` and `forall` call `Seq.exists` and `Seq.forall`, which allocate only the enumerator: 144 bytes instead of 232 for a hash set and 64 instead of 152 for a sorted set. `ISet.filter` keeps LINQ `Where`: `Seq.filter` allocates as many bytes and runs slower, and a list of the accepted elements allocates more for large sets (measured in Release, D5). - The complexity notes of `ISet` say that the complexity depends on the implementation of the set, and give it for `ImmutableHashSet` and, where it is the same, for `ImmutableSortedSet`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd `ISet` Follows the reworked ADR 0001 (D2, D8, D12) and the review of #29: - Test methods carry no categories: the classes carry them. - `HashSetOps` and `ISetOps` are the root modules of their own files. `SetInputs` and `SetParamNames` move out of the contract files into root modules of their own, so that the contract files hold types only. - The creation facet also runs the members of the struct that `forComparer` creates, with a comparer that considers two numbers equal when their remainders in the division by 5 are equal: `SetInputs.sameRemainder` for hash and frozen sets, `SetInputs.byRemainder` for sorted sets. It checks one element of each class, the comparer of the struct, the copy of an array, `null` inputs and an existing set that gets the comparer, in terms that hold for `SortedSet` and `FrozenSet` too. The facet asks for the `comparer` function of the module and for a set type that is a sequence of numbers. - `HashSet`: tests of `forComparer` and the members of the struct, of `ofSeq` giving an existing hash set the comparer or returning it, of the default value of the struct, and of the `ArgumentNullException` of `forComparer`, the constructor of the struct and `HashSetCE` for a `null` comparer, with the parameter name `comparer`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
9722553 to
8f55c4b
Compare
…FSharp.Core `Set` module Follows the reworked ADR 0001 (D2, D5, D12) and the review of #29: - `HashSet`, `ISet` and their `Option` and `Tuple` modules are the root modules of their files. The `HashSet<'T>` abbreviation moves to `HashSet/HashSetTypes.fs`, which needs no signature file. The files that open `System.Collections.Generic` open `FSharp.Collections.Immutable` after it, so that `HashSet` names the abbreviation, not the mutable set of the BCL. - `CustomComparerHashSet` (D12) creates hash sets with an equality comparer: its members `empty`, `ofArray`, `ofList`, `ofSeq`, `singleton` and `builder` have the names and the compiled names of the module functions. `HashSet.forComparer` creates it. The struct has an explicit private field and constructor, because a primary constructor cannot check its argument: the constructor throws `ArgumentNullException` for a `null` comparer, and so does `forComparer`. The signature reveals the field, as F# requires of a struct (FS0314). The default value of the struct compares with `EqualityComparer<'T>.Default`. - `emptyWithComparer`, `ofSeqWithComparer`, `builderWithComparer` and `withComparer` are removed: `ofSeq` of the struct gives an existing hash set its comparer, and returns it when it has the comparer already. The creation functions of the module are the members of the struct with the default comparer, so that each has one implementation. - `HashSetCE` checks its comparer for `null` when it is created. - `HashSet.exists` and `forall` loop over the struct enumerator of the hash set and allocate nothing per call, where LINQ `Any` and `All` allocated 232 bytes, a delegate and the boxed enumerator. `ISet.exists` and `forall` call `Seq.exists` and `Seq.forall`, which allocate only the enumerator: 144 bytes instead of 232 for a hash set and 64 instead of 152 for a sorted set. `ISet.filter` keeps LINQ `Where`: `Seq.filter` allocates as many bytes and runs slower, and a list of the accepted elements allocates more for large sets (measured in Release, D5). - The complexity notes of `ISet` say that the complexity depends on the implementation of the set, and give it for `ImmutableHashSet` and, where it is the same, for `ImmutableSortedSet`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd `ISet` Follows the reworked ADR 0001 (D2, D8, D12) and the review of #29: - Test methods carry no categories: the classes carry them. - `HashSetOps` and `ISetOps` are the root modules of their own files. `SetInputs` and `SetParamNames` move out of the contract files into root modules of their own, so that the contract files hold types only. - The creation facet also runs the members of the struct that `forComparer` creates, with a comparer that considers two numbers equal when their remainders in the division by 5 are equal: `SetInputs.sameRemainder` for hash and frozen sets, `SetInputs.byRemainder` for sorted sets. It checks one element of each class, the comparer of the struct, the copy of an array, `null` inputs and an existing set that gets the comparer, in terms that hold for `SortedSet` and `FrozenSet` too. The facet asks for the `comparer` function of the module and for a set type that is a sequence of numbers. - `HashSet`: tests of `forComparer` and the members of the struct, of `ofSeq` giving an existing hash set the comparer or returning it, of the default value of the struct, and of the `ArgumentNullException` of `forComparer`, the constructor of the struct and `HashSetCE` for a `null` comparer, with the parameter name `comparer`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8f55c4b to
93c99b5
Compare
…FSharp.Core `Set` module Follows the reworked ADR 0001 (D2, D5, D12) and the review of #29: - `HashSet`, `ISet` and their `Option` and `Tuple` modules are the root modules of their files. The `HashSet<'T>` abbreviation moves to `HashSet/HashSetTypes.fs`, which needs no signature file. The files that open `System.Collections.Generic` open `FSharp.Collections.Immutable` after it, so that `HashSet` names the abbreviation, not the mutable set of the BCL. - `CustomComparerHashSet` (D12) creates hash sets with an equality comparer: its members `empty`, `ofArray`, `ofList`, `ofSeq`, `singleton` and `builder` have the names and the compiled names of the module functions. `HashSet.forComparer` creates it. The struct has an explicit private field and constructor, because a primary constructor cannot check its argument: the constructor throws `ArgumentNullException` for a `null` comparer, and so does `forComparer`. The signature reveals the field, as F# requires of a struct (FS0314). The default value of the struct compares with `EqualityComparer<'T>.Default`. - `emptyWithComparer`, `ofSeqWithComparer`, `builderWithComparer` and `withComparer` are removed: `ofSeq` of the struct gives an existing hash set its comparer, and returns it when it has the comparer already. The creation functions of the module are the members of the struct with the default comparer, so that each has one implementation. - `HashSetCE` checks its comparer for `null` when it is created. - `HashSet.exists` and `forall` loop over the struct enumerator of the hash set and allocate nothing per call, where LINQ `Any` and `All` allocated 232 bytes, a delegate and the boxed enumerator. `ISet.exists` and `forall` call `Seq.exists` and `Seq.forall`, which allocate only the enumerator: 144 bytes instead of 232 for a hash set and 64 instead of 152 for a sorted set. `ISet.filter` keeps LINQ `Where`: `Seq.filter` allocates as many bytes and runs slower, and a list of the accepted elements allocates more for large sets (measured in Release, D5). - The complexity notes of `ISet` say that the complexity depends on the implementation of the set, and give it for `ImmutableHashSet` and, where it is the same, for `ImmutableSortedSet`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd `ISet` Follows the reworked ADR 0001 (D2, D8, D12) and the review of #29: - Test methods carry no categories: the classes carry them. - `HashSetOps` and `ISetOps` are the root modules of their own files. `SetInputs` and `SetParamNames` move out of the contract files into root modules of their own, so that the contract files hold types only. - The creation facet also runs the members of the struct that `forComparer` creates, with a comparer that considers two numbers equal when their remainders in the division by 5 are equal: `SetInputs.sameRemainder` for hash and frozen sets, `SetInputs.byRemainder` for sorted sets. It checks one element of each class, the comparer of the struct, the copy of an array, `null` inputs and an existing set that gets the comparer, in terms that hold for `SortedSet` and `FrozenSet` too. The facet asks for the `comparer` function of the module and for a set type that is a sequence of numbers. - `HashSet`: tests of `forComparer` and the members of the struct, of `ofSeq` giving an existing hash set the comparer or returning it, of the default value of the struct, and of the `ArgumentNullException` of `forComparer`, the constructor of the struct and `HashSetCE` for a `null` comparer, with the parameter name `comparer`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4a1836d to
3dc6fb8
Compare
…e `Set` module Every `seq<'T>` parameter rejects `null` and a default flat list that is given as a sequence with an `ArgumentNullException` that names the parameter of our function (ADR 0001, D6), as `HashSet` and `ISet` do since the review of #29. A default flat list is a boxed, non-null struct, so the check for `null` alone let it reach the builder or the BCL call, which failed with `InvalidOperationException` when it enumerated the sequence. - `SortedSet.ofSeq` and the member `ofSeq` of `CustomComparerSortedSet` (`elements`), `addRange` and `removeRange` (`elements`), and `intersectMany` and `unionMany` (`sets`) call `checkNotNullOrDefaultSeq` first. The check of each set of `sets` stays. `ofList` and `ofArray` take another type and keep their check for `null`. - The `yield!` overloads of `sortedSet { }` for a sequence, an array and a sorted set (`items`) and its `for` (`source`) check their input before the buffer is returned. A `null` array or sorted set reached `UnionWith`, so the exception named `other`, and a `null` source made `for` throw `NullReferenceException`. - The exception documentation of the affected functions says "null or a default flat list", as the documentation of `FlatList` does, and the helper `SortedSets.ofSeq` says that its callers check the input. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
758443d to
4562f5a
Compare
…FSharp.Core `Set` module Follows the reworked ADR 0001 (D2, D5, D12) and the review of #29: - `HashSet`, `ISet` and their `Option` and `Tuple` modules are the root modules of their files. The `HashSet<'T>` abbreviation moves to `HashSet/HashSetTypes.fs`, which needs no signature file. The files that open `System.Collections.Generic` open `FSharp.Collections.Immutable` after it, so that `HashSet` names the abbreviation, not the mutable set of the BCL. - `CustomComparerHashSet` (D12) creates hash sets with an equality comparer: its members `empty`, `ofArray`, `ofList`, `ofSeq`, `singleton` and `builder` have the names and the compiled names of the module functions. `HashSet.forComparer` creates it. The struct has an explicit private field and constructor, because a primary constructor cannot check its argument: the constructor throws `ArgumentNullException` for a `null` comparer, and so does `forComparer`. The signature reveals the field, as F# requires of a struct (FS0314). The default value of the struct compares with `EqualityComparer<'T>.Default`. - `emptyWithComparer`, `ofSeqWithComparer`, `builderWithComparer` and `withComparer` are removed: `ofSeq` of the struct gives an existing hash set its comparer, and returns it when it has the comparer already. The creation functions of the module are the members of the struct with the default comparer, so that each has one implementation. - `HashSetCE` checks its comparer for `null` when it is created. - `HashSet.exists` and `forall` loop over the struct enumerator of the hash set and allocate nothing per call, where LINQ `Any` and `All` allocated 232 bytes, a delegate and the boxed enumerator. `ISet.exists` and `forall` call `Seq.exists` and `Seq.forall`, which allocate only the enumerator: 144 bytes instead of 232 for a hash set and 64 instead of 152 for a sorted set. `ISet.filter` keeps LINQ `Where`: `Seq.filter` allocates as many bytes and runs slower, and a list of the accepted elements allocates more for large sets (measured in Release, D5). - The complexity notes of `ISet` say that the complexity depends on the implementation of the set, and give it for `ImmutableHashSet` and, where it is the same, for `ImmutableSortedSet`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd `ISet` Follows the reworked ADR 0001 (D2, D8, D12) and the review of #29: - Test methods carry no categories: the classes carry them. - `HashSetOps` and `ISetOps` are the root modules of their own files. `SetInputs` and `SetParamNames` move out of the contract files into root modules of their own, so that the contract files hold types only. - The creation facet also runs the members of the struct that `forComparer` creates, with a comparer that considers two numbers equal when their remainders in the division by 5 are equal: `SetInputs.sameRemainder` for hash and frozen sets, `SetInputs.byRemainder` for sorted sets. It checks one element of each class, the comparer of the struct, the copy of an array, `null` inputs and an existing set that gets the comparer, in terms that hold for `SortedSet` and `FrozenSet` too. The facet asks for the `comparer` function of the module and for a set type that is a sequence of numbers. - `HashSet`: tests of `forComparer` and the members of the struct, of `ofSeq` giving an existing hash set the comparer or returning it, of the default value of the struct, and of the `ArgumentNullException` of `forComparer`, the constructor of the struct and `HashSetCE` for a `null` comparer, with the parameter name `comparer`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
c5af422 to
cbf0a56
Compare
…e `Set` module Every `seq<'T>` parameter rejects `null` and a default flat list that is given as a sequence with an `ArgumentNullException` that names the parameter of our function (ADR 0001, D6), as `HashSet` and `ISet` do since the review of #29. A default flat list is a boxed, non-null struct, so the check for `null` alone let it reach the builder or the BCL call, which failed with `InvalidOperationException` when it enumerated the sequence. - `SortedSet.ofSeq` and the member `ofSeq` of `CustomComparerSortedSet` (`elements`), `addRange` and `removeRange` (`elements`), and `intersectMany` and `unionMany` (`sets`) call `checkNotNullOrDefaultSeq` first. The check of each set of `sets` stays. `ofList` and `ofArray` take another type and keep their check for `null`. - The `yield!` overloads of `sortedSet { }` for a sequence, an array and a sorted set (`items`) and its `for` (`source`) check their input before the buffer is returned. A `null` array or sorted set reached `UnionWith`, so the exception named `other`, and a `null` source made `for` throw `NullReferenceException`. - The exception documentation of the affected functions says "null or a default flat list", as the documentation of `FlatList` does, and the helper `SortedSets.ofSeq` says that its callers check the input. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…FSharp.Core `Set` module Follows the reworked ADR 0001 (D2, D5, D12) and the review of #29: - `HashSet`, `ISet` and their `Option` and `Tuple` modules are the root modules of their files. The `HashSet<'T>` abbreviation moves to `HashSet/HashSetTypes.fs`, which needs no signature file. The files that open `System.Collections.Generic` open `FSharp.Collections.Immutable` after it, so that `HashSet` names the abbreviation, not the mutable set of the BCL. - `CustomComparerHashSet` (D12) creates hash sets with an equality comparer: its members `empty`, `ofArray`, `ofList`, `ofSeq`, `singleton` and `builder` have the names and the compiled names of the module functions. `HashSet.forComparer` creates it. The struct has an explicit private field and constructor, because a primary constructor cannot check its argument: the constructor throws `ArgumentNullException` for a `null` comparer, and so does `forComparer`. The signature reveals the field, as F# requires of a struct (FS0314). The default value of the struct compares with `EqualityComparer<'T>.Default`. - `emptyWithComparer`, `ofSeqWithComparer`, `builderWithComparer` and `withComparer` are removed: `ofSeq` of the struct gives an existing hash set its comparer, and returns it when it has the comparer already. The creation functions of the module are the members of the struct with the default comparer, so that each has one implementation. - `HashSetCE` checks its comparer for `null` when it is created. - `HashSet.exists` and `forall` loop over the struct enumerator of the hash set and allocate nothing per call, where LINQ `Any` and `All` allocated 232 bytes, a delegate and the boxed enumerator. `ISet.exists` and `forall` call `Seq.exists` and `Seq.forall`, which allocate only the enumerator: 144 bytes instead of 232 for a hash set and 64 instead of 152 for a sorted set. `ISet.filter` keeps LINQ `Where`: `Seq.filter` allocates as many bytes and runs slower, and a list of the accepted elements allocates more for large sets (measured in Release, D5). - The complexity notes of `ISet` say that the complexity depends on the implementation of the set, and give it for `ImmutableHashSet` and, where it is the same, for `ImmutableSortedSet`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd `ISet` Follows the reworked ADR 0001 (D2, D8, D12) and the review of #29: - Test methods carry no categories: the classes carry them. - `HashSetOps` and `ISetOps` are the root modules of their own files. `SetInputs` and `SetParamNames` move out of the contract files into root modules of their own, so that the contract files hold types only. - The creation facet also runs the members of the struct that `forComparer` creates, with a comparer that considers two numbers equal when their remainders in the division by 5 are equal: `SetInputs.sameRemainder` for hash and frozen sets, `SetInputs.byRemainder` for sorted sets. It checks one element of each class, the comparer of the struct, the copy of an array, `null` inputs and an existing set that gets the comparer, in terms that hold for `SortedSet` and `FrozenSet` too. The facet asks for the `comparer` function of the module and for a set type that is a sequence of numbers. - `HashSet`: tests of `forComparer` and the members of the struct, of `ofSeq` giving an existing hash set the comparer or returning it, of the default value of the struct, and of the `ArgumentNullException` of `forComparer`, the constructor of the struct and `HashSetCE` for a `null` comparer, with the parameter name `comparer`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cbf0a56 to
008d0d0
Compare
…e `Set` module Every `seq<'T>` parameter rejects `null` and a default flat list that is given as a sequence with an `ArgumentNullException` that names the parameter of our function (ADR 0001, D6), as `HashSet` and `ISet` do since the review of #29. A default flat list is a boxed, non-null struct, so the check for `null` alone let it reach the builder or the BCL call, which failed with `InvalidOperationException` when it enumerated the sequence. - `SortedSet.ofSeq` and the member `ofSeq` of `CustomComparerSortedSet` (`elements`), `addRange` and `removeRange` (`elements`), and `intersectMany` and `unionMany` (`sets`) call `checkNotNullOrDefaultSeq` first. The check of each set of `sets` stays. `ofList` and `ofArray` take another type and keep their check for `null`. - The `yield!` overloads of `sortedSet { }` for a sequence, an array and a sorted set (`items`) and its `for` (`source`) check their input before the buffer is returned. A `null` array or sorted set reached `UnionWith`, so the exception named `other`, and a `null` source made `for` throw `NullReferenceException`. - The exception documentation of the affected functions says "null or a default flat list", as the documentation of `FlatList` does, and the helper `SortedSets.ofSeq` says that its callers check the input. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…FSharp.Core `Set` module Follows the reworked ADR 0001 (D2, D5, D12) and the review of #29: - `HashSet`, `ISet` and their `Option` and `Tuple` modules are the root modules of their files. The `HashSet<'T>` abbreviation moves to `HashSet/HashSetTypes.fs`, which needs no signature file. The files that open `System.Collections.Generic` open `FSharp.Collections.Immutable` after it, so that `HashSet` names the abbreviation, not the mutable set of the BCL. - `CustomComparerHashSet` (D12) creates hash sets with an equality comparer: its members `empty`, `ofArray`, `ofList`, `ofSeq`, `singleton` and `builder` have the names and the compiled names of the module functions. `HashSet.forComparer` creates it. The struct has an explicit private field and constructor, because a primary constructor cannot check its argument: the constructor throws `ArgumentNullException` for a `null` comparer, and so does `forComparer`. The signature reveals the field, as F# requires of a struct (FS0314). The default value of the struct compares with `EqualityComparer<'T>.Default`. - `emptyWithComparer`, `ofSeqWithComparer`, `builderWithComparer` and `withComparer` are removed: `ofSeq` of the struct gives an existing hash set its comparer, and returns it when it has the comparer already. The creation functions of the module are the members of the struct with the default comparer, so that each has one implementation. - `HashSetCE` checks its comparer for `null` when it is created. - `HashSet.exists` and `forall` loop over the struct enumerator of the hash set and allocate nothing per call, where LINQ `Any` and `All` allocated 232 bytes, a delegate and the boxed enumerator. `ISet.exists` and `forall` call `Seq.exists` and `Seq.forall`, which allocate only the enumerator: 144 bytes instead of 232 for a hash set and 64 instead of 152 for a sorted set. `ISet.filter` keeps LINQ `Where`: `Seq.filter` allocates as many bytes and runs slower, and a list of the accepted elements allocates more for large sets (measured in Release, D5). - The complexity notes of `ISet` say that the complexity depends on the implementation of the set, and give it for `ImmutableHashSet` and, where it is the same, for `ImmutableSortedSet`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd `ISet` Follows the reworked ADR 0001 (D2, D8, D12) and the review of #29: - Test methods carry no categories: the classes carry them. - `HashSetOps` and `ISetOps` are the root modules of their own files. `SetInputs` and `SetParamNames` move out of the contract files into root modules of their own, so that the contract files hold types only. - The creation facet also runs the members of the struct that `forComparer` creates, with a comparer that considers two numbers equal when their remainders in the division by 5 are equal: `SetInputs.sameRemainder` for hash and frozen sets, `SetInputs.byRemainder` for sorted sets. It checks one element of each class, the comparer of the struct, the copy of an array, `null` inputs and an existing set that gets the comparer, in terms that hold for `SortedSet` and `FrozenSet` too. The facet asks for the `comparer` function of the module and for a set type that is a sequence of numbers. - `HashSet`: tests of `forComparer` and the members of the struct, of `ofSeq` giving an existing hash set the comparer or returning it, of the default value of the struct, and of the `ArgumentNullException` of `forComparer`, the constructor of the struct and `HashSetCE` for a `null` comparer, with the parameter name `comparer`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
008d0d0 to
d31c12e
Compare
…e `Set` module Every `seq<'T>` parameter rejects `null` and a default flat list that is given as a sequence with an `ArgumentNullException` that names the parameter of our function (ADR 0001, D6), as `HashSet` and `ISet` do since the review of #29. A default flat list is a boxed, non-null struct, so the check for `null` alone let it reach the builder or the BCL call, which failed with `InvalidOperationException` when it enumerated the sequence. - `SortedSet.ofSeq` and the member `ofSeq` of `CustomComparerSortedSet` (`elements`), `addRange` and `removeRange` (`elements`), and `intersectMany` and `unionMany` (`sets`) call `checkNotNullOrDefaultSeq` first. The check of each set of `sets` stays. `ofList` and `ofArray` take another type and keep their check for `null`. - The `yield!` overloads of `sortedSet { }` for a sequence, an array and a sorted set (`items`) and its `for` (`source`) check their input before the buffer is returned. A `null` array or sorted set reached `UnionWith`, so the exception named `other`, and a `null` source made `for` throw `NullReferenceException`. - The exception documentation of the affected functions says "null or a default flat list", as the documentation of `FlatList` does, and the helper `SortedSets.ofSeq` says that its callers check the input. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…FSharp.Core `Set` module Follows the reworked ADR 0001 (D2, D5, D12) and the review of #29: - `HashSet`, `ISet` and their `Option` and `Tuple` modules are the root modules of their files. The `HashSet<'T>` abbreviation moves to `HashSet/HashSetTypes.fs`, which needs no signature file. The files that open `System.Collections.Generic` open `FSharp.Collections.Immutable` after it, so that `HashSet` names the abbreviation, not the mutable set of the BCL. - `CustomComparerHashSet` (D12) creates hash sets with an equality comparer: its members `empty`, `ofArray`, `ofList`, `ofSeq`, `singleton` and `builder` have the names and the compiled names of the module functions. `HashSet.forComparer` creates it. The struct has an explicit private field and constructor, because a primary constructor cannot check its argument: the constructor throws `ArgumentNullException` for a `null` comparer, and so does `forComparer`. The signature reveals the field, as F# requires of a struct (FS0314). The default value of the struct compares with `EqualityComparer<'T>.Default`. - `emptyWithComparer`, `ofSeqWithComparer`, `builderWithComparer` and `withComparer` are removed: `ofSeq` of the struct gives an existing hash set its comparer, and returns it when it has the comparer already. The creation functions of the module are the members of the struct with the default comparer, so that each has one implementation. - `HashSetCE` checks its comparer for `null` when it is created. - `HashSet.exists` and `forall` loop over the struct enumerator of the hash set and allocate nothing per call, where LINQ `Any` and `All` allocated 232 bytes, a delegate and the boxed enumerator. `ISet.exists` and `forall` call `Seq.exists` and `Seq.forall`, which allocate only the enumerator: 144 bytes instead of 232 for a hash set and 64 instead of 152 for a sorted set. `ISet.filter` keeps LINQ `Where`: `Seq.filter` allocates as many bytes and runs slower, and a list of the accepted elements allocates more for large sets (measured in Release, D5). - The complexity notes of `ISet` say that the complexity depends on the implementation of the set, and give it for `ImmutableHashSet` and, where it is the same, for `ImmutableSortedSet`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
d31c12e to
8ed4dc7
Compare
…nd `ISet` Follows the reworked ADR 0001 (D2, D8, D12) and the review of #29: - Test methods carry no categories: the classes carry them. - `HashSetOps` and `ISetOps` are the root modules of their own files. `SetInputs` and `SetParamNames` move out of the contract files into root modules of their own, so that the contract files hold types only. - The creation facet also runs the members of the struct that `forComparer` creates, with a comparer that considers two numbers equal when their remainders in the division by 5 are equal: `SetInputs.sameRemainder` for hash and frozen sets, `SetInputs.byRemainder` for sorted sets. It checks one element of each class, the comparer of the struct, the copy of an array, `null` inputs and an existing set that gets the comparer, in terms that hold for `SortedSet` and `FrozenSet` too. The facet asks for the `comparer` function of the module and for a set type that is a sequence of numbers. - `HashSet`: tests of `forComparer` and the members of the struct, of `ofSeq` giving an existing hash set the comparer or returning it, of the default value of the struct, and of the `ArgumentNullException` of `forComparer`, the constructor of the struct and `HashSetCE` for a `null` comparer, with the parameter name `comparer`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e `Set` module Every `seq<'T>` parameter rejects `null` and a default flat list that is given as a sequence with an `ArgumentNullException` that names the parameter of our function (ADR 0001, D6), as `HashSet` and `ISet` do since the review of #29. A default flat list is a boxed, non-null struct, so the check for `null` alone let it reach the builder or the BCL call, which failed with `InvalidOperationException` when it enumerated the sequence. - `SortedSet.ofSeq` and the member `ofSeq` of `CustomComparerSortedSet` (`elements`), `addRange` and `removeRange` (`elements`), and `intersectMany` and `unionMany` (`sets`) call `checkNotNullOrDefaultSeq` first. The check of each set of `sets` stays. `ofList` and `ofArray` take another type and keep their check for `null`. - The `yield!` overloads of `sortedSet { }` for a sequence, an array and a sorted set (`items`) and its `for` (`source`) check their input before the buffer is returned. A `null` array or sorted set reached `UnionWith`, so the exception named `other`, and a `null` source made `for` throw `NullReferenceException`. - The exception documentation of the affected functions says "null or a default flat list", as the documentation of `FlatList` does, and the helper `SortedSets.ofSeq` says that its callers check the input. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…FSharp.Core `Set` module Follows the reworked ADR 0001 (D2, D5, D12) and the review of #29: - `HashSet`, `ISet` and their `Option` and `Tuple` modules are the root modules of their files. The `HashSet<'T>` abbreviation moves to `HashSet/HashSetTypes.fs`, which needs no signature file. The files that open `System.Collections.Generic` open `FSharp.Collections.Immutable` after it, so that `HashSet` names the abbreviation, not the mutable set of the BCL. - `CustomComparerHashSet` (D12) creates hash sets with an equality comparer: its members `empty`, `ofArray`, `ofList`, `ofSeq`, `singleton` and `builder` have the names and the compiled names of the module functions. `HashSet.forComparer` creates it. The struct has an explicit private field and constructor, because a primary constructor cannot check its argument: the constructor throws `ArgumentNullException` for a `null` comparer, and so does `forComparer`. The signature reveals the field, as F# requires of a struct (FS0314). The default value of the struct compares with `EqualityComparer<'T>.Default`. - `emptyWithComparer`, `ofSeqWithComparer`, `builderWithComparer` and `withComparer` are removed: `ofSeq` of the struct gives an existing hash set its comparer, and returns it when it has the comparer already. The creation functions of the module are the members of the struct with the default comparer, so that each has one implementation. - `HashSetCE` checks its comparer for `null` when it is created. - `HashSet.exists` and `forall` loop over the struct enumerator of the hash set and allocate nothing per call, where LINQ `Any` and `All` allocated 232 bytes, a delegate and the boxed enumerator. `ISet.exists` and `forall` call `Seq.exists` and `Seq.forall`, which allocate only the enumerator: 144 bytes instead of 232 for a hash set and 64 instead of 152 for a sorted set. `ISet.filter` keeps LINQ `Where`: `Seq.filter` allocates as many bytes and runs slower, and a list of the accepted elements allocates more for large sets (measured in Release, D5). - The complexity notes of `ISet` say that the complexity depends on the implementation of the set, and give it for `ImmutableHashSet` and, where it is the same, for `ImmutableSortedSet`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd `ISet` Follows the reworked ADR 0001 (D2, D8, D12) and the review of #29: - Test methods carry no categories: the classes carry them. - `HashSetOps` and `ISetOps` are the root modules of their own files. `SetInputs` and `SetParamNames` move out of the contract files into root modules of their own, so that the contract files hold types only. - The creation facet also runs the members of the struct that `forComparer` creates, with a comparer that considers two numbers equal when their remainders in the division by 5 are equal: `SetInputs.sameRemainder` for hash and frozen sets, `SetInputs.byRemainder` for sorted sets. It checks one element of each class, the comparer of the struct, the copy of an array, `null` inputs and an existing set that gets the comparer, in terms that hold for `SortedSet` and `FrozenSet` too. The facet asks for the `comparer` function of the module and for a set type that is a sequence of numbers. - `HashSet`: tests of `forComparer` and the members of the struct, of `ofSeq` giving an existing hash set the comparer or returning it, of the default value of the struct, and of the `ArgumentNullException` of `forComparer`, the constructor of the struct and `HashSetCE` for a `null` comparer, with the parameter name `comparer`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8ed4dc7 to
7d3f26f
Compare
…e `Set` module Every `seq<'T>` parameter rejects `null` and a default flat list that is given as a sequence with an `ArgumentNullException` that names the parameter of our function (ADR 0001, D6), as `HashSet` and `ISet` do since the review of #29. A default flat list is a boxed, non-null struct, so the check for `null` alone let it reach the builder or the BCL call, which failed with `InvalidOperationException` when it enumerated the sequence. - `SortedSet.ofSeq` and the member `ofSeq` of `CustomComparerSortedSet` (`elements`), `addRange` and `removeRange` (`elements`), and `intersectMany` and `unionMany` (`sets`) call `checkNotNullOrDefaultSeq` first. The check of each set of `sets` stays. `ofList` and `ofArray` take another type and keep their check for `null`. - The `yield!` overloads of `sortedSet { }` for a sequence, an array and a sorted set (`items`) and its `for` (`source`) check their input before the buffer is returned. A `null` array or sorted set reached `UnionWith`, so the exception named `other`, and a `null` source made `for` throw `NullReferenceException`. - The exception documentation of the affected functions says "null or a default flat list", as the documentation of `FlatList` does, and the helper `SortedSets.ofSeq` says that its callers check the input. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…Core `Set` module
Rewrites `HashSet` after ADR 0001 in `HashSet/HashSet.fsi` and
`HashSet/HashSet.fs`: 45 functions in the canonical category order,
each documented in the signature file, with the names, the argument
order and the exceptions of the `Set` module.
- The functions call the members of `ImmutableHashSet`: `Contains`,
`TryGetValue`, `Add`, `Remove`, `Clear`, `WithComparer`, the set
algebra and the builders. `difference set1 set2` removes the
elements of `set2` from `set1`, `isSubset set1 set2` tests whether
`set1` is a subset of `set2`, and `intersectMany` of an empty
sequence throws what `Set.intersectMany` throws.
- `filter`, `partition`, `add`, `remove`, `clear` and the set algebra
keep the comparer of their input, the set algebra that of the first
set; `map`, `choose` and `partitionWith` build hash sets with the
default comparer of the result type.
- The new `ISet` module works on `IImmutableSet<'T>` without a type
abbreviation, which would shadow `System.Collections.Generic.ISet`.
Its transformations that keep the element type start from `Clear`
and fill it with `Union`, so they return the implementation and the
comparer of their input; the ones to another element type return a
`HashSet`.
- `tryGetValue` and `choose` use `voption` and `partition` and
`partitionWith` struct tuples; the `Option` and `Tuple` namespaces
repeat them with options and reference tuples.
- `hashSet { }` collects its elements in a builder; `HashSetCE`
created with an equality comparer builds hash sets that compare
with it.
- `Helper.fs` gets `Sets.reduce`, the combination of a sequence of sets
that `SortedSet` and `FrozenSet` will reuse, and the `HashSets`
algorithms of the type-changing transformations.
- The legacy `HashSet` module and the `HashSet`, `HashSetBuilder` and
`ISet` abbreviations are removed from `Sets.fs`; its `SortedSet` part
stays until its own rewrite.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- `SetOps`, `SetTransformOps`, `SetMutationOps` and `SetCreationOps`
record the functions of the four set facets of ADR 0001, D8:
reading, transformation, incremental change and creation.
- The oracle is the FSharp.Core `Set` module. Sets are compared as
sorted lists, because a hash set enumerates in an unspecified order;
folds and conversions are checked against the order of enumeration.
- `SetContractBase` compares exceptions with the `Set` oracle through
`ExceptionAssert.same`, so `Mirror` and `ExceptionAssert` stay
unchanged, and asserts `ArgumentNullException` where `Set`
dereferences a null input without a check.
- `HashSet` derives a test class from each facet, `ISet` from all but
creation, over hash sets behind the interface.
- Their own tests cover the comparers that the transformations keep or
reset, shared storage, `tryGetValue`, null inputs, the builder
functions, `hashSet { }` and the `Option` and `Tuple` namespaces.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7d3f26f to
0939b28
Compare


Proposed Changes
Rewrites
HashSet(ImmutableHashSet<'T>) and adds the interface moduleISet(IImmutableSet<'T>) after ADR 0001 (#23). It builds on #26 (FlatList), which is merged intomain.The published 1.0.0 has only the type abbreviations
ISet<'T>,SortedSet<'T>andHashSet<'T>for the sets, with no functions.mainhas an unreleasedHashSetmodule, which this pull request replaces. The changelog compares with 1.0.0, and the bullet "Againstmain" below compares withmain.HashSet,ISetand theirOptionandTuplemodules are the root modules of their files, for examplemodule FSharp.Collections.Immutable.HashSet.HashSet<'T>abbreviation is inHashSet/HashSetTypes.fs, which needs no signature file.HashSet/HashSet.CustomComparer.fsiand.fs. They nest underHashSet.fsiandHashSet.fsin Visual Studio through<DependentUpon>, and compile before the module.HashSetCE.fsicarries the signature and the XML docs of the builder of the computation expression.ComputationExpressions.fsideclareshashSetafterflatList.System.Collections.GenericopenFSharp.Collections.Immutableafter it. OtherwiseHashSetwould name the mutable set of the BCL instead of the abbreviation.HashSet/HashSet.fsiandHashSet.fs: 48 functions, in the canonical category order. They are theHashSetcolumn of ADR Appendix C.Setmodule keep its parameter names and its argument order. For example,difference set1 set2is set1 minus set2, andisSubset set1 set2is set1 ⊆ set2.comparer,tryGetValue,overlaps,setEqualsandsymmetricDifference, and the functions of the next three bullets.chooseandforComparerare extras.addRange elements setandremoveRange elements set: they wrapUnionandExceptwith a sequence, so a sequence needs noofSeqbeforeunionordifference.ImmutableHashSet.Unionis the exception: an empty input that receives a hash set gets that hash set or a copy of it, also when the argument is empty.addRangetherefore returns the input when the count is unchanged, whichUnionallows, because it only adds.nullelementsorsetthrowsArgumentNullExceptionwith the name of the parameter in the signature. The BCL names itother.ofReadOnlySpan spanandofSpan span: they wrapImmutableHashSet.Createwith a span, andofSpanconverts itsSpan<'T>. The first of the elements that the comparer finds equal stays. An empty span gives an empty hash set.build actionandupdate action set:buildfills a builder with an action, andupdatechanges a copy of a hash set with a builder, so the in-place operations of the builder (IntersectWith,SymmetricExceptWith, ...) are one expression.updatethrowsArgumentNullExceptionfor anullhash set.CustomComparerHashSetwithEqualityComparer<'T>.Default, so each creation has one implementation.existsandforallloop over the struct enumerator of the hash set and allocate nothing per call. LINQAnyandAllallocated 232 B per call (measured in Release).CustomComparerHashSet(D12):HashSet.forComparer comparercreates the struct, for example(HashSet.forComparer StringComparer.OrdinalIgnoreCase).ofSeq names. Creating it allocates nothing.build,builder,empty,ofArray,ofList,ofReadOnlySpan,ofSeq,ofSpanandsingletoncreate hash sets and builders that compare with the comparer.ofSeqalso gives an existing hash set the comparer, asWithComparerdoes. It returns the hash set itself when it has the same comparer instance. An equal but distinct instance rebuilds the hash set, because the BCL tests the instance (D14).forComparersay that anullcomparer throws, where the BCL members take it as the default comparer. They point toHashSet.ofSeqand to the default value of the struct for that meaning.nullcomparers:forComparerandHashSetCEthrowArgumentNullExceptionwith the parameter namecomparer.nullcomparer.seq<'T>parameter rejectsnulland a default flat list that is given as a sequence with anArgumentNullExceptionthat names the parameter of our function. That isofSeqofHashSetand of the struct,addRangeandremoveRange(elements),intersectManyandunionMany(sets) ofHashSetandISet, andyield!(items) andfor(source) ofhashSet { }. A default flat list is a boxed, non-null struct, so a check fornullalone let it reach the BCL call, which failed withInvalidOperationExceptionwhen it enumerated the sequence. The functions callcheckNotNullOrDefaultSeqfirst, so a default flat list of a derived type that arrives through the covariance ofseqis rejected too. Theyield!overloads for an array and for a hash set, and the expressionfor, check their input before the buffer is returned, so the exception no longer namesother(the parameter ofUnionWith) or comes as aNullReferenceException.ISet.fsiandISet.fs: 34 functions overIImmutableSet<'T>.Clear ()plusUnion).addRangeandremoveRangecallUnionandExceptwith a sequence and return the implementation of their input.addRangereturns the input when the count is unchanged, for the reason given above. The remarks say that which of two equal elements stays depends on the implementation:ImmutableHashSetkeeps the element of the set, whileImmutableSortedSet.Unionmay let a much larger sequence replace it.map,chooseandpartitionWithreturn aHashSet.ISet<'T>abbreviation, soSystem.Collections.Generic.ISet<'T>stays visible.ImmutableHashSet, and also forImmutableSortedSetwhere it is the same.existsandforallcallSeq.existsandSeq.forall. They allocate 144 B per call instead of 232 B with LINQ for a hash set, and 64 B instead of 152 B for a sorted set.filterkeeps LINQWhere, becauseSeq.filterallocates as much and runs slower.isProperSupersetanswers with set semantics under the comparer of the first set, as the documentation ofIsProperSupersetOfsays.ImmutableHashSetandImmutableSortedSetcount every element of the other sequence instead: elements that the comparer of the first set finds equal count twice. Thus{1, 2}against[1; 1]givesfalse, while the mutableHashSetandFrozenSetanswertrue.HashSetwraps the member when both hash sets have the same comparer. Otherwise it answers withIsSupersetOfandIsSubsetOf, which allocates one helper set.ISetalways does that, because the interface does not tell the comparers.IsProperSupersetOfwrong.isSubset,isSuperset,isProperSubset,overlapsandsetEqualswrap the BCL members.(HashSet.forComparer (HashSet.comparer set)).ofSeq elements, which gives the elements the comparer of the first hash set, as the members do.HashSet.ofSeq elementsconverts with the default comparer, which merges the elements that it finds equal before the function sees them. It gives the answer of the member only when the comparer of the first hash set finds equal every pair of elements that the default comparer finds equal, as the default comparer andOrdinalIgnoreCasedo.HashIdentity.Referenceon two strings with the same text is the counterexample: a hash set that holds both is a subset of the sequence of both, but not of the hash set that the default comparer makes of it.overlapsandisSupersetwith a sequence areSeq.existsandSeq.forallwithcontains, for every comparer. The same holds forintersectandsymmetricDifference.ISetcannot tell the comparer of its set: it converts the sequence withISet.addRange elements (ISet.clear set), which has the implementation and the comparer of the set.add,addRange,remove,removeRange,clear,update,filter,partitionand the set algebra keep the comparer of the input. When there are two input sets, they keep the comparer of the first one.map,chooseandpartitionWithuseEqualityComparer<'U>.Default, also when'Uis'T: keeping the comparer would need a type test. Each function documents this.intersectkeeps the element of the second set andunionandaddRangethe element of the first set, as the BCL does (D14).intersectManyof no sets throws whatSet.intersectManythrows.unionManystarts from the first set, so that the result keeps its comparer.Set.unionManystarts from the empty set.HashSetOptionandISetOptioncontainchooseandtryGetValue.HashSetTupleandISetTuplecontainpartitionandpartitionWith.hashSet { }uses the default comparer.HashSetCE comparer { }uses the given comparer.Helper/Sets.fsandHelper/HashSets.fs:Sets.reduceholds the shared part ofintersectManyandunionManyfor every set module.HashSetsholds the transformations that change the element type.Sets.fs: only the legacySortedSetremains. The unreleasedHashSetmodule ofmainand theISetandHashSetBuilderabbreviations are removed from it, and theHashSet<'T>abbreviation moved toHashSet/HashSetTypes.fs.main(the unreleasedHashSetmodule that this pull request replaces; none of this is in 1.0.0):lengthiscount,keyCompareriscomparer, andwhereisfilter.pick,tryPickandvTryPickare gone (HashSet.toSeq set |> Seq.pick chooser, orSeq.tryPickandValueOption.ofOption), and so is the publiccheck.ofComparer,ofSeqWithComparerandbuilderWithComparerare the membersempty,ofSeqandbuilderofHashSet.forComparer comparer.builderWithis gone without a replacement: its argument bound to the comparer overload of the BCL member, and hash sets have no capacity.differencetakes its arguments in the FSharp.Core order:mainhasdifference values set, which isremoveRange values setnow, andunion set valuesisaddRange values set.union,intersect,isSubset,isProperSubset,isSupersetandisProperSupersettake a hash set where they took a sequence.filterkeeps the comparer of its input, where it dropped it.unionManyof an empty sequence returns an empty hash set, where it threw.isProperSupersethas set semantics (see below). The compiled name of the module isHashSet, where it wasImmutableHashSetModule.Tests: 4751 per target framework, 1991 of them new.
CollectionAssert.AreEqual. They take their cases asDataRowandDynamicDatarows, so every case runs and reports as a test of its own.Contracts/(SetOps.fs,SetContract,SetTransformContract,SetMutationContractandSetCreationContract) compare every function with FSharp.CoreSeton the same elements. Sets are compared as sorted arrays.SetInputsis a static class of arrays with the row sources, andSetParamNamesis a root module.isProperSuperset, one of these pairs fails.forComparercreates, with the same comparer by remainder.SortedSetandFrozenSetwill run the same tests with their own comparers.addRangeandremoveRangewith the three shapes of a sequence (array, list and lazy sequence) over every pair of inputs, with the default comparer and with a comparer by remainder (D14: the elements of the set stay, and of the other elements the first of each remainder is added). The input itself must come back when the sequence adds nothing or removes nothing, with each comparer, and anullsequence is rejected.SortedSetderives these tests.ofReadOnlySpanandofSpanon a copy of every input that the test changes after the call, for the module and for the struct.HashSetpin what the contracts do not: the first of the equal elements of a span stays,addRangeof an empty hash set returns the empty input (the exception ofUnion), an equal but distinct comparer instance rebuilds inofSeqof the struct,buildandupdatewith the comparer of the struct and the in-place operations of the builder, and thenullhash set ofaddRange,removeRangeandupdate.ISetcheck thataddRangeandremoveRangekeep the implementation of a sorted set and the comparer of a hash set, andaddRangeof an empty hash set returns the empty input.null, the count check, the element that stays, the first or the last of equal elements, the comparer of the result, the order ofbuild, the span creators and the instance test ofofSeq), and a test failed each time.seq<'T>input is tested withnulland with a default flat list, in one data-row test for each of the functions ofHashSetand of the struct (15 rows), the functions ofISet(12 rows) andyield!andforofhashSet { }(8 rows). The rows also give a default flat list of strings as a sequence of objects, and a default flat list of hash sets as a sequence of interface sets, which a type test for the element type alone does not recognize. Three tests check that a flat list that is not default passes in these shapes. Each of the 13 fixes was reverted once and a test failed each time, and so did switching off the covariant case ofcheckNotNullOrDefaultSeq.HashSetandISet, in 30 rows each: the six relations andintersectandsymmetricDifference, each with the default comparer, a comparer that ignores case andHashIdentity.Referenceas the comparer of the first set, over the strings "a" (two instances), "A" and "b", andoverlapsandisSupersetasSeq.existsandSeq.forallwithcontainswith the three comparers. The conversion with the comparer of the first set answers as the member of the BCL type in every row. The conversion with the default comparer answers as the member for the first two comparers and differently for the reference comparer. Each pinned behavior was broken once (11 mutations: the struct converting with the default comparer, a relation comparing in the comparer of the second set,containsin the default comparer,intersectandsymmetricDifferenceconverting the second set with the default comparer,ISet.clearwithout the comparer, and the strings of the test data), and a test failed each time.Set.HashSetderives all four facets.ISetderives all of them except creation.forComparerand the struct;nullinput, andnullcomparers with their parameter names;Types of changes
What types of changes does your code introduce to FSharp.Collections.Immutable?
Put an
xin the boxes that applyThis change breaks code that uses the published 1.0.0 in one way: the
ISet<'T>abbreviation ofIImmutableSet<'T>is gone, because it shadowedSystem.Collections.Generic.ISet<'T>. UseIImmutableSet<'T>and theISetmodule. The published 1.0.0 has no other name that this pull request changes or removes:HashSet<'T>is unchanged, and the functions are new. The changes against the unreleasedmainare listed above.Checklist
Put an
xin the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code.Further comments
These decisions need a review:
nullinput:Set.ofSeqandSet.ofListdereference anullinput and throwNullReferenceException.HashSet.ofSeqandHashSet.ofListthrowArgumentNullExceptionwith the name of the parameter, as D6 requires fornullinput.map,chooseandpartitionWithalways use the default comparer of the result type (see Comparers above).HashSetCE<'T>is generic, because its comparer has the type of the elements. ThushashSetis a type function,[<GeneralizableValue>] let hashSet<'T> = HashSetCE<'T> (). EveryhashSet { }expression infers its own element type.addRangecompares the counts:ImmutableHashSet.Unionreturns the input itself when it adds nothing, except for an empty input that receives a hash set.addRangeofHashSetandISetreturns the input when the count is unchanged, so the documented promise holds in that case too. The check costs two reads ofCount.removeRangeneeds none:ExceptofImmutableHashSetandImmutableSortedSetreturns the input.ISet.addRangeand equal elements: the result depends on the implementation. A sorted set may take the elements of a much larger sequence instead of its own (a quirk ofImmutableSortedSet.Union), and the remarks say so.🤖 Generated with Claude Code