Repository navigation
feat: SortedSet module with the vocabulary of the FSharp.Core Set module - #35
Open
xperiandri wants to merge 3 commits into
Open
xperiandri wants to merge 3 commits into
xperiandri wants to merge 3 commits into
Conversation
There was a problem hiding this comment.
🟡 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
sortedSetcomputation 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
xperiandri
force-pushed
the
feat/hash-set
branch
from
October 18, 2026 18:49
9722553 to
8f55c4b
Compare
xperiandri
force-pushed
the
feat/sorted-set
branch
from
October 18, 2026 18:59
18f2765 to
151f6a3
Compare
xperiandri
force-pushed
the
feat/hash-set
branch
from
October 18, 2026 20:13
8f55c4b to
93c99b5
Compare
xperiandri
force-pushed
the
feat/sorted-set
branch
from
October 18, 2026 20:18
151f6a3 to
e4e946b
Compare
xperiandri
force-pushed
the
feat/hash-set
branch
2 times, most recently
from
October 18, 2026 22:38
3dc6fb8 to
9a6be57
Compare
xperiandri
force-pushed
the
feat/sorted-set
branch
from
October 18, 2026 22:39
e4e946b to
8bcada2
Compare
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
force-pushed
the
feat/sorted-set
branch
from
October 19, 2026 11:00
8bcada2 to
699b9fd
Compare
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
force-pushed
the
feat/sorted-set
branch
from
October 19, 2026 12:31
699b9fd to
af2dc7a
Compare
xperiandri
force-pushed
the
feat/hash-set
branch
from
October 19, 2026 14:42
58d1f96 to
c5af422
Compare
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
force-pushed
the
feat/sorted-set
branch
from
October 19, 2026 14:45
af2dc7a to
21f6508
Compare
xperiandri
force-pushed
the
feat/hash-set
branch
from
October 19, 2026 14:47
c5af422 to
cbf0a56
Compare
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
force-pushed
the
feat/sorted-set
branch
from
October 19, 2026 14:47
21f6508 to
1641c82
Compare
xperiandri
force-pushed
the
feat/hash-set
branch
from
October 19, 2026 14:48
cbf0a56 to
008d0d0
Compare
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
force-pushed
the
feat/sorted-set
branch
from
October 19, 2026 14:48
1641c82 to
e881820
Compare
xperiandri
force-pushed
the
feat/hash-set
branch
from
October 19, 2026 15:55
008d0d0 to
d31c12e
Compare
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
force-pushed
the
feat/sorted-set
branch
from
October 19, 2026 15:55
e881820 to
a8c5fb5
Compare
xperiandri
force-pushed
the
feat/hash-set
branch
from
October 19, 2026 17:31
d31c12e to
8ed4dc7
Compare
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
force-pushed
the
feat/sorted-set
branch
from
October 19, 2026 17:31
a8c5fb5 to
81fa531
Compare
xperiandri
force-pushed
the
feat/hash-set
branch
from
October 19, 2026 17:40
8ed4dc7 to
7d3f26f
Compare
xperiandri
force-pushed
the
feat/sorted-set
branch
from
October 19, 2026 17:41
81fa531 to
fa51801
Compare
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>
… 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>
xperiandri
force-pushed
the
feat/hash-set
branch
from
October 20, 2026 16:28
7d3f26f to
0939b28
Compare
xperiandri
force-pushed
the
feat/sorted-set
branch
from
October 20, 2026 16:28
fa51801 to
b8fb3da
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


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>andHashSet<'T>for the sets, with no functions.mainhas an unreleasedSortedSetmodule, which this pull request replaces. The changelog compares with 1.0.0, and the bullet "Againstmain" below compares withmain.Library files: everything is in
SortedSet/, in this compile order:SortedSetTypes.fs: theSortedSet<'T>abbreviation of 1.0.0, unchanged, with its documentation. It needs no signature file (D2).SortedSet.CustomComparer.fsiand.fs: the struct thatforComparercreates (D12). In Visual Studio they nest underSortedSet.fsiandSortedSet.fsthrough<DependentUpon>.SortedSet.fsiandSortedSet.fs: 54 functions, in the canonical category order. They are theSortedSetcolumn of ADR Appendix C: the 33 functions of the FSharp.CoreSetmodule, with its parameter names and its argument order, and 21 extras. For example,difference set1 set2is set1 minus set2.SortedSetOption.fsi/.fsandSortedSetTuple.fsi/.fs: theOptionandTuplenamespaces.SortedSetCE.fsiandSortedSetCE.fs, plus thesortedSetinstance inComputationExpressions.fsiand.fs.System.Collections.GenericopenFSharp.Collections.Immutableafter it. OtherwiseSortedSetwould name the mutable set of the BCL instead of the abbreviation.ImmutableSortedSet:addRange,removeRange, the builder functions (build,builder,ofBuilder,toBuilderandupdate),comparer,item,tryItem,indexOf,tryGetValue,overlaps,setEquals,symmetricDifference,revandclear.chooseandforComparerhave no BCL counterpart, andofReadOnlySpanandofSpanreplace the BCLCreatewith a span (below).minElementandmaxElementthrow whatSetthrows for an empty set. They read the indexer, becauseMinandMaxreturn the default value for an empty set.itemthrows what the indexer throws, becauseSethas noitem.foldBackandpartitionWithvisit the elements in descending order, as theirSetcounterparts do.foldBackwalksReverse ()without a copy, andrevreturns that descending sequence.add,remove,clear,filter,partitionand the set algebra keep the comparer of the input, or of the first set.Helper/SortedSets.fsholdsSortedSets, with the creation through a builder and the transformations that change the element type, andHelper/ErrorStrings.fsgets the message ofSet.minElement.Sets.fsis removed. After feat:HashSetandISetmodules with the vocabulary of the FSharp.CoreSetmodule #29 it held only the unreleasedSortedSetmodule ofmainand theSortedSetBuilder<'T>abbreviation, besides theSortedSet<'T>abbreviation that moved toSortedSetTypes.fs.addRangeandremoveRange:addRange elements setandremoveRange elements settake the place of theUnionandExceptmembers that take a sequence. Both keep the comparer of the set, return the input itself when nothing changes, and throwArgumentNullExceptionfor anullset or sequence, with the parameter namessetandelements.removeRangewrapsExcept.addRangedoes not wrapUnion. 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).Unionrefills the tree from an unstable sort once the sequence has at least 6 times as many elements as the set (measured onnet10.0), and then keeps an arbitrary one: 4 of 25 elements of the set stayed when 250 elements were added.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 asUnionfor 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,Unionadds 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 ofaddRangesay that the BCL member is faster for a large sequence, and give no number.ofReadOnlySpanandofSpan:'T : comparison, asofSeqdoes. The members of the struct need no constraint.Set.ofArraydoes.ImmutableSortedSet.Createwith a span sorts unstably, asCreateRangedoes: it kept the first spelling of 32 of 100 keys in a measurement, the builder loop kept all 100.ofSpanconverts itsSpanto aReadOnlySpan. An empty span gives the empty sorted set.buildandupdate:build actionfills 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 setapplies 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 asIntersectWithandSymmetricExceptWith, are one expression with it.CustomComparerSortedSetandforComparer(D12):SortedSet.forComparer comparercreates the struct, for example(SortedSet.forComparer StringComparer.OrdinalIgnoreCase).ofSeq names. Creating it allocates nothing.build,builder,empty,ofArray,ofList,ofReadOnlySpan,ofSeq,ofSpanandsingletoncreate sorted sets and builders that order with the comparer. Their compiled names are PascalCase.ofSeqalso 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, asImmutableSortedSet.WithComparerdoes (D14). An equal but distinct instance rebuilds the set, because the BCL tests the instance. The remarks say "the same comparer instance".[<Struct; NoComparison; NoEquality>]with an explicit field and constructor, because a primary constructor cannot check its argument.forComparerand the constructor throwArgumentNullExceptionwith the parameter namecomparer, where the BCL members take anullcomparer asComparer<'T>.Default. The remarks offorComparerpoint to the default value of the struct for that meaning, and toofSeqfor F# generic comparison.'T : comparison, because a comparer can order any type. Its default value orders withComparer<'T>.Default, the comparer that the BCL takes fornull(D14).'T : comparison, asSetdoes.Default comparer (D5):
builder,build,unionManyof no sets,map,choose,partitionWithandsortedSet { }order with F# generic comparison. They use the sharedDefaultComparer<'T>.Instanceof feat:FlatListmodule with the vocabulary of the FSharp.CoreArraymodule #26, one cached instance per type, so the BCL can take its shortcuts for two sets with the same comparer."B"before"a"), and so do the strings inside tuples.map,chooseandpartitionWithuse 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,Createwith a span andWithComparersort the elements with the unstableList.Sortbefore they drop the duplicates. So they can keep any of the elements that compare equal.Set.ofSeqand the BCL builders do. This holds forofArray,ofList,ofReadOnlySpan,ofSeqandofSpan, and for an existing sorted set with another comparer.Sequence inputs (D6): every
seq<'T>parameter rejectsnulland a default flat list that is given as a sequence with anArgumentNullExceptionthat names the parameter of our function. That isofSeqofSortedSetand of the struct,addRangeandremoveRange(elements),intersectManyandunionMany(sets), andyield!(items) andfor(source) ofsortedSet { }. A default flat list is a boxed, non-null struct, so a check fornullalone let it reach the builder or the BCL call, which failed withInvalidOperationExceptionwhen it enumerated the sequence. The functions callcheckNotNullOrDefaultSeqfirst, so a default flat list of a derived type that arrives through the covariance ofseqis rejected too. Theyield!overloads for an array and for a sorted set, and the expressionfor, check their input before the buffer is returned, so the exception no longer namesother(the parameter ofUnionWith) or comes as aNullReferenceException.Edge cases (D14):
isProperSupersetanswers with set semantics under the comparer of the first set, as the documentation ofIsProperSupersetOfsays. That member counts every element of the second set, also those that the comparer finds equal, so it could answerfalsefor a proper superset. The function wraps the member for the same comparer and otherwise answers withIsSupersetOfandIsSubsetOf. The other relations ofImmutableSortedSetanswer with set semantics on every pair of the contract inputs.intersectkeeps the element of the second set where the comparer finds two different elements equal, asImmutableSortedSet.Intersectdoes.unionkeeps the element of the first set, except for two sorted sets with the same comparer:ImmutableSortedSet.Unionthen 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 onnet10.0),Unionrefills 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 ofunionand the changelog say so and point toaddRange, which keeps the element of the set in every case (Copilot's review of the changelog asked for this exception).partitionWithkeeps the same one of two equal values asSet.partitionWith, because it visits the elements in the same order.Option and Tuple namespaces (D3):
Option.SortedSetcontainschoose,tryGetValueandtryItem.Tuple.SortedSetcontainspartitionandpartitionWith.Computation expression (D7):
sortedSet { }orders with F# generic comparison.SortedSetCE comparer { }orders with the given comparer, and anullcomparer throwsArgumentNullExceptionwhen the builder is created.sortedSetis a type function, ashashSetis.existsandforallloop over the struct enumerator of the sorted set and allocate nothing per call. LINQAnyandAllallocated 152 B per call, andSeq.existsandSeq.forall64 B (measured in Release onnet10.0).Documentation: XML documentation on every function and member, in the signature files. The changelog lists
SortedSetunder[Unreleased], inAddedonly, 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:WithComparer(includingnull),Create,CreateRange,ToImmutableSortedSetandCreateBuilderwith a comparer,UnionandExceptasaddRangeandremoveRange,OverlapsandIsSupersetOfasSeq.existsandSeq.forallwithcontains,MinandMax,Reverse, the indexer,IndexOf,ItemRef, the builder members, and the mutating interface members that throwNotSupportedException;unionManyof nothing, thevoptionand 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.mdanddocsSrc/Explanations/Background.mdlistsortedSet { }.Against
main(the unreleasedSortedSetmodule that this pull request replaces; none of this is in 1.0.0):lengthiscount,keyCompareriscomparer, andwhereisfilter.pick,tryPickandvTryPickare gone (SortedSet.toSeq set |> Seq.pick chooser, orSeq.tryPickandValueOption.ofOption), and so is the publiccheck. TheSortedSetBuilder<'T>abbreviation is gone: useSortedSet<'T>.Builder.ofComparer,ofSeqWithComparerandbuilderWithComparerare the membersempty,ofSeqandbuilderofSortedSet.forComparer comparer.builderWithis gone without a replacement: its argument bound to the comparer overload of the BCL member, and sorted sets have no capacity.differencetakes its arguments in the FSharp.Core order:mainhasdifference values set, which isremoveRange values setnow, andunion set valuesisaddRange values set.union,intersect,isSubset,isProperSubset,isSupersetandisProperSupersettake a sorted set where they took a sequence.mainusedComparer<'T>.Default: strings, and tuples that contain strings, order ordinally instead of in the order of the current culture, andSortedSet.forComparer Comparer<'T>.Defaultrestores the order ofmain.ofSeq,ofArrayandofSeqWithComparerkeep the first of the elements that compare equal, where they could keep any of them.filterkeeps the comparer of its input, where it dropped it.unionManyof an empty sequence returns an empty sorted set, where it threw.isProperSupersethas set semantics (see the edge cases). The compiled name of the module isSortedSet, where it wasImmutableSortedSetModule.Tests: 5876 per target framework, 1125 of them new. Collections are arrays, compared with
CollectionAssert.AreEqual, and cases are data rows.SortedSetderives all four set contracts of feat:HashSetandISetmodules with the vocabulary of the FSharp.CoreSetmodule #29 (868 tests): reading (433), transformation (159), mutation (211) and creation (65).toArraythat checks the ascending order of the set's comparer. For the sets created without a comparer, that is the order ofSet, so the shared contracts check the order too.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.add,addRange,clear,removeandremoveRange, among them the pairs whose elements the comparer by remainders finds equal. The creation contract runsofReadOnlySpanandofSpan, also as members of the struct.Set.minElementnames its sets, which the ops map toset.buildand the builder included), andfoldBack,partitionWithandrevin descending order;ofArray,ofList,ofReadOnlySpan,ofSeqandofSpan;addRangethat 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,Unionkeeps an arbitrary one), for a larger sorted set with the same comparer, whereunionkeeps the larger one, and for an empty sorted set with another comparer;removeRangewith the comparer of the set;buildandupdate: the comparer, the copy, the elements handed to the action, andIntersectWithandSymmetricExceptWiththroughupdate;ofSeqthat returns a sorted set only for the same comparer instance;isProperSupersetwith elements that the comparer of the first set finds equal;item,tryItem,indexOf,minElementandmaxElement, with the exceptions ofSetand of the BCL;nullinputs of every function, andnullcomparers with their parameter names;seq<'T>input withnulland with a default flat list, in one data-row test for the functions ofSortedSetand of the struct (15 rows) and one foryield!andforofsortedSet { }(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 ofIComparable, becauseobjhas no F# comparison, which the expression requires. Two tests check that a flat list that is not default passes in these shapes;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.ISetTestsgets a test thatISet.filterkeeps the implementation and the comparer of a sorted set.IsProperSupersetOfalone fails 2 tests, an ascendingpartitionWithfails 1, andCreateRangein place of the builder fails the tests of the first equal element. For the functions of this round:addRangethroughUnionfails 3 tests, an inverted result check 29, a reversed visiting order 7, and the missing null checks 1 each; aremoveRangethat removes nothing fails 25;ofReadOnlySpanof the struct throughCreatefails 4, anofSpanthat returns the empty set 14,Comparer<'T>.Defaultin place of F# comparison inofReadOnlySpanfails 4, inbuildof the module 6 and inbuildof the struct 8; anupdatethat starts from an empty builder fails 3 and one that ignores its action 2; anofSeqof the struct that testsEqualsof the comparers in place of the instance fails 1. Dropping the flag thataddRangeuses 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; revertingyield!for a sequence andforof the expression to the check fornullalone failed 2 tests each, and switching off the covariant case ofcheckNotNullOrDefaultSeqfailed 5.Types of changes
What types of changes does your code introduce to FSharp.Collections.Immutable?
Put an
xin the boxes that applyThis change breaks no code that uses the published 1.0.0:
SortedSet<'T>is unchanged, and the functions are new. The changes against the unreleasedmainare listed above.Checklist
Put an
xin the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code.Further comments
These decisions need a review:
Setand the BCL builders do. For that they give up the bottom-up build ofCreateRangeand ofCreatewith a span, which sort unstably.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.addRangeadds one by one instead of wrappingUnion, so that the element of the set stays in every case, as inHashSet. For a sequence with at least 6 times as many elements as the set it is 1.8 to 11 times slower thanUnion, which builds the tree from a sorted array (numbers above). The alternative is to wrapUnionand to document that, for such a sequence, an element of the sequence can replace the equal element of the set.🤖 Generated with Claude Code