Repository navigation
feat: SortedMap module with the vocabulary of the FSharp.Core Map module - #37
Open
xperiandri wants to merge 4 commits into
Open
xperiandri wants to merge 4 commits into
xperiandri wants to merge 4 commits into
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Creation and computation-expression paths can retain earlier bindings when values compare equal, violating their last-binding-wins contract.
2 open issues
What changed in this PR
Rewrites SortedMap around the FSharp.Core Map vocabulary, continuing ADR 0001 and reusing the shared map contracts from #30.
Changes:
- Adds comparer-based creation, binding-shape overloads, and Option/Tuple variants.
- Introduces
sortedMap { }and F# generic key ordering. - Expands tests and documents breaking changes.
| File | Description |
|---|---|
| tests/FSharp.Collections.Immutable.Tests/SortedMap/SortedMapTupleTests.fs | Tests reference-tuple overrides. |
| tests/FSharp.Collections.Immutable.Tests/SortedMap/SortedMapTests.fs | Tests ordering, comparers, and operations. |
| tests/FSharp.Collections.Immutable.Tests/SortedMap/SortedMapOptionTests.fs | Tests option overrides. |
| tests/FSharp.Collections.Immutable.Tests/SortedMap/SortedMapOps.fs | Supplies shared-contract adapters. |
| tests/FSharp.Collections.Immutable.Tests/SortedMap/SortedMapExtensionsTests.fs | Tests creation overloads and pipelines. |
| tests/FSharp.Collections.Immutable.Tests/SortedMap/SortedMapContractTests.fs | Instantiates shared map contracts. |
| tests/FSharp.Collections.Immutable.Tests/SortedMap/SortedMapComputationExpressionTests.fs | Tests computation expressions. |
| tests/FSharp.Collections.Immutable.Tests/SortedMap/SortedMapBuilderTests.fs | Tests builder behavior. |
| tests/FSharp.Collections.Immutable.Tests/SortedMap/CustomComparersSortedMapTests.fs | Tests custom-comparer creation. |
| tests/FSharp.Collections.Immutable.Tests/HashMap/IMapTests.fs | Checks sorted-map preservation through IMap. |
| tests/FSharp.Collections.Immutable.Tests/FSharp.Collections.Immutable.Tests.fsproj | Registers new tests. |
| src/FSharp.Collections.Immutable/SortedMap/SortedMapTypes.fs | Declares the collection abbreviation. |
| src/FSharp.Collections.Immutable/SortedMap/SortedMapTuple.fsi | Documents tuple overrides. |
| src/FSharp.Collections.Immutable/SortedMap/SortedMapTuple.fs | Implements tuple overrides. |
| src/FSharp.Collections.Immutable/SortedMap/SortedMapOption.fsi | Documents option overrides. |
| src/FSharp.Collections.Immutable/SortedMap/SortedMapOption.fs | Implements option overrides. |
| src/FSharp.Collections.Immutable/SortedMap/SortedMapExtensions.fsi | Declares creation overloads. |
| src/FSharp.Collections.Immutable/SortedMap/SortedMapExtensions.fs | Implements creation overloads. |
| src/FSharp.Collections.Immutable/SortedMap/SortedMapCE.fs | Implements computation-expression support. |
| src/FSharp.Collections.Immutable/SortedMap/SortedMap.fsi | Defines the public module contract. |
| src/FSharp.Collections.Immutable/SortedMap/SortedMap.fs | Implements map operations. |
| src/FSharp.Collections.Immutable/SortedMap/CustomComparersSortedMap.fsi | Defines comparer-based creation contracts. |
| src/FSharp.Collections.Immutable/SortedMap/CustomComparersSortedMap.fs | Implements comparer-based creation. |
| src/FSharp.Collections.Immutable/Maps.fs | Removes the legacy implementation. |
| src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj | Registers replacement library files. |
| src/FSharp.Collections.Immutable/ComputationExpressions.fs | Exposes sortedMap. |
| README.md | Lists computation-expression support. |
| docsSrc/index.md | Updates the collection table. |
| docsSrc/Explanations/Background.md | Updates the collection comparison. |
| CHANGELOG.md | Records additions and breaking changes. |
Review effort: Balanced
xperiandri
force-pushed
the
feat/hash-map
branch
from
October 18, 2026 18:47
3f22fda to
8d763b8
Compare
xperiandri
force-pushed
the
feat/sorted-map
branch
from
October 18, 2026 19:05
5ddc879 to
4e35919
Compare
xperiandri
force-pushed
the
feat/hash-map
branch
from
October 18, 2026 20:11
8d763b8 to
ed2f285
Compare
xperiandri
force-pushed
the
feat/sorted-map
branch
from
October 18, 2026 20:19
4e35919 to
d84e23f
Compare
xperiandri
force-pushed
the
feat/hash-map
branch
from
October 18, 2026 22:36
d61c19f to
0e1a08a
Compare
xperiandri
force-pushed
the
feat/sorted-map
branch
from
October 18, 2026 22:40
d84e23f to
42c8542
Compare
xperiandri
force-pushed
the
feat/sorted-map
branch
from
October 19, 2026 11:33
42c8542 to
0a45628
Compare
xperiandri
force-pushed
the
feat/hash-map
branch
3 times, most recently
from
October 19, 2026 14:48
53cd7b3 to
ec4d653
Compare
xperiandri
force-pushed
the
feat/sorted-map
branch
from
October 19, 2026 14:49
0a45628 to
d3b692b
Compare
xperiandri
force-pushed
the
feat/hash-map
branch
from
October 19, 2026 15:55
ec4d653 to
40871e3
Compare
xperiandri
force-pushed
the
feat/sorted-map
branch
from
October 19, 2026 15:55
d3b692b to
9599e5e
Compare
xperiandri
force-pushed
the
feat/hash-map
branch
from
October 19, 2026 17:31
40871e3 to
b487c08
Compare
xperiandri
force-pushed
the
feat/sorted-map
branch
from
October 19, 2026 17:31
9599e5e to
0c3a63f
Compare
xperiandri
force-pushed
the
feat/hash-map
branch
from
October 19, 2026 17:40
b487c08 to
02cb1b5
Compare
xperiandri
force-pushed
the
feat/sorted-map
branch
from
October 19, 2026 17:42
0c3a63f to
2c33fff
Compare
… module
Rewrites `SortedMap` after ADR 0001 in `SortedMap/SortedMap.fsi` and
`SortedMap/SortedMap.fs`, each function documented in the signature
file, and deletes the legacy `Maps.fs`.
- The functions with a name of `Map` have its semantics and
exceptions, `minKeyValue` and `maxKeyValue` included: `add` replaces
the value of a key, `filter` and `map` return maps, `find`, `pick`,
`minKeyValue` and `maxKeyValue` throw the `KeyNotFoundException` of
`Map`, and `findKey` the one of `Seq.pick`. The wrappers of
`ImmutableSortedDictionary` members keep the semantics of the BCL, so
`addRange`, `ofKeyValuePairs`, `ofSeqBy` and `ofSeqWithKeyComparer`
reject a repeated key with another value.
- Every function that creates a map without a key comparer orders the
keys with F# generic comparison (`FastGenericComparer`), as `Map`
does, instead of `Comparer<'Key>.Default`: `empty` (created once per
type), the creation functions, `builder` and `sortedMap { }`. These
functions, and `SortedMapCE`, require `'Key : comparison`.
- `singleton` takes the key and the value; the legacy one passed its
argument to `ImmutableSortedDictionary.Create` as the key comparer
and built an empty map.
- Every transformation keeps the key comparer and so the order, the
ones that keep the type of the values keep the value comparer, and
`map` and `choose` give the new values the default comparer.
`foldBack` visits the keys in descending order.
- `minKeyValue` takes the first binding in O(log n); `maxKeyValue`
visits every binding, because the dictionary has no other access to
its largest key.
- `try*` functions return `voption`, tuples are struct tuples, and the
`Option` and `Tuple` namespaces repeat those functions with options
and reference tuples.
- `sortedMap { }` collects `yield struct (key, value)` and
`yield KeyValuePair` in a builder; `SortedMapCE` takes a key comparer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- `SortedMapOps` records the functions of `SortedMap`, which derives
the reading, transformation, mutation and creation facets of the map
contracts.
- `SortedMapTests` compares the order of the creation functions, the
functions that visit the bindings and the transformations that
rebuild the map with the FSharp.Core `Map` over string, negative and
tuple keys, and covers `minKeyValue`, `maxKeyValue`, `singleton`,
the comparers, null inputs, results that share the input and the
semantics of the functions that wrap the BCL.
- The builder functions, `sortedMap { }`, `SortedMapCE` and the
`Option` and `Tuple` namespaces have their own test classes.
- `IMapTests` checks that `IMap.filter` keeps the implementation, the
comparers and the order of a `SortedMap`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The changelog lists the move from `Comparer<'Key>.Default` to F# generic comparison and the repaired `singleton` as breaking changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri
force-pushed
the
feat/hash-map
branch
from
October 20, 2026 16:28
02cb1b5 to
53d466c
Compare
xperiandri
force-pushed
the
feat/sorted-map
branch
from
October 20, 2026 16:28
2c33fff to
7d3263f
Compare
…fault modules by their short names Each of these files opens `FSharp.Collections.Immutable`, and inside `FSharp.Collections.Immutable.Tuple.SortedMap` the name `SortedMap` reaches the default module, also for the functions that both modules define, so the full namespace in front of it added nothing. The compiled code is the same: the opcodes and the called methods of every method of both builds match one to one. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
3 of 6 tasks
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
SortedMap(ImmutableSortedDictionary<'Key, 'Value>) after ADR 0001 (#23). This pull request is stacked on #30 (feat/hash-map). It reuses the map contracts of #30 and theDictionariesalgorithms ofHelper/Dictionaries.fs.The published 1.0.0 has the type abbreviations
SortedMap<'Key, 'Value>andSortedMapBuilder<'Key, 'Value>and aSortedMapmodule whose only function isempty(checked in the metadata of both target frameworks of the package, and with a script that compiles against the DLL).mainhas an unreleasedSortedMapmodule, which this pull request replaces. The changelog compares with 1.0.0, and the bullet "Againstmain" below compares withmain.SortedMap/SortedMapTypes.fs: the abbreviationSortedMap<'Key, 'Value>.SortedMapand itsOptionandTuplemodules are root modules (D2), and a root module cannot declare a type of its namespace.SortedMap/SortedMap.fsiandSortedMap.fs: 48 functions in the canonical category order, and the nested modulesTupleandValueTuple. They are theSortedMapcolumn of ADR Appendix B, with the parameter names of the FSharp.CoreMapmodule.partition,toArray,toList,toSeq,minKeyValueandmaxKeyValuereturn struct tuples (D3).add,addRange,containsBinding,containsValue, the comparer accessors, the builder functions (build,builder,ofBuilder,toBuilderandupdate),ofSeqBy,removeRange,setItem,setItemsandtryGetKey.addRangeandsetItemstakeKeyValuePairs. The nested modules hold them for reference and struct tuples (D13):SortedMap.Tuple.addRange pairs table,SortedMap.ValueTuple.setItems pairs table.forComparersandforKeyComparercreate the comparer struct.chooseandsingletonare extras.SortedMap/SortedMap.CustomComparers.fsiand.fs(D12): the structCustomComparersSortedMap<'Key, 'Value>holds the comparer that orders the keys (IComparer<'Key>) and the equality comparer of the values (IEqualityComparer<'Value>),(keyComparer, valueComparer), or the key comparer andEqualityComparer<'Value>.Default,(keyComparer).KeyComparerandValueComparer. Its members have the names of the creation functions and PascalCase compiled names:empty,singleton,ofArray,ofListandofSeqin the three shapes,ofSeqBy,buildandbuilder.SortedMap.forComparers keyComparer valueComparerandSortedMap.forKeyComparer keyComparercreate it:(SortedMap.forKeyComparer StringComparer.OrdinalIgnoreCase).ofSeq pairs.nullcomparer throwsArgumentNullExceptionwith the name of its parameter, in the constructors and in thefor*functions.ofSeqof a sorted map whose key comparer is the instance of the struct keeps its tree. It returns the map itself, or the map with the value comparer of the struct, in O(1) throughWithComparers. An empty map comes back too. Any other map gets the comparers of the struct binding by binding.WithComparersin three ways, which the remarks of the struct,forComparersandforKeyComparerdescribe, with an example: a key comparer that is another instance rebuilds the map, even an equivalent one (10^6 bindings, one run of a Debug build: 0.8 ms to change only the value comparer through the key comparer of the map, 827 ms to rebuild); keys that the new comparer makes equal merge, the later binding winning (D14), whereWithComparersthrowsArgumentException; andforKeyComparergives the values the default comparer, whereWithComparerskeeps the value comparer, soforComparers keyComparer (valueComparer table)keeps it.null:Comparer<'Key>.DefaultandEqualityComparer<'Value>.Default(D14). The struct cannot require'Key : comparison, because a custom comparer also serves keys without it, so it cannot fall back to F# generic comparison.SortedMap/SortedMap.Extensions.fsiand.fs(D13): the auto-opened moduleSortedMapExtensionsextendsImmutableSortedDictionary<'Key, 'Value>with staticofArray,ofListandofSeq, each overloaded forKeyValuePairs, reference tuples and struct tuples.SortedMapmodule has no function with their names, soSortedMap.ofSeq pairsreaches the overloads, also aspairs |> SortedMap.ofSeq. Lists, arrays,dict, an F#Map, a hash map and an existing sorted map resolve.'Key : comparison, and a member cannot constrain the type parameters of the type that it extends (FS0340). Each member therefore declares type parameters of its own:static member ofSeq<'K, 'V when 'K : not null and 'K : comparison>.ImmutableSortedDictionary. It calls them as static methods ofSortedMapExtensions(OfArray,OfListandOfSeq, one overload per shape) or uses the struct. A reflection over theSortedMapmodule lists none of the three names.DependentUponin the project file, the.fsionSortedMap.fsiand the.fsonSortedMap.fs, so that Visual Studio shows them under the module. Their names start with the name of the module; the compile order and the names of the struct and the module stay.DefaultComparer<'Key>.InstancefromHelper/Comparers.fs. That is F# generic comparison, asMapuses, in one cached instance per type.sortedMap { }use it.emptyis created once per type.Map. They do not sort by the rules of the current culture, asComparer<string>.Defaultsorts them.WithComparerswithout a copy.FastGenericComparerat a concrete tuple type creates a new comparer on each call.ImmutableSortedDictionary.SetItem,AddRange,ToImmutableSortedDictionaryand the indexer of the builder do. This holds foradd,change,setItem,setItems,addRange, theirTuple,ValueTupleandOptionvariants, the creation functions and members, andsortedMap { }. The functions that set one binding then return the input itself. The remarks of every affected function and member say so.ImmutableSortedDictionarykeep the earlier binding too, whatever the size of the input:SetItems,AddRangeandToImmutableSortedDictionaryon an empty or a filled map (checked up to 200 pairs, in and out of order). The module wraps them as they are.addis the BCLAdd:SortedMap.addcallsImmutableSortedDictionary.Add, the decision of the maintainers. A key that is bound to a value that the value comparer finds different throwsArgumentException, the same binding returns the input itself, and a key without a binding gets it.Map.addreplaces the value, and so doessetItem. The remarks of the module,add,setItemandchangesay so, andsetItemis no longer called a synonym ofadd.containsBinding key value table: wrapsContains (KeyValuePair), which compares the value with the value comparer of the map.containsKeyignores the value,containsValueignores the key and costs O(n), andtryFindfollowed by=compares with F# equality, which for a comparer that ignores the case finds"Hello"and"HELLO"different. Anullkey throwsArgumentNullException, as the member does.IMap.containsBindingof feat:HashMapandIMapmodules with the vocabulary of the FSharp.CoreMapmodule #30 covers the interface.build actionandupdate action table:buildfills a builder with the default comparers and returns the sorted map;updatechanges a builder over the bindings and comparers of a map and returns the input itself when nothing changed. The struct has the memberbuild, with its comparers. There is noofReadOnlySpan, because the BCL has no span creation for sorted dictionaries.TryGetValueistryFind,GetValueOrDefaultistryFindfollowed byValueOption.defaultValue, and adding a binding only when the key has none ischangewith a function that keeps an existing value.ImmutableInterlockedhas no member for a sorted dictionary, but itsUpdateswaps a field with the result of a function of the map. The rejecting form of the creation functions isSortedMap.empty |> SortedMap.addRange pairs(it throws asCreateRangedoes), and an element selector isaddRangeof the mapped pairs (ToImmutableSortedDictionary). Maps have no builder module: the members of the BCL builder are called on it.ofBuilder,buildandupdatesay thatToImmutablefreezes the nodes that changed since the builder last returned a sorted map and shares them instead of copying the entries. Measured with 10^6 bindings (one run of a Debug build): 26 ms for a builder that was just filled, microseconds for the same builder again, and 0.1 ms for a builder over a map that nothing changed.ofArray,ofListandofSeqkeep the last binding of a key in every shape, asMap.ofSeqdoes, both as extension members and as members of the struct, unless the value comparer finds the two values equal: then the earlier binding stays (D14). This also holds for keys that only a custom comparer finds equal, and for an existing map that gets another comparer.ImmutableSortedDictionary.CreateRangewould reject them.ofSeqByandaddRangekeep the behavior ofImmutableSortedDictionary. A repeated key with an equal value is accepted and keeps the first binding; a repeated key with another value throwsArgumentException.setItemsreplaces the value.seqargument throwsArgumentNullExceptionthat names the parameter fornulland for a default flat list given as a sequence, which would otherwise fail withInvalidOperationExceptioninside the BCL call. One call ofcheckNotNullOrDefaultSeqguards each of these before anything else:addRange,removeRangeandsetItemsofSortedMap, theTupleandValueTuplemodules, and theofSeqandofSeqBymembers of the struct (SortedMap.ofSeqByand the extension overloads ofofSeqcall them). Arrays keep their null check.sortedMap { }checked none of its inputs: anullsequence, array or sorted map failed withNullReferenceExceptionwhen the buffer ran, and a default flat list withInvalidOperationException.yield!of a sequence andforcallcheckNotNullOrDefaultSeq,yield!of an array and of a sorted map callcheckNotNull, and all of them do it when the member runs, before the buffer, so the exception namespairs,tableorsource. The function arguments of the expression (buffer,bodyand the others) are not checked, as the function arguments of the module are not.minKeyValuetakes the first binding in O(log n).maxKeyValuetakes O(n), becauseImmutableSortedDictionaryenumerates only forwards and hides its tree. Both throw theKeyNotFoundExceptionofMapfor an empty map.mapandchoosegive the new values the default comparer.foldBackvisits the keys in descending order, asMap.foldBackdoes.existsandforallloop over the struct enumerator of the sorted map, and allocate 0 B per call in Release. The shared algorithm overseq<KeyValuePair>allocated 56 B, LINQ 136 B andSeq80 B.Option.SortedMaprepeatschange,choose,pick,tryFind,tryFindKey,tryGetKeyandtryPickwith options.Tuple.SortedMaprepeatsmaxKeyValue,minKeyValue,partition,toArray,toListandtoSeqwith reference tuples. The nested modulesTupleandValueTupleresolve afteropen FSharp.Collections.Immutable.Tupletoo.sortedMap { }yieldsstruct (key, value)orKeyValuePair, and a later binding of a key wins unless its value is equal (D14).SortedMapCE keyComparer { }takes a key comparer, and anullone throwsArgumentNullException.sortedMapis a type function that requires'Key : comparison.SortedMap/SortedMapCE.fsi, compiled right beforeSortedMapCE.fs, carries the documentation of the builder;Usingtakes'Resource :> IDisposable | null.ComputationExpressions.fsideclaressortedMapafterhashMap, under theCreationbanner.emptyWithComparers,emptyWithKeyComparer,ofKeyValuePairs,ofKeyValuePairsWithKeyComparer,ofSeqWithKeyComparer,builderWithComparers,builderWithKeyComparer,withComparersandwithKeyComparer. The struct replaces them, andofKeyValuePairsis theKeyValuePairoverload ofofSeq.Maps.fs: deleted. ItsSortedMapmodule was the last one in it.Addedhas the new module and says what the gap analysis found missing: the three differences ofofSeqof the struct fromWithComparers, the rejecting form of the creation functions, the compiled names of the extension members, the idioms ofTryGetValueandGetValueOrDefault, the missing builder module and the element selector. It also holds the notes that the entries againstmaincarried and that are still true: the key order of the creation functions, the comparers that the transformations keep, the optional values and struct tuples, the null checks, the convention of D14 withadd, and the replacement ofSeq.ofSortedMapandSeq.toSortedMap.Changedhas the key order, the constraint and the compiled name ofSortedMap.empty, the only function of 1.0.0, which scripts observed by running the 1.0.0 function and the new one.Removedhas theSortedMapBuilderabbreviation, the only name of 1.0.0 that is gone, andAddednames nothing that the 1.0.0 DLL has. The collection tables namesortedMap { }.main(the unreleasedSortedMapmodule that this pull request replaces; none of this is in 1.0.0):lengthiscount,appendisaddRange,exceptisremoveRange, andwhereisfilter.ofKeyComparer,ofComparers,builderWithKeyComparerandbuilderWithComparersare the membersemptyandbuilderofSortedMap.forKeyComparerandSortedMap.forComparers.vTryFind,vTryPick,vTryFindKeyandofSeqWitharetryFind,tryPick,tryFindKeyandofSeqBy.ofSeqGroupByand the publiccheckare gone: group with thegroupByof a collection module and pass the groups toofSeq.Seq.ofSortedMapandSeq.toSortedMapof the unreleasedSeqmodule areSortedMap.toSeqandSortedMap.ofSeq.toSeqyields struct tuples instead ofKeyValuePairs, andSeq.toArray tablegives the old shape.tryFind,tryFindKeyandtryPickreturnvoption,tryFindKeyreturns the key instead of theKeyValuePair,pickandtryPicktake choosers that returnvoption, andtoArrayandtoSeqreturn struct tuples.SortedMapfollowsMap:ofArrayandofSeqkeep the last binding of a repeated key instead of throwing,filterreturns a map instead of a sequence,mapkeeps the keys and takes a function of the key and the value,find,findKeyandpickthrow theKeyNotFoundExceptionofMap, and anullmap throwsArgumentNullException.singletontakes the key and the value curried; it took one argument, which it passed toImmutableSortedDictionary.Createas the key comparer, and returned an empty map.builderorder the keys with F# generic comparison, wheremainusedComparer<'Key>.Default.addis theAddofImmutableSortedDictionary, as it is onmain.Tests: 4926 per target framework, 869 of them new: 868 for
SortedMapand one forIMap.DataRowandDynamicDatarows, so every case runs and reports as a test of its own. The 167 cases of the former loops became 167 rows. The tests ofminKeyValue,maxKeyValueandcleargave one test of their own to what shared a body.SortedMapderives the reading, transformation and mutation contracts of feat:HashMapandIMapmodules with the vocabulary of the FSharp.CoreMapmodule #30 (458 of the 868 tests come from the contracts). The mutation contract runsaddRangeandsetItemsin the three shapes of the bindings, checksaddagainstMap.addfor keys without a binding and against the BCLAddfor the others, and the reading contract has a row ofcontainsBindingfor every binding, every key with another value and every absent key.SortedMapderives the creation contract twice. The first derivation runs the module and the extension members with the default comparers. The second runsCustomComparersSortedMapwith a key comparer that orders the keys by their absolute values. Every shape runs on every input, the bindings that repeat keys included.Mapover string, negative and tuple keys: the creation functions (24 rows),ofSeqBy, the functions that visit the bindings and the transformations. They also coverminKeyValue,maxKeyValue,singleton,nullinputs of every function (42 and 14), results that share the input, and the comparers that every function keeps.FlatListAssert.RejectsNull: anullsequence and a default flat list given as a sequence must throwArgumentNullExceptionwith the name of the parameter of the function that was called. The rows are the sequence functions of the module (addRange,removeRange,setItems,ofSeqin three shapes,ofSeqBy, and theTupleandValueTuplevariants ofaddRangeandsetItems), the members of the struct (ofSeqin three shapes andofSeqBy), andyield!andforofsortedMap { }, which also have rows for anullarray and anullsorted map. Rows with a default flat list of strings given as a sequence of objects (removeRange,ofSeqByof the struct,for) cover the covariance ofseq, which a type test for a flat list of objects does not see. A test per facet checks that a flat list that is not default still passes. Each of the 14 checks of a default flat list, put back to the check fornull, fails its rows, and so does each of the 5 checks ofsortedMap { }, deleted.nullcomparers with the names of their parameters, and the comparers and the order of the maps of every member. They check the last binding of keys that the comparer considers equal, from 12 sources,ofSeqof an existing map, and the default value.dict,Map, a hash map and a sorted map. One test checks that every creation function shares one comparer per type of tuple keys.buildand thebuildof the struct, every member of the struct, every shape of the extension members and every way of setting a binding insortedMap { }, with a value comparer that finds distinct values equal or with copies of strings that the default comparer finds equal.addis tested against the BCL: it throws whatImmutableSortedDictionary.Addthrows for a key bound to another value, the value comparer decides which values are different, andsetItemreplaces the value thataddrejects.containsBindingis tested with a value comparer that decides equality, wheretryFindfollowed by=disagrees, with the key comparer, with a key without a binding, with anullkey (it throws whatContainsthrows) and with anullmap.buildandupdateare tested for the bindings, the comparers (F# generic comparison and the default value comparer for the module, both comparers for the struct), the input that stays unchanged, the input itself when the action changes nothing, the earlier binding for an equal value, and thenullmap. They are rows of the tables of creations and transformations too.tryFindfollowed bydefaultValuegivesGetValueOrDefault,changeadds a binding only when the key has none,addRangeto an empty map rejects a repeated key asCreateRangedoes whileofSeqlets the last binding win, andaddRangeof mapped pairs does whatToImmutableSortedDictionarywith an element selector does. A reflection test checks the compiled names of the extension members and that the module has none of the three names.ofSeqof the struct is tested: another instance of an equivalent key comparer rebuilds, the key comparer of the map keeps the tree, keys that become equal merge whereWithComparersthrows, and the value comparer is replaced byforKeyComparerand kept byforComparerswith the value comparer of the map.tryFindKeyand the binding shapes are union cases, and the tables of functions are F# maps whose keys are the rows.open FSharp.Collections.Immutable.Tuple, the builder, the computation expression and both namespaces.IMapTestschecks thatIMap.filterkeeps the implementation, the comparers and the order of a sorted map.Types of changes
What types of changes does your code introduce to FSharp.Collections.Immutable?
Put an
xin the boxes that applyThis change breaks code that uses the published 1.0.0 in two ways:
SortedMap.emptyorders the keys with F# generic comparison, asMap.emptydoes, and requires'Key : comparison. The 1.0.0emptyusedComparer<'Key>.Default, which orders strings by the rules of the current culture, so a sorted map of strings now enumerates them in ordinal order, and it accepted any key type. For keys without comparison, or for another order, use(SortedMap.forKeyComparer comparer).empty. C# callers see the compiled nameEmpty, where it wasempty.SortedMapBuilder<'Key, 'Value>abbreviation is gone. UseSortedMap<'Key, 'Value>.Builder.The published 1.0.0 has no other name that this pull request changes or removes:
SortedMap<'Key, 'Value>abbreviates the same type, and the other 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
'Kand'V, so its compiled method has four type parameters. The two of the extended type are unused. C# callers use the struct anyway. Inside generic F# code, the shape of the bindings needs a type annotation (FS0041).SortedMapCErequires'Key : comparison: the constraint is on the class, becausenew ()uses F# generic comparison. SosortedMap { }cannot build a map of keys without F# comparison, even with a custom comparer. The struct can.maxKeyValueis O(n):ImmutableSortedDictionaryoffers no reverse enumeration and no access to its tree.existsandforallloop over the struct enumerator.fold,iter,findKey,tryFindKey,pickandtryPickuse the shared algorithms overseq<KeyValuePair>, which box the enumerator (56 B per call). They could take the same path.🤖 Generated with Claude Code