Skip to content

feat: SortedMap module with the vocabulary of the FSharp.Core Map module - #37

Open
xperiandri wants to merge 4 commits into
feat/hash-mapfrom
feat/sorted-map
Open

xperiandri wants to merge 4 commits into
feat/hash-mapfrom
feat/sorted-map

Conversation

@xperiandri

@xperiandri xperiandri commented Oct 18, 2026 •

Copy link
Copy Markdown
Collaborator

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 the Dictionaries algorithms of Helper/Dictionaries.fs.

The published 1.0.0 has the type abbreviations SortedMap<'Key, 'Value> and SortedMapBuilder<'Key, 'Value> and a SortedMap module whose only function is empty (checked in the metadata of both target frameworks of the package, and with a script that compiles against the DLL). main has an unreleased SortedMap module, which this pull request replaces. The changelog compares with 1.0.0, and the bullet "Against main" below compares with main.

  • SortedMap/SortedMapTypes.fs: the abbreviation SortedMap<'Key, 'Value>. SortedMap and its Option and Tuple modules are root modules (D2), and a root module cannot declare a type of its namespace.
  • SortedMap/SortedMap.fsi and SortedMap.fs: 48 functions in the canonical category order, and the nested modules Tuple and ValueTuple. They are the SortedMap column of ADR Appendix B, with the parameter names of the FSharp.Core Map module.
    • partition, toArray, toList, toSeq, minKeyValue and maxKeyValue return struct tuples (D3).
    • The BCL wrappers are add, addRange, containsBinding, containsValue, the comparer accessors, the builder functions (build, builder, ofBuilder, toBuilder and update), ofSeqBy, removeRange, setItem, setItems and tryGetKey.
    • addRange and setItems take KeyValuePairs. The nested modules hold them for reference and struct tuples (D13): SortedMap.Tuple.addRange pairs table, SortedMap.ValueTuple.setItems pairs table.
    • forComparers and forKeyComparer create the comparer struct. choose and singleton are extras.
  • SortedMap/SortedMap.CustomComparers.fsi and .fs (D12): the struct CustomComparersSortedMap<'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 and EqualityComparer<'Value>.Default, (keyComparer).
    • Its properties are KeyComparer and ValueComparer. Its members have the names of the creation functions and PascalCase compiled names: empty, singleton, ofArray, ofList and ofSeq in the three shapes, ofSeqBy, build and builder.
    • SortedMap.forComparers keyComparer valueComparer and SortedMap.forKeyComparer keyComparer create it: (SortedMap.forKeyComparer StringComparer.OrdinalIgnoreCase).ofSeq pairs.
    • A null comparer throws ArgumentNullException with the name of its parameter, in the constructors and in the for* functions.
    • ofSeq of 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) through WithComparers. An empty map comes back too. Any other map gets the comparers of the struct binding by binding.
    • This is how to change the comparers of a map. It differs from WithComparers in three ways, which the remarks of the struct, forComparers and forKeyComparer describe, 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), where WithComparers throws ArgumentException; and forKeyComparer gives the values the default comparer, where WithComparers keeps the value comparer, so forComparers keyComparer (valueComparer table) keeps it.
    • The default value of the struct holds no comparers and falls back to what the BCL takes for null: Comparer<'Key>.Default and EqualityComparer<'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.fsi and .fs (D13): the auto-opened module SortedMapExtensions extends ImmutableSortedDictionary<'Key, 'Value> with static ofArray, ofList and ofSeq, each overloaded for KeyValuePairs, reference tuples and struct tuples.
    • The SortedMap module has no function with their names, so SortedMap.ofSeq pairs reaches the overloads, also as pairs |> SortedMap.ofSeq. Lists, arrays, dict, an F# Map, a hash map and an existing sorted map resolve.
    • F# generic comparison needs '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>.
    • They are the members of the struct with the default comparers, so each shape has one implementation. C# does not see them as members of ImmutableSortedDictionary. It calls them as static methods of SortedMapExtensions (OfArray, OfList and OfSeq, one overload per shape) or uses the struct. A reflection over the SortedMap module lists none of the three names.
  • File nesting (D2): the struct and the extension files carry DependentUpon in the project file, the .fsi on SortedMap.fsi and the .fs on SortedMap.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.
  • Default comparer (D5): a sorted map that is created without a comparer orders its keys with DefaultComparer<'Key>.Instance from Helper/Comparers.fs. That is F# generic comparison, as Map uses, in one cached instance per type.
    • The creation functions of the module, the extension members and sortedMap { } use it. empty is created once per type.
    • Strings sort ordinally, as in Map. They do not sort by the rules of the current culture, as Comparer<string>.Default sorts them.
    • Maps of one key type share one comparer instance. The BCL needs the same instance for its shortcuts, such as WithComparers without a copy. FastGenericComparer at a concrete tuple type creates a new comparer on each call.
  • Values that the value comparer finds equal (D14): a new value of a key that the value comparer finds equal to the current one leaves the binding as it is, its key included, as ImmutableSortedDictionary.SetItem, AddRange, ToImmutableSortedDictionary and the indexer of the builder do. This holds for add, change, setItem, setItems, addRange, their Tuple, ValueTuple and Option variants, the creation functions and members, and sortedMap { }. The functions that set one binding then return the input itself. The remarks of every affected function and member say so.
    • The bulk paths of ImmutableSortedDictionary keep the earlier binding too, whatever the size of the input: SetItems, AddRange and ToImmutableSortedDictionary on an empty or a filled map (checked up to 200 pairs, in and out of order). The module wraps them as they are.
  • add is the BCL Add: SortedMap.add calls ImmutableSortedDictionary.Add, the decision of the maintainers. A key that is bound to a value that the value comparer finds different throws ArgumentException, the same binding returns the input itself, and a key without a binding gets it. Map.add replaces the value, and so does setItem. The remarks of the module, add, setItem and change say so, and setItem is no longer called a synonym of add.
  • containsBinding key value table: wraps Contains (KeyValuePair), which compares the value with the value comparer of the map. containsKey ignores the value, containsValue ignores the key and costs O(n), and tryFind followed by = compares with F# equality, which for a comparer that ignores the case finds "Hello" and "HELLO" different. A null key throws ArgumentNullException, as the member does. IMap.containsBinding of feat: HashMap and IMap modules with the vocabulary of the FSharp.Core Map module #30 covers the interface.
  • build action and update action table: build fills a builder with the default comparers and returns the sorted map; update changes a builder over the bindings and comparers of a map and returns the input itself when nothing changed. The struct has the member build, with its comparers. There is no ofReadOnlySpan, because the BCL has no span creation for sorted dictionaries.
  • Remarks on the idioms of the BCL: TryGetValue is tryFind, GetValueOrDefault is tryFind followed by ValueOption.defaultValue, and adding a binding only when the key has none is change with a function that keeps an existing value. ImmutableInterlocked has no member for a sorted dictionary, but its Update swaps a field with the result of a function of the map. The rejecting form of the creation functions is SortedMap.empty |> SortedMap.addRange pairs (it throws as CreateRange does), and an element selector is addRange of the mapped pairs (ToImmutableSortedDictionary). Maps have no builder module: the members of the BCL builder are called on it.
  • Cost of the builder functions: the remarks of ofBuilder, build and update say that ToImmutable freezes 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.
  • Repeated keys:
    • ofArray, ofList and ofSeq keep the last binding of a key in every shape, as Map.ofSeq does, 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.CreateRange would reject them.
    • The BCL wrappers ofSeqBy and addRange keep the behavior of ImmutableSortedDictionary. A repeated key with an equal value is accepted and keeps the first binding; a repeated key with another value throws ArgumentException. setItems replaces the value.
    • Each function documents its behavior.
  • Sequence inputs (D6): a seq argument throws ArgumentNullException that names the parameter for null and for a default flat list given as a sequence, which would otherwise fail with InvalidOperationException inside the BCL call. One call of checkNotNullOrDefaultSeq guards each of these before anything else: addRange, removeRange and setItems of SortedMap, the Tuple and ValueTuple modules, and the ofSeq and ofSeqBy members of the struct (SortedMap.ofSeqBy and the extension overloads of ofSeq call them). Arrays keep their null check.
    • sortedMap { } checked none of its inputs: a null sequence, array or sorted map failed with NullReferenceException when the buffer ran, and a default flat list with InvalidOperationException. yield! of a sequence and for call checkNotNullOrDefaultSeq, yield! of an array and of a sorted map call checkNotNull, and all of them do it when the member runs, before the buffer, so the exception names pairs, table or source. The function arguments of the expression (buffer, body and the others) are not checked, as the function arguments of the module are not.
  • Other functions:
    • minKeyValue takes the first binding in O(log n). maxKeyValue takes O(n), because ImmutableSortedDictionary enumerates only forwards and hides its tree. Both throw the KeyNotFoundException of Map for an empty map.
    • Every transformation keeps the key comparer, and so the order. The transformations that keep the value type keep the value comparer; map and choose give the new values the default comparer. foldBack visits the keys in descending order, as Map.foldBack does.
    • exists and forall loop over the struct enumerator of the sorted map, and allocate 0 B per call in Release. The shared algorithm over seq<KeyValuePair> allocated 56 B, LINQ 136 B and Seq 80 B.
  • Option and Tuple namespaces (D3): Option.SortedMap repeats change, choose, pick, tryFind, tryFindKey, tryGetKey and tryPick with options. Tuple.SortedMap repeats maxKeyValue, minKeyValue, partition, toArray, toList and toSeq with reference tuples. The nested modules Tuple and ValueTuple resolve after open FSharp.Collections.Immutable.Tuple too.
  • Computation expression (D7): sortedMap { } yields struct (key, value) or KeyValuePair, and a later binding of a key wins unless its value is equal (D14). SortedMapCE keyComparer { } takes a key comparer, and a null one throws ArgumentNullException. sortedMap is a type function that requires 'Key : comparison.
    • SortedMap/SortedMapCE.fsi, compiled right before SortedMapCE.fs, carries the documentation of the builder; Using takes 'Resource :> IDisposable | null.
    • ComputationExpressions.fsi declares sortedMap after hashMap, under the Creation banner.
  • Removed comparer functions: emptyWithComparers, emptyWithKeyComparer, ofKeyValuePairs, ofKeyValuePairsWithKeyComparer, ofSeqWithKeyComparer, builderWithComparers, builderWithKeyComparer, withComparers and withKeyComparer. The struct replaces them, and ofKeyValuePairs is the KeyValuePair overload of ofSeq.
  • Maps.fs: deleted. Its SortedMap module was the last one in it.
  • Docs: the changelog compares with 1.0.0. Added has the new module and says what the gap analysis found missing: the three differences of ofSeq of the struct from WithComparers, the rejecting form of the creation functions, the compiled names of the extension members, the idioms of TryGetValue and GetValueOrDefault, the missing builder module and the element selector. It also holds the notes that the entries against main carried 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 with add, and the replacement of Seq.ofSortedMap and Seq.toSortedMap. Changed has the key order, the constraint and the compiled name of SortedMap.empty, the only function of 1.0.0, which scripts observed by running the 1.0.0 function and the new one. Removed has the SortedMapBuilder abbreviation, the only name of 1.0.0 that is gone, and Added names nothing that the 1.0.0 DLL has. The collection tables name sortedMap { }.
  • Against main (the unreleased SortedMap module that this pull request replaces; none of this is in 1.0.0):
    • length is count, append is addRange, except is removeRange, and where is filter. ofKeyComparer, ofComparers, builderWithKeyComparer and builderWithComparers are the members empty and builder of SortedMap.forKeyComparer and SortedMap.forComparers. vTryFind, vTryPick, vTryFindKey and ofSeqWith are tryFind, tryPick, tryFindKey and ofSeqBy. ofSeqGroupBy and the public check are gone: group with the groupBy of a collection module and pass the groups to ofSeq.
    • Seq.ofSortedMap and Seq.toSortedMap of the unreleased Seq module are SortedMap.toSeq and SortedMap.ofSeq. toSeq yields struct tuples instead of KeyValuePairs, and Seq.toArray table gives the old shape.
    • tryFind, tryFindKey and tryPick return voption, tryFindKey returns the key instead of the KeyValuePair, pick and tryPick take choosers that return voption, and toArray and toSeq return struct tuples.
    • SortedMap follows Map: ofArray and ofSeq keep the last binding of a repeated key instead of throwing, filter returns a map instead of a sequence, map keeps the keys and takes a function of the key and the value, find, findKey and pick throw the KeyNotFoundException of Map, and a null map throws ArgumentNullException. singleton takes the key and the value curried; it took one argument, which it passed to ImmutableSortedDictionary.Create as the key comparer, and returned an empty map.
    • The creation functions and builder order the keys with F# generic comparison, where main used Comparer<'Key>.Default. add is the Add of ImmutableSortedDictionary, as it is on main.

Tests: 4926 per target framework, 869 of them new: 868 for SortedMap and one for IMap.

  • The tests compare arrays and take their cases as DataRow and DynamicData rows, so every case runs and reports as a test of its own. The 167 cases of the former loops became 167 rows. The tests of minKeyValue, maxKeyValue and clear gave one test of their own to what shared a body.
  • SortedMap derives the reading, transformation and mutation contracts of feat: HashMap and IMap modules with the vocabulary of the FSharp.Core Map module #30 (458 of the 868 tests come from the contracts). The mutation contract runs addRange and setItems in the three shapes of the bindings, checks add against Map.add for keys without a binding and against the BCL Add for the others, and the reading contract has a row of containsBinding for every binding, every key with another value and every absent key.
  • SortedMap derives the creation contract twice. The first derivation runs the module and the extension members with the default comparers. The second runs CustomComparersSortedMap with a key comparer that orders the keys by their absolute values. Every shape runs on every input, the bindings that repeat keys included.
  • The per-collection tests compare the order with Map over string, negative and tuple keys: the creation functions (24 rows), ofSeqBy, the functions that visit the bindings and the transformations. They also cover minKeyValue, maxKeyValue, singleton, null inputs of every function (42 and 14), results that share the input, and the comparers that every function keeps.
  • The sequence inputs (D6) have rows per facet, and every row goes through FlatListAssert.RejectsNull: a null sequence and a default flat list given as a sequence must throw ArgumentNullException with the name of the parameter of the function that was called. The rows are the sequence functions of the module (addRange, removeRange, setItems, ofSeq in three shapes, ofSeqBy, and the Tuple and ValueTuple variants of addRange and setItems), the members of the struct (ofSeq in three shapes and ofSeqBy), and yield! and for of sortedMap { }, which also have rows for a null array and a null sorted map. Rows with a default flat list of strings given as a sequence of objects (removeRange, ofSeqBy of the struct, for) cover the covariance of seq, 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 for null, fails its rows, and so does each of the 5 checks of sortedMap { }, deleted.
  • The struct tests cover null comparers 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, ofSeq of an existing map, and the default value.
  • The extension tests cover the last binding and the ordinal order of strings in all nine overloads. They also run pipelines from arrays, lists, sequences, dict, Map, a hash map and a sorted map. One test checks that every creation function shares one comparer per type of tuple keys.
  • The convention of D14 has 39 tests: every function that sets a binding, build and the build of the struct, every member of the struct, every shape of the extension members and every way of setting a binding in sortedMap { }, with a value comparer that finds distinct values equal or with copies of strings that the default comparer finds equal.
  • add is tested against the BCL: it throws what ImmutableSortedDictionary.Add throws for a key bound to another value, the value comparer decides which values are different, and setItem replaces the value that add rejects.
  • containsBinding is tested with a value comparer that decides equality, where tryFind followed by = disagrees, with the key comparer, with a key without a binding, with a null key (it throws what Contains throws) and with a null map.
  • build and update are 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 the null map. They are rows of the tables of creations and transformations too.
  • The recipes of the remarks are pinned against the BCL: tryFind followed by defaultValue gives GetValueOrDefault, change adds a binding only when the key has none, addRange to an empty map rejects a repeated key as CreateRange does while ofSeq lets the last binding win, and addRange of mapped pairs does what ToImmutableSortedDictionary with 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.
  • The change of comparers through ofSeq of 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 where WithComparers throws, and the value comparer is replaced by forKeyComparer and kept by forComparers with the value comparer of the map.
  • A row holds printable data: the sets of keys and the creations of the order tests, the predicates of tryFindKey and the binding shapes are union cases, and the tables of functions are F# maps whose keys are the rows.
  • Other tests cover the nested modules after open FSharp.Collections.Immutable.Tuple, the builder, the computation expression and both namespaces. IMapTests checks that IMap.filter keeps 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 x in the boxes that apply

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

This change breaks code that uses the published 1.0.0 in two ways:

  • SortedMap.empty orders the keys with F# generic comparison, as Map.empty does, and requires 'Key : comparison. The 1.0.0 empty used Comparer<'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 name Empty, where it was empty.
  • The SortedMapBuilder<'Key, 'Value> abbreviation is gone. Use SortedMap<'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 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

  • Type parameters of the extension members: each member declares 'K and '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).
  • SortedMapCE requires 'Key : comparison: the constraint is on the class, because new () uses F# generic comparison. So sortedMap { } cannot build a map of keys without F# comparison, even with a custom comparer. The struct can.
  • maxKeyValue is O(n): ImmutableSortedDictionary offers no reverse enumeration and no access to its tree.
  • The other visiting functions: only exists and forall loop over the struct enumerator. fold, iter, findKey, tryFindKey, pick and tryPick use the shared algorithms over seq<KeyValuePair>, which box the enumerator (56 B per call). They could take the same path.

🤖 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

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

Comment thread src/FSharp.Collections.Immutable/SortedMap/SortedMapCE.fs

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Needs a closer look

The broad breaking API replacement warrants human review and confirmation of cross-target build and test results.

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 broad breaking API replacement and overload-resolution changes need maintainer validation, with builds and tests not independently verified.

1 open issue

Review effort: Balanced

Comment thread src/FSharp.Collections.Immutable/SortedMap/SortedMap.fsi 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 broad public API rewrite and breaking comparer changes require human compatibility validation.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

… 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>
xperiandri and others added 2 commits October 20, 2026 18:13
- `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>
…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>

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