Skip to content

feat: SortedSet module with the vocabulary of the FSharp.Core Set module - #35

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

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

Conversation

@xperiandri

@xperiandri xperiandri commented Oct 18, 2026 •

Copy link
Copy Markdown
Collaborator

Proposed Changes

Rewrites SortedSet (ImmutableSortedSet<'T>) after ADR 0001 (#23). This pull request is stacked on #29 (feat/hash-set).

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 SortedSet module, which this pull request replaces. The changelog compares with 1.0.0, and the bullet "Against main" below compares with main.

  • Library files: everything is in SortedSet/, in this compile order:

    • SortedSetTypes.fs: the SortedSet<'T> abbreviation of 1.0.0, unchanged, with its documentation. It needs no signature file (D2).
    • SortedSet.CustomComparer.fsi and .fs: the struct that forComparer creates (D12). In Visual Studio they nest under SortedSet.fsi and SortedSet.fs through <DependentUpon>.
    • SortedSet.fsi and SortedSet.fs: 54 functions, in the canonical category order. They are the SortedSet column of ADR Appendix C: the 33 functions of the FSharp.Core Set module, with its parameter names and its argument order, and 21 extras. For example, difference set1 set2 is set1 minus set2.
    • SortedSetOption.fsi/.fs and SortedSetTuple.fsi/.fs: the Option and Tuple namespaces.
    • SortedSetCE.fsi and SortedSetCE.fs, plus the sortedSet instance in ComputationExpressions.fsi and .fs.
    • Each module is the root module of its file (D2). The files that open System.Collections.Generic open FSharp.Collections.Immutable after it. Otherwise SortedSet would name the mutable set of the BCL instead of the abbreviation.
    • The extras wrap members of ImmutableSortedSet: addRange, removeRange, the builder functions (build, builder, ofBuilder, toBuilder and update), comparer, item, tryItem, indexOf, tryGetValue, overlaps, setEquals, symmetricDifference, rev and clear. choose and forComparer have no BCL counterpart, and ofReadOnlySpan and ofSpan replace the BCL Create with a span (below).
    • minElement and maxElement throw what Set throws for an empty set. They read the indexer, because Min and Max return the default value for an empty set. item throws what the indexer throws, because Set has no item.
    • foldBack and partitionWith visit the elements in descending order, as their Set counterparts do. foldBack walks Reverse () without a copy, and rev returns that descending sequence.
    • add, remove, clear, filter, partition and the set algebra keep the comparer of the input, or of the first set.
    • Helper/SortedSets.fs holds SortedSets, with the creation through a builder and the transformations that change the element type, and Helper/ErrorStrings.fs gets the message of Set.minElement.
    • Sets.fs is removed. After feat: HashSet and ISet modules with the vocabulary of the FSharp.Core Set module #29 it held only the unreleased SortedSet module of main and the SortedSetBuilder<'T> abbreviation, besides the SortedSet<'T> abbreviation that moved to SortedSetTypes.fs.
  • addRange and removeRange:

    • addRange elements set and removeRange elements set take the place of the Union and Except members that take a sequence. Both keep the comparer of the set, return the input itself when nothing changes, and throw ArgumentNullException for a null set or sequence, with the parameter names set and elements.
    • removeRange wraps Except.
    • addRange does not wrap Union. It adds the elements one by one to the builder of the set (toBuilder, Add, ofBuilder), so the element of the set stays when the comparer finds two elements equal, whatever the size of the sequence, and of several equal new elements the first is added (D14). Union refills the tree from an unstable sort once the sequence has at least 6 times as many elements as the set (measured on net10.0), and then keeps an arbitrary one: 4 of 25 elements of the set stayed when 250 elements were added.
    • The price is time for a sequence that is much larger than the set. Measured with dotnet fsi --optimize+, best of 9 runs, on a set of 1 000 elements, in two sessions on a busy machine: the function took 1.8 to 1.9 times as long as Union for 10 000 added elements, 4.3 to 7.5 times for 100 000, and 6.7 to 11 times for 1 000 000, and it allocated 0.82 to 0.92 times as much. For a sequence with fewer than 6 times as many elements as the set, Union adds one by one as well: the allocation was the same within 24 B and the time within 15 % (1 000 to 100 000 added elements, set of 100 000). The remarks of addRange say that the BCL member is faster for a large sequence, and give no number.
  • ofReadOnlySpan and ofSpan:

    • The module functions require 'T : comparison, as ofSeq does. The members of the struct need no constraint.
    • They add the elements of the span to a builder and keep the first of the elements that compare equal, as Set.ofArray does. ImmutableSortedSet.Create with a span sorts unstably, as CreateRange does: it kept the first spelling of 32 of 100 keys in a measurement, the builder loop kept all 100.
    • ofSpan converts its Span to a ReadOnlySpan. An empty span gives the empty sorted set.
  • build and update:

    • build action fills a builder that compares with F# generic comparison, and returns its contents. It requires 'T : comparison. The member of the struct fills a builder that orders with the comparer and needs no constraint.
    • update action set applies the action to a builder of the set and returns its contents, with the comparer of the set. The in-place operations of the builder that have no function, such as IntersectWith and SymmetricExceptWith, are one expression with it.
  • CustomComparerSortedSet and forComparer (D12):

    • SortedSet.forComparer comparer creates the struct, for example (SortedSet.forComparer StringComparer.OrdinalIgnoreCase).ofSeq names. Creating it allocates nothing.
    • Its members build, builder, empty, ofArray, ofList, ofReadOnlySpan, ofSeq, ofSpan and singleton create sorted sets and builders that order with the comparer. Their compiled names are PascalCase.
    • ofSeq also gives an existing sorted set the comparer. It returns the sorted set itself when the set has the same comparer instance, an empty set included, as ImmutableSortedSet.WithComparer does (D14). An equal but distinct instance rebuilds the set, because the BCL tests the instance. The remarks say "the same comparer instance".
    • The struct is [<Struct; NoComparison; NoEquality>] with an explicit field and constructor, because a primary constructor cannot check its argument. forComparer and the constructor throw ArgumentNullException with the parameter name comparer, where the BCL members take a null comparer as Comparer<'T>.Default. The remarks of forComparer point to the default value of the struct for that meaning, and to ofSeq for F# generic comparison.
    • The struct does not require 'T : comparison, because a comparer can order any type. Its default value orders with Comparer<'T>.Default, the comparer that the BCL takes for null (D14).
    • The creation functions of the module are the members of the struct with the default comparer, so each creation has one implementation. They require 'T : comparison, as Set does.
  • Default comparer (D5):

    • The creation functions, builder, build, unionMany of no sets, map, choose, partitionWith and sortedSet { } order with F# generic comparison. They use the shared DefaultComparer<'T>.Instance of feat: FlatList module with the vocabulary of the FSharp.Core Array module #26, one cached instance per type, so the BCL can take its shortcuts for two sets with the same comparer.
    • A sorted set thus enumerates its elements in the order of the F# set. Strings order ordinally ("B" before "a"), and so do the strings inside tuples.
    • map, choose and partitionWith use F# comparison of the result type, also when it is the element type: keeping the comparer would need a type test. Each function documents this.
  • Creation through a builder:

    • ImmutableSortedSet.CreateRange, Create with a span and WithComparer sort the elements with the unstable List.Sort before they drop the duplicates. So they can keep any of the elements that compare equal.
    • The members of the struct add the elements one by one to a builder, which keeps the first of them, as Set.ofSeq and the BCL builders do. This holds for ofArray, ofList, ofReadOnlySpan, ofSeq and ofSpan, and for an existing sorted set with another 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 SortedSet and of the struct, addRange and removeRange (elements), intersectMany and unionMany (sets), and yield! (items) and for (source) of sortedSet { }. A default flat list is a boxed, non-null struct, so a check for null alone let it reach the builder or 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 sorted 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.

  • Edge cases (D14):

    • isProperSuperset answers with set semantics under the comparer of the first set, as the documentation of IsProperSupersetOf says. That member counts every element of the second set, also those that the comparer finds equal, so it could answer false for a proper superset. The function wraps the member for the same comparer and otherwise answers with IsSupersetOf and IsSubsetOf. The other relations of ImmutableSortedSet answer with set semantics on every pair of the contract inputs.
    • intersect keeps the element of the second set where the comparer finds two different elements equal, as ImmutableSortedSet.Intersect does.
    • union keeps the element of the first set, except for two sorted sets with the same comparer: ImmutableSortedSet.Union then adds the smaller set to the larger one, so the element of the larger set stays. When the second set has another comparer and is much larger than the first one (at least 6 times as many elements in the measurements on net10.0), Union refills the tree from an unstable sort and either element can stay: 18 and 12 of 25 shared keys kept the element of the first set in two measurements. The remarks of union and the changelog say so and point to addRange, which keeps the element of the set in every case (Copilot's review of the changelog asked for this exception).
    • partitionWith keeps the same one of two equal values as Set.partitionWith, because it visits the elements in the same order.
  • Option and Tuple namespaces (D3): Option.SortedSet contains choose, tryGetValue and tryItem. Tuple.SortedSet contains partition and partitionWith.

  • Computation expression (D7): sortedSet { } orders with F# generic comparison. SortedSetCE comparer { } orders with the given comparer, and a null comparer throws ArgumentNullException when the builder is created. sortedSet is a type function, as hashSet is.

  • exists and forall loop over the struct enumerator of the sorted set and allocate nothing per call. LINQ Any and All allocated 152 B per call, and Seq.exists and Seq.forall 64 B (measured in Release on net10.0).

  • Documentation: XML documentation on every function and member, in the signature files. The changelog lists SortedSet under [Unreleased], in Added only, because 1.0.0 has no functions for sorted sets and so nothing to change or remove. It says what a user of the BCL type does differently:

    • the BCL members that the module covers differently: WithComparer (including null), Create, CreateRange, ToImmutableSortedSet and CreateBuilder with a comparer, Union and Except as addRange and removeRange, Overlaps and IsSupersetOf as Seq.exists and Seq.forall with contains, Min and Max, Reverse, the indexer, IndexOf, ItemRef, the builder members, and the mutating interface members that throw NotSupportedException;
    • which functions keep the comparer, the null checks, unionMany of nothing, the voption and struct tuple results, the order of F# generic comparison, and the element that stays when the comparer finds two elements equal.

    The collection tables of README.md, docsSrc/index.md and docsSrc/Explanations/Background.md list sortedSet { }.

  • Against main (the unreleased SortedSet 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 (SortedSet.toSeq set |> Seq.pick chooser, or Seq.tryPick and ValueOption.ofOption), and so is the public check. The SortedSetBuilder<'T> abbreviation is gone: use SortedSet<'T>.Builder.
    • ofComparer, ofSeqWithComparer and builderWithComparer are the members empty, ofSeq and builder of SortedSet.forComparer comparer. builderWith is gone without a replacement: its argument bound to the comparer overload of the BCL member, and sorted 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 sorted set where they took a sequence.
    • Sorted sets created without a comparer order with F# generic comparison where main used Comparer<'T>.Default: strings, and tuples that contain strings, order ordinally instead of in the order of the current culture, and SortedSet.forComparer Comparer<'T>.Default restores the order of main. ofSeq, ofArray and ofSeqWithComparer keep the first of the elements that compare equal, where they could keep any of them.
    • filter keeps the comparer of its input, where it dropped it. unionMany of an empty sequence returns an empty sorted set, where it threw. isProperSuperset has set semantics (see the edge cases). The compiled name of the module is SortedSet, where it was ImmutableSortedSetModule.

Tests: 5876 per target framework, 1125 of them new. Collections are arrays, compared with CollectionAssert.AreEqual, and cases are data rows.

  • SortedSet derives all four set contracts of feat: HashSet and ISet modules with the vocabulary of the FSharp.Core Set module #29 (868 tests): reading (433), transformation (159), mutation (211) and creation (65).
    • Its ops read every sorted set through a toArray that checks the ascending order of the set's comparer. For the sets created without a comparer, that is the order of Set, so the shared contracts check the order too.
    • The reading contract runs the relations under the comparer of the first set with SortedSet.forComparer SetInputs.ByRemainder, which orders numbers by their remainders in the division by 5. The creation contract runs the members of the same struct.
    • The mutation contract runs add, addRange, clear, remove and removeRange, among them the pairs whose elements the comparer by remainders finds equal. The creation contract runs ofReadOnlySpan and ofSpan, also as members of the struct.
    • Set.minElement names its set s, which the ops map to set.
  • The per-collection tests (256) cover:
    • the order of strings, negative numbers and tuples through every creation function (spans, build and the builder included), and foldBack, partitionWith and rev in descending order;
    • the first of the elements that compare equal, on 300 pairs with 100 keys, where an unstable sort keeps later elements, for ofArray, ofList, ofReadOnlySpan, ofSeq and ofSpan;
    • addRange that keeps the elements of the sorted set whatever the size of the sequence (1, 2, 10 and 40 spellings of 25 keys; for the last two, Union keeps an arbitrary one), for a larger sorted set with the same comparer, where union keeps the larger one, and for an empty sorted set with another comparer; removeRange with the comparer of the set;
    • build and update: the comparer, the copy, the elements handed to the action, and IntersectWith and SymmetricExceptWith through update;
    • the members of the struct, its default value, an element type without F# comparison, and ofSeq that returns a sorted set only for the same comparer instance;
    • the comparer that each function keeps or resets, and isProperSuperset with elements that the comparer of the first set finds equal;
    • item, tryItem, indexOf, minElement and maxElement, with the exceptions of Set and of the BCL;
    • null inputs of every function, and null comparers with their parameter names;
    • every seq<'T> input with null and with a default flat list, in one data-row test for the functions of SortedSet and of the struct (15 rows) and one for yield! and for of sortedSet { } (8 rows). The rows also give a default flat list of strings as a sequence of objects, which a type test for the element type alone does not recognize; the expression gets it as a sequence of IComparable, because obj has no F# comparison, which the expression requires. Two tests check that a flat list that is not default passes in these shapes;
    • the builder, the computation expression and both namespaces.
  • The creation functions without a comparer are the cases of a union, SortedSetCreation, whose rows print their names and create sorted sets of any element type. The other tables are maps by name whose keys are the rows.
  • ISetTests gets a test that ISet.filter keeps the implementation and the comparer of a sorted set.
  • Each new behaviour was broken once on purpose. For example, IsProperSupersetOf alone fails 2 tests, an ascending partitionWith fails 1, and CreateRange in place of the builder fails the tests of the first equal element. For the functions of this round: addRange through Union fails 3 tests, an inverted result check 29, a reversed visiting order 7, and the missing null checks 1 each; a removeRange that removes nothing fails 25; ofReadOnlySpan of the struct through Create fails 4, an ofSpan that returns the empty set 14, Comparer<'T>.Default in place of F# comparison in ofReadOnlySpan fails 4, in build of the module 6 and in build of the struct 8; an update that starts from an empty builder fails 3 and one that ignores its action 2; an ofSeq of the struct that tests Equals of the comparers in place of the instance fails 1. Dropping the flag that addRange uses to return the input fails none: the builder of the BCL returns the input itself when nothing changed, so the flag only makes the promise of the function independent of that. For the sequence inputs, each of the 9 fixes was reverted once and 1 to 3 tests failed each time; reverting yield! for a sequence and for of the expression to the check for null alone failed 2 tests each, and switching off the covariant case of checkNotNullOrDefaultSeq failed 5.
  • 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 no code that uses the published 1.0.0: SortedSet<'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. Creation through a builder: the creation functions keep the first of the elements that compare equal, as Set and the BCL builders do. For that they give up the bottom-up build of CreateRange and of Create with a span, which sort unstably.
  2. SortedSetCE<'T> requires 'T : comparison, also when it is created with a comparer. Its constructor without a comparer needs the constraint, and a class constraint holds for every constructor. Sorted sets of a type without F# comparison come from the struct.
  3. addRange adds one by one instead of wrapping Union, so that the element of the set stays in every case, as in HashSet. For a sequence with at least 6 times as many elements as the set it is 1.8 to 11 times slower than Union, which builds the tree from a sorted array (numbers above). The alternative is to wrap Union and to document that, for such a sequence, an element of the sequence can replace the equal element of the set.

🤖 Generated with Claude Code

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

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

Mixed-comparer proper-superset checks and partitionWith representative selection can produce incorrect results.

2 open issues
What changed in this PR

Replaces the legacy SortedSet bindings with the per-collection API defined by ADR 0001, following the HashSet rewrite.

Changes:

  • Adds Set-style operations, custom comparer factories, and F# default comparison.
  • Adds Option/Tuple variants and sortedSet computation expressions.
  • Expands contract tests, regression coverage, and migration documentation.
File Description
tests/​FSharp.Collections.Immutable.Tests/​SortedSet/​SortedSetTupleTests.fs Tests reference-tuple variants.
tests/​FSharp.Collections.Immutable.Tests/​SortedSet/​SortedSetTests.fs Tests ordering, comparers, and edge cases.
tests/​FSharp.Collections.Immutable.Tests/​SortedSet/​SortedSetOptionTests.fs Tests option variants.
tests/​FSharp.Collections.Immutable.Tests/​SortedSet/​SortedSetOps.fs Connects SortedSet to shared contracts.
tests/​FSharp.Collections.Immutable.Tests/​SortedSet/​SortedSetContractTests.fs Instantiates set contracts.
tests/​FSharp.Collections.Immutable.Tests/​SortedSet/​SortedSetComputationExpressionTests.fs Tests computation expressions.
tests/​FSharp.Collections.Immutable.Tests/​SortedSet/​SortedSetBuilderTests.fs Tests builder behavior.
tests/​FSharp.Collections.Immutable.Tests/​HashSet/​ISetTests.fs Checks sorted-set preservation through ISet.
tests/​FSharp.Collections.Immutable.Tests/​FSharp.Collections.Immutable.Tests.fsproj Registers new tests.
src/​FSharp.Collections.Immutable/​SortedSet/​SortedSetTypes.fs Defines the collection abbreviation.
src/​FSharp.Collections.Immutable/​SortedSet/​SortedSetTuple.fsi Declares tuple variants.
src/​FSharp.Collections.Immutable/​SortedSet/​SortedSetTuple.fs Implements tuple variants.
src/​FSharp.Collections.Immutable/​SortedSet/​SortedSetOption.fsi Declares option variants.
src/​FSharp.Collections.Immutable/​SortedSet/​SortedSetOption.fs Implements option variants.
src/​FSharp.Collections.Immutable/​SortedSet/​SortedSetCE.fs Adds the computation-expression builder.
src/​FSharp.Collections.Immutable/​SortedSet/​SortedSet.fsi Documents the public API.
src/​FSharp.Collections.Immutable/​SortedSet/​SortedSet.fs Implements Set-style operations.
src/​FSharp.Collections.Immutable/​SortedSet/​CustomComparerSortedSet.fsi Declares comparer-based creation.
src/​FSharp.Collections.Immutable/​SortedSet/​CustomComparerSortedSet.fs Implements comparer-based creation.
src/​FSharp.Collections.Immutable/​Sets.fs Removes legacy bindings.
src/​FSharp.Collections.Immutable/​Helper.fs Adds shared sorted-set algorithms.
src/​FSharp.Collections.Immutable/​FSharp.Collections.Immutable.fsproj Registers replacement library files.
src/​FSharp.Collections.Immutable/​ComputationExpressions.fs Exposes sortedSet.
README.md Lists computation-expression support.
docsSrc/​index.md Updates the collection table.
docsSrc/​Explanations/​Background.md Updates the collection table.
CHANGELOG.md Records additions and breaking changes.

Review effort: Balanced

Comment thread src/FSharp.Collections.Immutable/Helper.fs Outdated
Comment thread src/FSharp.Collections.Immutable/SortedSet/SortedSet.fs Outdated

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 replacement and comparer and construction tradeoffs warrant final human validation.

No open issues 🎉

2 resolved since last review

Review effort: Balanced

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, comparer semantics, and creation-performance tradeoff warrant final maintainer review.

No open issues 🎉

Review effort: Balanced

@xperiandri
xperiandri force-pushed the feat/hash-set branch 2 times, most recently from 3dc6fb8 to 9a6be57 Compare October 18, 2026 22:38
@xperiandri
xperiandri requested a balanced review from Copilot October 18, 2026 22:40

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 public API and deliberate comparer and bulk-insertion performance tradeoffs warrant human sign-off.

1 open issue

Review effort: Balanced

Comment thread CHANGELOG.md Outdated
xperiandri added a commit that referenced this pull request Oct 19, 2026
- The sentence about the null checks of `SortedSet` also names the default flat list that is
  given as a sequence, and says that `yield!` and `for` of `sortedSet { }` do the same.
- The sentence about the element that stays names the exception of `union` that the remarks of
  `union` in `SortedSet.fsi` already document: when the second sorted set has another comparer
  and is much larger, the BCL refills the tree from an unstable sort and either element can
  stay, so only `addRange` keeps the element of the sorted set in every case (review of #35).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@xperiandri
xperiandri requested a balanced review from Copilot October 19, 2026 11:01

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 public API rewrite and explicit comparer, element-retention, and performance trade-offs warrant final human review.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

xperiandri added a commit that referenced this pull request Oct 19, 2026
- The sentence about the null checks of `SortedSet` also names the default flat list that is
  given as a sequence, and says that `yield!` and `for` of `sortedSet { }` do the same.
- The sentence about the element that stays names the exception of `union` that the remarks of
  `union` in `SortedSet.fsi` already document: when the second sorted set has another comparer
  and is much larger, the BCL refills the tree from an unstable sort and either element can
  stay, so only `addRange` keeps the element of the sorted set in every case (review of #35).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
xperiandri added a commit that referenced this pull request Oct 19, 2026
- The sentence about the null checks of `SortedSet` also names the default flat list that is
  given as a sequence, and says that `yield!` and `for` of `sortedSet { }` do the same.
- The sentence about the element that stays names the exception of `union` that the remarks of
  `union` in `SortedSet.fsi` already document: when the second sorted set has another comparer
  and is much larger, the BCL refills the tree from an unstable sort and either element can
  stay, so only `addRange` keeps the element of the sorted set in every case (review of #35).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
xperiandri added a commit that referenced this pull request Oct 19, 2026
- The sentence about the null checks of `SortedSet` also names the default flat list that is
  given as a sequence, and says that `yield!` and `for` of `sortedSet { }` do the same.
- The sentence about the element that stays names the exception of `union` that the remarks of
  `union` in `SortedSet.fsi` already document: when the second sorted set has another comparer
  and is much larger, the BCL refills the tree from an unstable sort and either element can
  stay, so only `addRange` keeps the element of the sorted set in every case (review of #35).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
xperiandri added a commit that referenced this pull request Oct 19, 2026
- The sentence about the null checks of `SortedSet` also names the default flat list that is
  given as a sequence, and says that `yield!` and `for` of `sortedSet { }` do the same.
- The sentence about the element that stays names the exception of `union` that the remarks of
  `union` in `SortedSet.fsi` already document: when the second sorted set has another comparer
  and is much larger, the BCL refills the tree from an unstable sort and either element can
  stay, so only `addRange` keeps the element of the sorted set in every case (review of #35).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
xperiandri added a commit that referenced this pull request Oct 19, 2026
- The sentence about the null checks of `SortedSet` also names the default flat list that is
  given as a sequence, and says that `yield!` and `for` of `sortedSet { }` do the same.
- The sentence about the element that stays names the exception of `union` that the remarks of
  `union` in `SortedSet.fsi` already document: when the second sorted set has another comparer
  and is much larger, the BCL refills the tree from an unstable sort and either element can
  stay, so only `addRange` keeps the element of the sorted set in every case (review of #35).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
xperiandri added a commit that referenced this pull request Oct 19, 2026
- The sentence about the null checks of `SortedSet` also names the default flat list that is
  given as a sequence, and says that `yield!` and `for` of `sortedSet { }` do the same.
- The sentence about the element that stays names the exception of `union` that the remarks of
  `union` in `SortedSet.fsi` already document: when the second sorted set has another comparer
  and is much larger, the BCL refills the tree from an unstable sort and either element can
  stay, so only `addRange` keeps the element of the sorted set in every case (review of #35).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
xperiandri added a commit that referenced this pull request Oct 19, 2026
- The sentence about the null checks of `SortedSet` also names the default flat list that is
  given as a sequence, and says that `yield!` and `for` of `sortedSet { }` do the same.
- The sentence about the element that stays names the exception of `union` that the remarks of
  `union` in `SortedSet.fsi` already document: when the second sorted set has another comparer
  and is much larger, the BCL refills the tree from an unstable sort and either element can
  stay, so only `addRange` keeps the element of the sorted set in every case (review of #35).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
xperiandri and others added 3 commits October 20, 2026 18:13
… module

Rewrites `SortedSet` after ADR 0001 in `SortedSet/SortedSet.fsi` and
`SortedSet/SortedSet.fs`: 51 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.

- Wherever no comparer is given, the sorted sets compare with F#
  generic comparison, the comparer of `Set`, instead of
  `Comparer<'T>.Default`: creation, the builders, `sortedSet { }` and
  the results of `map`, `choose` and `partitionWith`. A sorted set
  enumerates its elements in the order of the F# set of the same
  elements, and `foldBack` visits them in descending order.
- `SortedSets.DefaultComparer` in `Helper.fs` resolves that comparer
  once per type. `FastGenericComparer` creates a new comparer at each
  call with a concrete type that is not primitive, and an optimized
  build makes such calls when it inlines a small function of the
  module into its caller. The sorted sets of one type share one
  comparer, so the shortcuts of `ImmutableSortedSet` for two sets with
  the same comparer apply.
- The creation functions add the elements to a builder, which keeps
  the first of the elements that compare equal as `Set.ofSeq` does;
  `ImmutableSortedSet.CreateRange` sorts unstably and can keep any.
- `minElement` and `maxElement` throw for an empty sorted set as `Set`
  does. `item`, `tryItem`, `indexOf` and `rev` wrap the indexer,
  `IndexOf` and `Reverse` and throw what they throw.
- `filter`, `partition`, `add`, `remove`, `clear` and the set algebra
  keep the comparer of their input, the set algebra that of the first
  set; `intersect` keeps the elements of the second set, as the BCL
  does.
- `tryGetValue`, `tryItem` and `choose` use `voption` and `partition`
  and `partitionWith` struct tuples; the `Option` and `Tuple`
  namespaces repeat them with options and reference tuples.
- `sortedSet { }` collects its elements in a builder; `SortedSetCE`
  created with a comparer builds sorted sets that order with it.
- `Helper.fs` gets `ErrorStrings.SetContainsNoElements` and the
  `SortedSets` module; `intersectMany` and `unionMany` reuse
  `Sets.reduce`.
- The legacy `Sets.fs`, with the old `SortedSet` module and the
  `SortedSetBuilder` abbreviation, is removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- `SortedSet` derives a test class from each of the four set facets.
  Its ops records read every sorted set through a `toList` that checks
  the order of `Set`, so the contracts, which compare the elements of a
  hash set as a sorted list, check the order of a sorted set too
  without a change. `Set.minElement` names its set `s`, which the ops
  map to `set`.
- Its own tests cover the order of strings, negative numbers and
  tuples through every creation function, `foldBack` and `rev` in
  descending order, the comparers that the functions keep or set, one
  comparer per element type, the first of the equal elements that the
  creation functions keep, the sorted-only functions `item`, `tryItem`,
  `indexOf`, `minElement` and `maxElement` with the exceptions of `Set`
  and of the BCL, shared storage, null inputs, the builder functions,
  `sortedSet { }` and the `Option` and `Tuple` namespaces.
- `ISet.filter` keeps the implementation and the comparer of a sorted
  set.

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