Skip to content

feat: HashSet and ISet modules with the vocabulary of the FSharp.Core Set module - #29

Open
xperiandri wants to merge 3 commits into
mainfrom
feat/hash-set
Open

xperiandri wants to merge 3 commits into
mainfrom
feat/hash-set

Conversation

@xperiandri

@xperiandri xperiandri commented Oct 18, 2026 •

Copy link
Copy Markdown
Collaborator

Proposed Changes

Rewrites HashSet (ImmutableHashSet<'T>) and adds the interface module ISet (IImmutableSet<'T>) after ADR 0001 (#23). It builds on #26 (FlatList), which is merged into main.

The published 1.0.0 has only the type abbreviations ISet<'T>, SortedSet<'T> and HashSet<'T> for the sets, with no functions. main has an unreleased HashSet module, which this pull request replaces. The changelog compares with 1.0.0, and the bullet "Against main" below compares with main.

  • Layout (D2):
    • HashSet, ISet and their Option and Tuple modules are the root modules of their files, for example module FSharp.Collections.Immutable.HashSet.
    • The HashSet<'T> abbreviation is in HashSet/HashSetTypes.fs, which needs no signature file.
    • The struct of D12 is in HashSet/HashSet.CustomComparer.fsi and .fs. They nest under HashSet.fsi and HashSet.fs in Visual Studio through <DependentUpon>, and compile before the module.
    • HashSetCE.fsi carries the signature and the XML docs of the builder of the computation expression. ComputationExpressions.fsi declares hashSet after flatList.
    • The files that open System.Collections.Generic open FSharp.Collections.Immutable after it. Otherwise HashSet would name the mutable set of the BCL instead of the abbreviation.
  • HashSet/HashSet.fsi and HashSet.fs: 48 functions, in the canonical category order. They are the HashSet column of ADR Appendix C.
    • The functions of the FSharp.Core Set module keep its parameter names and its argument order. For example, difference set1 set2 is set1 minus set2, and isSubset set1 set2 is set1 ⊆ set2.
    • The BCL wrappers are the builder functions, comparer, tryGetValue, overlaps, setEquals and symmetricDifference, and the functions of the next three bullets. choose and forComparer are extras.
    • addRange elements set and removeRange elements set: they wrap Union and Except with a sequence, so a sequence needs no ofSeq before union or difference.
      • They keep the comparer of the hash set. An element of the sequence that the comparer finds equal to an element of the hash set does not replace it, and of several equal elements of the sequence the first is added (D14).
      • The input itself comes back when nothing changes. ImmutableHashSet.Union is 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. addRange therefore returns the input when the count is unchanged, which Union allows, because it only adds.
      • A null elements or set throws ArgumentNullException with the name of the parameter in the signature. The BCL names it other.
    • ofReadOnlySpan span and ofSpan span: they wrap ImmutableHashSet.Create with a span, and ofSpan converts its Span<'T>. The first of the elements that the comparer finds equal stays. An empty span gives an empty hash set.
    • build action and update action set: build fills a builder with an action, and update changes a copy of a hash set with a builder, so the in-place operations of the builder (IntersectWith, SymmetricExceptWith, ...) are one expression. update throws ArgumentNullException for a null hash set.
    • The creation functions call the members of CustomComparerHashSet with EqualityComparer<'T>.Default, so each creation has one implementation.
    • exists and forall loop over the struct enumerator of the hash set and allocate nothing per call. LINQ Any and All allocated 232 B per call (measured in Release).
  • CustomComparerHashSet (D12):
    • HashSet.forComparer comparer creates the struct, for example (HashSet.forComparer StringComparer.OrdinalIgnoreCase).ofSeq names. Creating it allocates nothing.
    • Its members build, builder, empty, ofArray, ofList, ofReadOnlySpan, ofSeq, ofSpan and singleton create hash sets and builders that compare with the comparer.
    • ofSeq also gives an existing hash set the comparer, as WithComparer does. 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).
    • The remarks of forComparer say that a null comparer throws, where the BCL members take it as the default comparer. They point to HashSet.ofSeq and to the default value of the struct for that meaning.
  • null comparers:
    • The constructor of the struct, forComparer and HashSetCE throw ArgumentNullException with the parameter name comparer.
    • A primary constructor cannot check its argument, so the struct has an explicit field and constructor.
    • The default value of the struct compares with the default comparer, as the BCL does for a null comparer.
  • Sequence inputs (D6): 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. That is ofSeq of HashSet and of the struct, addRange and removeRange (elements), intersectMany and unionMany (sets) of HashSet and ISet, and yield! (items) and for (source) of hashSet { }. A default flat list is a boxed, non-null struct, so a check for null alone let it reach the BCL call, which failed with InvalidOperationException when it enumerated the sequence. The functions call checkNotNullOrDefaultSeq first, so a default flat list of a derived type that arrives through the covariance of seq is rejected too. The yield! overloads for an array and for a hash set, and the expression for, check their input before the buffer is returned, so the exception no longer names other (the parameter of UnionWith) or comes as a NullReferenceException.
  • ISet.fsi and ISet.fs: 34 functions over IImmutableSet<'T>.
    • The transformations that keep the element type also keep the implementation and its comparer (Clear () plus Union).
    • addRange and removeRange call Union and Except with a sequence and return the implementation of their input. addRange returns 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: ImmutableHashSet keeps the element of the set, while ImmutableSortedSet.Union may let a much larger sequence replace it.
    • map, choose and partitionWith return a HashSet.
    • There is no ISet<'T> abbreviation, so System.Collections.Generic.ISet<'T> stays visible.
    • The cost notes say that the cost depends on the implementation. They give it for ImmutableHashSet, and also for ImmutableSortedSet where it is the same.
    • exists and forall call Seq.exists and Seq.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. filter keeps LINQ Where, because Seq.filter allocates as much and runs slower.
  • Set relations (D14):
    • isProperSuperset answers with set semantics under the comparer of the first set, as the documentation of IsProperSupersetOf says. ImmutableHashSet and ImmutableSortedSet count 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] gives false, while the mutable HashSet and FrozenSet answer true.
    • HashSet wraps the member when both hash sets have the same comparer. Otherwise it answers with IsSupersetOf and IsSubsetOf, which allocates one helper set. ISet always does that, because the interface does not tell the comparers.
    • A probe of the six relations over every pair of the test inputs found only IsProperSupersetOf wrong. isSubset, isSuperset, isProperSubset, overlaps and setEquals wrap the BCL members.
    • A sequence for a relation: the relations and the set algebra take sets, where the members of the BCL types take sequences. The way to pass a sequence is to convert it with (HashSet.forComparer (HashSet.comparer set)).ofSeq elements, which gives the elements the comparer of the first hash set, as the members do. HashSet.ofSeq elements converts 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 and OrdinalIgnoreCase do. HashIdentity.Reference on 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. overlaps and isSuperset with a sequence are Seq.exists and Seq.forall with contains, for every comparer. The same holds for intersect and symmetricDifference. ISet cannot tell the comparer of its set: it converts the sequence with ISet.addRange elements (ISet.clear set), which has the implementation and the comparer of the set.
  • Comparers (D5):
    • add, addRange, remove, removeRange, clear, update, filter, partition and the set algebra keep the comparer of the input. When there are two input sets, they keep the comparer of the first one.
    • map, choose and partitionWith use EqualityComparer<'U>.Default, also when 'U is 'T: keeping the comparer would need a type test. Each function documents this.
    • When the comparer finds two different elements equal, intersect keeps the element of the second set and union and addRange the element of the first set, as the BCL does (D14).
  • Many sets:
    • intersectMany of no sets throws what Set.intersectMany throws.
    • unionMany starts from the first set, so that the result keeps its comparer. Set.unionMany starts from the empty set.
  • Option and Tuple namespaces: HashSetOption and ISetOption contain choose and tryGetValue. HashSetTuple and ISetTuple contain partition and partitionWith.
  • Computation expression (D7): hashSet { } uses the default comparer. HashSetCE comparer { } uses the given comparer.
  • Helper/Sets.fs and Helper/HashSets.fs: Sets.reduce holds the shared part of intersectMany and unionMany for every set module. HashSets holds the transformations that change the element type.
  • Sets.fs: only the legacy SortedSet remains. The unreleased HashSet module of main and the ISet and HashSetBuilder abbreviations are removed from it, and the HashSet<'T> abbreviation moved to HashSet/HashSetTypes.fs.
  • Against main (the unreleased HashSet module that this pull request replaces; none of this is in 1.0.0):
    • length is count, keyComparer is comparer, and where is filter. pick, tryPick and vTryPick are gone (HashSet.toSeq set |> Seq.pick chooser, or Seq.tryPick and ValueOption.ofOption), and so is the public check.
    • ofComparer, ofSeqWithComparer and builderWithComparer are the members empty, ofSeq and builder of HashSet.forComparer comparer. builderWith is gone without a replacement: its argument bound to the comparer overload of the BCL member, and hash sets have no capacity.
    • difference takes its arguments in the FSharp.Core order: main has difference values set, which is removeRange values set now, and union set values is addRange values set. union, intersect, isSubset, isProperSubset, isSuperset and isProperSuperset take a hash set where they took a sequence.
    • filter keeps the comparer of its input, where it dropped it. unionMany of an empty sequence returns an empty hash set, where it threw. isProperSuperset has set semantics (see below). The compiled name of the module is HashSet, where it was ImmutableHashSetModule.

Tests: 4751 per target framework, 1991 of them new.

  • The tests compare arrays with CollectionAssert.AreEqual. They take their cases as DataRow and DynamicData rows, so every case runs and reports as a test of its own.
  • The shared set contracts in Contracts/ (SetOps.fs, SetContract, SetTransformContract, SetMutationContract and SetCreationContract) compare every function with FSharp.Core Set on the same elements. Sets are compared as sorted arrays.
  • SetInputs is a static class of arrays with the row sources, and SetParamNames is a root module.
  • The relations also run with a first set that compares by the remainder in the division by 5, over every pair of inputs. Without the fix of isProperSuperset, one of these pairs fails.
  • The creation facet also runs the members of the struct that forComparer creates, with the same comparer by remainder. SortedSet and FrozenSet will run the same tests with their own comparers.
  • The mutation facet runs addRange and removeRange with 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 a null sequence is rejected. SortedSet derives these tests.
  • The creation facet runs ofReadOnlySpan and ofSpan on a copy of every input that the test changes after the call, for the module and for the struct.
  • The tests of HashSet pin what the contracts do not: the first of the equal elements of a span stays, addRange of an empty hash set returns the empty input (the exception of Union), an equal but distinct comparer instance rebuilds in ofSeq of the struct, build and update with the comparer of the struct and the in-place operations of the builder, and the null hash set of addRange, removeRange and update.
  • The tests of ISet check that addRange and removeRange keep the implementation of a sorted set and the comparer of a hash set, and addRange of an empty hash set returns the empty input.
  • Every new behavior was broken once in the code (25 mutations: the checks of null, the count check, the element that stays, the first or the last of equal elements, the comparer of the result, the order of build, the span creators and the instance test of ofSeq), and a test failed each time.
  • Every seq<'T> input is tested with null and with a default flat list, in one data-row test for each of the functions of HashSet and of the struct (15 rows), the functions of ISet (12 rows) and yield! and for of hashSet { } (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 of checkNotNullOrDefaultSeq.
  • The recipe for a sequence is pinned for HashSet and ISet, in 30 rows each: the six relations and intersect and symmetricDifference, each with the default comparer, a comparer that ignores case and HashIdentity.Reference as the comparer of the first set, over the strings "a" (two instances), "A" and "b", and overlaps and isSuperset as Seq.exists and Seq.forall with contains with 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, contains in the default comparer, intersect and symmetricDifference converting the second set with the default comparer, ISet.clear without the comparer, and the strings of the test data), and a test failed each time.
  • Every exception is compared with the exception of Set.
  • HashSet derives all four facets. ISet derives all of them except creation.
  • The per-collection tests cover:
    • comparers, forComparer and the struct;
    • null input, and null comparers with their parameter names;
    • the builder, the computation expression and both namespaces.
  • The test classes carry the categories; test methods carry none.

Types of changes

What types of changes does your code introduce to FSharp.Collections.Immutable?
Put an x in the boxes that apply

  • Bugfix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)

This change breaks code that uses the published 1.0.0 in one way: the ISet<'T> abbreviation of IImmutableSet<'T> is gone, because it shadowed System.Collections.Generic.ISet<'T>. Use IImmutableSet<'T> and the ISet module. 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 unreleased main are listed above.

Checklist

Put an x in 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.

  • Build and tests pass locally
  • I have added tests that prove my fix is effective or that my feature works (if appropriate)
  • I have added necessary documentation (if appropriate)

Further comments

These decisions need a review:

  1. null input: Set.ofSeq and Set.ofList dereference a null input and throw NullReferenceException. HashSet.ofSeq and HashSet.ofList throw ArgumentNullException with the name of the parameter, as D6 requires for null input.
  2. Default comparer after a type change: map, choose and partitionWith always use the default comparer of the result type (see Comparers above).
  3. Generic builder: HashSetCE<'T> is generic, because its comparer has the type of the elements. Thus hashSet is a type function, [<GeneralizableValue>] let hashSet<'T> = HashSetCE<'T> (). Every hashSet { } expression infers its own element type.
  4. addRange compares the counts: ImmutableHashSet.Union returns the input itself when it adds nothing, except for an empty input that receives a hash set. addRange of HashSet and ISet returns the input when the count is unchanged, so the documented promise holds in that case too. The check costs two reads of Count. removeRange needs none: Except of ImmutableHashSet and ImmutableSortedSet returns the input.
  5. ISet.addRange and 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 of ImmutableSortedSet.Union), and the remarks say so.

🤖 Generated with Claude Code

Copilot AI balanced review requested due to automatic review settings October 18, 2026 11:15

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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
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 HashSet and ISet APIs (+ Option/Tuple “shadowing” namespaces) and a hashSet computation expression.
  • Adds shared set contract test infrastructure and per-module tests (comparers, null handling, builders, CE).
  • Removes legacy HashSet/ISet abbreviations from Sets.fs and 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

Comment thread src/FSharp.Collections.Immutable/HashSet/HashSet.fs Outdated
Comment thread src/FSharp.Collections.Immutable/HashSet/HashSet.fs Outdated
Comment thread src/FSharp.Collections.Immutable/HashSet/HashSet.fs Outdated
Comment thread src/FSharp.Collections.Immutable/HashSet/HashSet.fs Outdated
Comment thread src/FSharp.Collections.Immutable/HashSet/HashSetCE.fs
Comment thread src/FSharp.Collections.Immutable/HashSet/ISet.fsi Outdated
Comment thread src/FSharp.Collections.Immutable/HashSet/ISet.fs Outdated
Comment thread src/FSharp.Collections.Immutable/HashSet/ISet.fs Outdated

Copilot AI left a comment

Copy link
Copy Markdown

xperiandri added a commit that referenced this pull request Oct 18, 2026
…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>
xperiandri added a commit that referenced this pull request Oct 18, 2026
…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>
@xperiandri
xperiandri requested a balanced review from Copilot October 18, 2026 18:50

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Needs a closer look

The broad breaking API rewrite and comparer-sensitive behavior warrant human review with confirmed test results on both target frameworks.

No open issues 🎉

Review effort: Balanced

xperiandri added a commit that referenced this pull request Oct 18, 2026
…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>
xperiandri added a commit that referenced this pull request Oct 18, 2026
…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>
@xperiandri
xperiandri requested a balanced review from Copilot October 18, 2026 20:13

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Missing input validation causes inconsistent exceptions for null and default flat-list sequences.

4 open issues

Review effort: Balanced

Comment thread src/FSharp.Collections.Immutable/HashSet/HashSet.CustomComparer.fs Outdated
Comment thread src/FSharp.Collections.Immutable/HashSet/HashSetCE.fs Outdated
Comment thread src/FSharp.Collections.Immutable/HashSet/HashSetCE.fs
Comment thread src/FSharp.Collections.Immutable/Helper.fs Outdated
xperiandri added a commit that referenced this pull request Oct 18, 2026
…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>
xperiandri added a commit that referenced this pull request Oct 18, 2026
…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>
@xperiandri
xperiandri requested a balanced review from Copilot October 18, 2026 22:14

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Needs a closer look

The breaking API rewrite needs human validation of comparer-sensitive behavior and resolution of the sequence-input validation gap.

5 open issues

Review effort: Balanced

Comment thread src/FSharp.Collections.Immutable/HashSet/HashSet.fs Outdated
xperiandri added a commit that referenced this pull request Oct 19, 2026
…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>
@xperiandri
xperiandri force-pushed the build/assembly-info-props branch from 758443d to 4562f5a Compare October 19, 2026 14:46
xperiandri added a commit that referenced this pull request Oct 19, 2026
…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>
xperiandri added a commit that referenced this pull request Oct 19, 2026
…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>
xperiandri added a commit that referenced this pull request Oct 19, 2026
…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>
xperiandri added a commit that referenced this pull request Oct 19, 2026
…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>
xperiandri added a commit that referenced this pull request Oct 19, 2026
…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>
xperiandri added a commit that referenced this pull request Oct 19, 2026
…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>
Base automatically changed from build/assembly-info-props to main October 19, 2026 15:14
xperiandri added a commit that referenced this pull request Oct 19, 2026
…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>
xperiandri added a commit that referenced this pull request Oct 19, 2026
…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>
xperiandri added a commit that referenced this pull request Oct 19, 2026
…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>
xperiandri added a commit that referenced this pull request Oct 19, 2026
…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>
xperiandri added a commit that referenced this pull request Oct 19, 2026
…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>
xperiandri added a commit that referenced this pull request Oct 19, 2026
…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>
xperiandri added a commit that referenced this pull request Oct 19, 2026
…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>
xperiandri added a commit that referenced this pull request Oct 19, 2026
…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>
xperiandri added a commit that referenced this pull request Oct 19, 2026
…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>
xperiandri and others added 3 commits October 20, 2026 18:13
…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>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants