Skip to content

feat: HashMap and IMap modules with the vocabulary of the FSharp.Core Map module - #30

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

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

Conversation

@xperiandri

@xperiandri xperiandri commented Oct 18, 2026 •

Copy link
Copy Markdown
Collaborator

Proposed Changes

Rewrites HashMap (ImmutableDictionary<'Key, 'Value>) and adds the interface module IMap (IImmutableDictionary<'Key, 'Value>) after ADR 0001 (#23). It builds on #26 (FlatList), which is merged into main.

The branch follows the reworked ADR, and the three points of the review are done: the modules are root modules, a comparer struct creates hash maps with comparers, and extension overloads take the bindings in every shape.

  • HashMap/HashMapTypes.fs: the abbreviations HashMap<'Key, 'Value> and IMap<'Key, 'Value>. HashMap, IMap and their Option and Tuple modules are the root modules of their files (D2), and a root module cannot declare a type of its namespace.
  • HashMap/HashMap.fsi and HashMap.fs: 48 functions, in the canonical category order, and the nested modules Tuple and ValueTuple. They are the HashMap column of ADR Appendix B, with the parameter names of the FSharp.Core Map module (key, value, table, elements, f and so on) and span for a span, as in FlatList.
    • partition, toArray, toList and toSeq return struct tuples (D3).
    • The BCL wrappers are the comparer accessors, the builder functions build, builder, ofBuilder, toBuilder and update, add, addRange, containsBinding, containsValue, ofReadOnlySpan, ofSeqBy, removeRange, setItem, setItems and tryGetKey.
    • add is the BCL Add, as the 1.0.0 function was (the maintainer's decision): a key that is bound to a value that the value comparer finds different throws ArgumentException, an equal binding returns the input itself, and a new key is added. Map.add replaces the value, and setItem is the function that does; change adds a binding only when the key has none. There is no addNew.
    • containsBinding key value table wraps Contains of a KeyValuePair, so the value comparer of the map decides whether the values are equal; tryFind followed by = compares with F# equality instead. IMap has it too, where the comparers are out of reach.
    • ofReadOnlySpan span and ofSpan span create a map from a ReadOnlySpan or a Span of KeyValuePair<_, _> (CreateRangeWithOverwrite, which takes a read-only span only); a repeated key never throws. The pair exists because F# converts a Span to the parameter of ofReadOnlySpan only when it has inferred the types of the call (otherwise FS0001), as FlatList has ofReadOnlySpan and ofSpan. An array converts to either parameter under the same condition.
    • build action and update action table fill or change a builder with an action, as FlatList.build and update do. update returns the input itself when the action changes nothing.
    • addRange and setItems take KeyValuePairs. The nested modules hold them for reference and struct tuples (D13): HashMap.Tuple.addRange pairs table, HashMap.ValueTuple.setItems pairs table.
    • forComparers and forKeyComparer create the comparer struct.
    • choose is an extra.
  • HashMap/HashMap.CustomComparers.fsi and .fs (D12): the struct CustomComparersHashMap<'Key, 'Value> holds the key comparer and the value comparer, (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, ofReadOnlySpan, ofSpan, ofSeqBy, build and builder.
    • HashMap.forComparers keyComparer valueComparer and HashMap.forKeyComparer keyComparer create it: (HashMap.forKeyComparer StringComparer.OrdinalIgnoreCase).ofSeq pairs.
    • A null comparer throws ArgumentNullException with the name of its parameter, in the constructors and in the for* functions. The default value of the struct stands for the default comparers.
    • ofReadOnlySpan wraps CreateRangeWithOverwrite for the default value comparer. The BCL overload with a key comparer has no value comparer, so another one sets the bindings in a builder with the comparers, which gives the same result; a test runs both paths on the same bindings. ofSpan passes its span to ofReadOnlySpan.
    • ofSeq gives an existing map the comparers of the struct, which is how to change the comparers of a map. It differs from WithComparers in three ways that the remarks and an example now state: it takes O(1) only when the key comparer of the map is the instance of the struct, keys that become equal merge where WithComparers throws, and forKeyComparer resets the value comparer.
  • HashMap/HashMap.Extensions.fsi and .fs (D13): the auto-opened module HashMapExtensions extends ImmutableDictionary<'Key, 'Value> with static ofArray, ofList and ofSeq, each overloaded for KeyValuePairs, reference tuples and struct tuples.
    • They are the members of the struct with the default comparers, so each shape has one implementation.
    • The HashMap module has no function with their names, so HashMap.ofSeq pairs reaches the overloads, also as pairs |> HashMap.ofSeq. Lists, arrays, dict, an F# Map and an existing hash map resolve.
    • C# does not see them as members of ImmutableDictionary. It calls the public static methods HashMapExtensions.OfArray, OfList and OfSeq (checked by reflection) or uses the struct. A reflection over the HashMap module does not list them.
    • A repeated key never throws, where CreateRange rejects another value of it. The rejecting form is HashMap.empty |> HashMap.addRange pairs.
  • File nesting (D2): the struct and the extension files carry DependentUpon in the project file, the .fsi on HashMap.fsi and the .fs on HashMap.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.
  • HashMap/IMap.fsi and IMap.fs: 33 functions over IImmutableDictionary, and the nested modules Tuple and ValueTuple with addRange and setItems.
    • add is the Add of the interface and containsBinding its Contains of a KeyValuePair; the latter is the one member of the interface that no other function reaches. The interface declares no creation, builder, comparer or containsValue members, so the module has no such functions.
    • filter and partition keep the implementation of the map (Clear () plus AddRange). When every binding stays, they return the input itself.
    • map and choose return a HashMap with the default comparers.
  • Comparers (D5): every transformation keeps the key comparer. The transformations that keep the value type also keep the value comparer.
  • 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 ImmutableDictionary.SetItem, Add, AddRange, ToImmutableDictionary and the indexer of the builder do. This holds for add, change, setItem, setItems, addRange, their Tuple, ValueTuple and Option variants, IMap, the creation functions and members, ofReadOnlySpan, ofSpan, build and hashMap { }. The functions that set one binding then return the input itself; add throws for another value. The remarks of every affected function and member say so, IMap.map and IMap.choose included, where keys that only the default comparer finds equal keep the first of equal results.
  • Repeated keys:
    • ofArray, ofList, ofSeq, ofReadOnlySpan and ofSpan 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).
    • The BCL wrappers add, ofSeqBy and addRange keep the behavior of ImmutableDictionary. A repeated key with an equal value is accepted and keeps the first binding; a repeated key with another value throws ArgumentException. setItem and setItems replace the value.
    • Each function documents its behavior.
  • Exceptions: find and pick throw KeyNotFoundException as Map does. findKey throws the message that Map.findKey gets from Seq.pick.
  • 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: ofSeqBy, addRange, removeRange and setItems of HashMap and of IMap, the Tuple and ValueTuple modules of both, the ofSeq and ofSeqBy members of the struct (the extension overloads call them), and yield! and for of hashMap { }, which check when the member runs and not when the buffer does; yield! of an array and of a hash map checks for null the same way, with ArgumentNullException that names pairs or table. Arrays keep their null check.
  • Order of foldBack: foldBack visits the bindings in the reverse order of their enumeration. Its remarks say so.
  • Option and Tuple namespaces: HashMapOption and IMapOption contain change, choose, pick, tryFind, tryFindKey, tryGetKey and tryPick with options. HashMapTuple and IMapTuple contain partition, toArray, toList and toSeq with reference tuples. The functions that take bindings need no counterpart there, and the nested modules Tuple and ValueTuple resolve after open FSharp.Collections.Immutable.Tuple too.
  • Computation expression (D7): hashMap { } yields struct (key, value) or KeyValuePair, and a later binding of a key wins unless its value is equal (D14). HashMapCE keyComparer { } takes a key comparer, and a null one throws ArgumentNullException. HashMapCE<'Key> is generic in the key type, because its comparer has that type, so hashMap is a type function: [<GeneralizableValue>] let hashMap<'Key when 'Key : not null> = HashMapCE<'Key> ().
    • HashMap/HashMapCE.fsi, compiled right before HashMapCE.fs, carries the documentation of the builder; Using takes 'Resource :> IDisposable | null.
    • ComputationExpressions.fsi declares hashMap after flatList, under the Creation banner.
  • Comparer functions of main that the struct replaces: emptyWithComparers, emptyWithKeyComparer, ofKeyValuePairs, ofKeyValuePairsWithKeyComparer, ofSeqWithKeyComparer, builderWithComparers, builderWithKeyComparer, withComparers and withKeyComparer. The published 1.0.0 did not have them, so the changelog does not list them as removed. The struct replaces them, and ofKeyValuePairs is the KeyValuePair overload of ofSeq.
  • Decisions on the questions of the first version:
    1. addRange and setItems take the same shape, KeyValuePairs, and the nested modules Tuple and ValueTuple take the tuples.
    2. Every creation function keeps the last binding of a repeated key, unless the value comparer finds the values equal (D14). The comparer variants that threw are gone; only ofSeqBy keeps the behavior of the BCL.
    3. HashMap.keys and values return ICollection<_>, as Map.keys and Map.values do. IMap.keys and values return seq<_>, because the interface only exposes IReadOnlyDictionary.Keys (ADR Appendix B).
    4. add works as in the BCL, as it did in 1.0.0: it throws for a key bound to another value, and setItem replaces. The first version of this rewrite made add replace, which was a mistake; the changelog no longer says that add changed.
  • Helper/:
    • Raise.keyNotFound is new (Helper/Raise.fs).
    • The internal module Dictionaries, in Helper/Dictionaries.fs, holds the algorithms over seq<KeyValuePair> and IDictionary and the deconstructors of the binding shapes, splitKeyValuePair and splitTuple, so that SortedMap, FrozenMap and the interface modules can reuse them.
  • Maps.fs: only the legacy SortedMap remains. The legacy HashMap module and the HashMap, HashMapBuilder and IMap abbreviations are removed.
  • Docs: docsSrc/Tutorials/Getting_Started.md creates its hash map from reference tuples and shows HashMap.forKeyComparer. The changelog lists the struct, the overloads, the nested modules, the new functions and the values that the value comparer finds equal. It also says what the API gap analysis found undocumented, and the remarks of the functions repeat it where users look:
    • the three differences between ofSeq of the struct and WithComparers, with an example, and a pointer from forComparers and forKeyComparer;
    • the rejecting form of the creation functions, HashMap.empty |> HashMap.addRange pairs, and the recipe for ToImmutableDictionary with an element selector;
    • the compiled names of the extension members and that a reflection over the module does not list them;
    • the idioms of TryGetValue (tryFind), GetValueOrDefault (tryFind followed by ValueOption.defaultValue) and TryAdd (change);
    • that maps have no builder module, and that IMap has only what the interface declares;
    • the functions and the abbreviation of 1.0.0 that are gone (length, append, except, check and HashMapBuilder), each with its replacement, and the three functions whose behavior differs from 1.0.0 (tryFind, ofSeq and filter); both lists were checked against the published 1.0.0 package.

Tests: 4057 per target framework, 1297 of them for HashMap (805) and IMap (492).

  • 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 1045 cases of the former 347 tests became 1044 rows: the test that the order of enumeration is the order of the key/value pairs merged into the rows of toArray, and the comparers of map and choose are a row each.
  • The shared map contracts in Contracts/ (MapOps.fs, MapInputs.fs, MapContract, MapTransformContract, MapMutationContract and MapCreationContract) compare every function with FSharp.Core Map. MapInputs is a static class whose properties build new arrays on each read.
  • Whole maps are compared after sorting by key.
  • The functions that depend on the order are compared over the enumeration order of the map. These are findKey, tryFindKey, pick, tryPick, fold, foldBack, iter, toArray, toSeq, keys and values; toArray is compared with the map's own enumeration of key/value pairs, because a hash map does not keep the order of its input.
  • Every exception is compared with the exception of Map, except that of add, which is compared with the BCL member that it wraps.
  • A row holds printable data: predicates, changes of change, binding shapes and the implementations behind IMap are union cases, and the tables of functions are F# maps whose keys are the rows.
  • The creation facet runs ofArray, ofList and ofSeq in the three shapes on every input, the bindings that repeat a key included. It takes dict, an F# Map and an existing map as sources of KeyValuePairs, and checks that every created map finds its keys with its key comparer.
  • The mutation facet runs addRange and setItems in the three shapes: the module for KeyValuePairs, the nested modules for tuples. It compares add with Map.add only for keys without a binding; for the keys of the input it checks the BCL result, the input itself for an equal binding and ArgumentException for another value.
  • The reading facet checks containsBinding on every binding, every key with another value and every absent key. The tests of the modules check it with a value comparer that ignores the case, where tryFind and = disagree with the BCL, on both implementations of IMap, and check that a null key is left to the key comparer.
  • HashMap derives all four facets, and the creation facet a second time for CustomComparersHashMap with a key comparer. IMap derives all of them except creation.
  • The convention of D14 has rows for every function that sets a binding, on both implementations of IMap, every member of the struct, every shape of the extension members and every member of hashMap { } that sets a binding, with a value comparer that finds distinct values equal or with copies of strings that the default comparer finds equal. add has a row for the equal value (the input itself) and for another value (it throws).
  • ofReadOnlySpan and ofSpan are tested once per function, as the cases of a union that builds the span of an array inside its own members: for the module and for both paths of the struct (the default value comparer and another one), on bindings whose keys differ in their case, against the rule of D14 and against each other; they copy the span, and take an array when the types of the call are known. build and update are tested for the bindings, the comparers, the input that stays unchanged and the null table.
  • The sequence inputs have a row per function for null and for a default flat list, for the module, IMap, the members of the struct and hashMap { }; the extension overloads run through the module and the struct. yield! of a null array and of a null hash map has a row too. A default flat list of strings given as seq<obj> has a row per module (FlatListOracles.defaultStringsAsObjects), and a flat list that is not default is checked to pass.
  • Further tests cover null comparers, forKeyComparer StringComparer.OrdinalIgnoreCase, the last binding in every shape, pipelines and the nested modules after open FSharp.Collections.Immutable.Tuple.
  • The test categories are on the test classes only; test methods carry none (D8).

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)

These changes break existing code; the list compares with the published 1.0.0, as the changelog does:

  • tryFind returns a voption, where it returned an option.
  • ofSeq keeps the last binding of a repeated key instead of throwing.
  • filter returns the bindings that satisfy the predicate, where it returned all the bindings.
  • length, append, except, check and the HashMapBuilder abbreviation are gone; the Removed section of the changelog names their replacements.
  • C# calls the creation overloads as static methods of HashMapExtensions or through the struct.

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

ADR 0001, D14 says that a sorted map also keeps the stored key for an equal value. A hash map keeps it too: ImmutableDictionary.SetItem and the indexer of its builder leave the whole binding as it is, so the documentation of HashMap says "its key included" as well.

🤖 Generated with Claude Code

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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Needs a closer look

The broad breaking API rewrite and comparer-sensitive behavior warrant human compatibility review and validation across both target frameworks.

No open issues 🎉

What changed in this PR

Rewrites HashMap and introduces IMap using FSharp.Core Map vocabulary, continuing the per-collection architecture established by ADR 0001.

Changes:

  • Adds map operations, shared dictionary algorithms, Option/Tuple variants, and hashMap computation expressions.
  • Adds reusable map contracts and collection-specific tests.
  • Updates documentation and migration notes for breaking API changes.
File Description
tests/​FSharp.Collections.Immutable.Tests/​HashMap/​IMapTupleTests.fs Tests reference-tuple overrides.
tests/​FSharp.Collections.Immutable.Tests/​HashMap/​IMapTests.fs Tests interface behavior and comparers.
tests/​FSharp.Collections.Immutable.Tests/​HashMap/​IMapOptionTests.fs Tests option overrides.
tests/​FSharp.Collections.Immutable.Tests/​HashMap/​IMapOps.fs Supplies interface contract operations.
tests/​FSharp.Collections.Immutable.Tests/​HashMap/​IMapContractTests.fs Instantiates interface contracts.
tests/​FSharp.Collections.Immutable.Tests/​HashMap/​HashMapTupleTests.fs Tests reference-tuple overrides.
tests/​FSharp.Collections.Immutable.Tests/​HashMap/​HashMapTests.fs Tests hash-map-specific behavior.
tests/​FSharp.Collections.Immutable.Tests/​HashMap/​HashMapOptionTests.fs Tests option overrides.
tests/​FSharp.Collections.Immutable.Tests/​HashMap/​HashMapOps.fs Supplies hash-map contract operations.
tests/​FSharp.Collections.Immutable.Tests/​HashMap/​HashMapContractTests.fs Instantiates hash-map contracts.
tests/​FSharp.Collections.Immutable.Tests/​HashMap/​HashMapComputationExpressionTests.fs Tests expression construction and control flow.
tests/​FSharp.Collections.Immutable.Tests/​HashMap/​HashMapBuilderTests.fs Tests builder behavior.
tests/​FSharp.Collections.Immutable.Tests/​FSharp.Collections.Immutable.Tests.fsproj Registers new tests and contracts.
tests/​FSharp.Collections.Immutable.Tests/​Contracts/​MapTransformContract.fs Adds shared transformation tests.
tests/​FSharp.Collections.Immutable.Tests/​Contracts/​MapOps.fs Defines contract inputs and operations.
tests/​FSharp.Collections.Immutable.Tests/​Contracts/​MapMutationContract.fs Adds shared mutation tests.
tests/​FSharp.Collections.Immutable.Tests/​Contracts/​MapCreationContract.fs Adds shared creation tests.
tests/​FSharp.Collections.Immutable.Tests/​Contracts/​MapContract.fs Adds reading, ordering, and exception contracts.
src/​FSharp.Collections.Immutable/​Maps.fs Removes legacy hash-map definitions.
src/​FSharp.Collections.Immutable/​Helper.fs Adds shared dictionary algorithms and exceptions.
src/​FSharp.Collections.Immutable/​HashMap/​IMapTuple.fsi Declares reference-tuple interface API.
src/​FSharp.Collections.Immutable/​HashMap/​IMapTuple.fs Implements reference-tuple interface operations.
src/​FSharp.Collections.Immutable/​HashMap/​IMapOption.fsi Declares option-based interface API.
src/​FSharp.Collections.Immutable/​HashMap/​IMapOption.fs Implements interface option adapters.
src/​FSharp.Collections.Immutable/​HashMap/​IMap.fsi Documents the interface map API.
src/​FSharp.Collections.Immutable/​HashMap/​IMap.fs Implements interface map operations.
src/​FSharp.Collections.Immutable/​HashMap/​HashMapTuple.fsi Declares reference-tuple hash-map API.
src/​FSharp.Collections.Immutable/​HashMap/​HashMapTuple.fs Implements reference-tuple hash-map operations.
src/​FSharp.Collections.Immutable/​HashMap/​HashMapOption.fsi Declares option-based hash-map API.
src/​FSharp.Collections.Immutable/​HashMap/​HashMapOption.fs Implements hash-map option adapters.
src/​FSharp.Collections.Immutable/​HashMap/​HashMapCE.fs Adds the hash-map expression builder.
src/​FSharp.Collections.Immutable/​HashMap/​HashMap.fs Implements the rewritten hash-map operations.
src/​FSharp.Collections.Immutable/​FSharp.Collections.Immutable.fsproj Registers new library files.
src/​FSharp.Collections.Immutable/​ComputationExpressions.fs Exposes the hashMap expression.
README.md Lists interface and expression support.
docsSrc/​Tutorials/​Getting_Started.md Updates construction to struct tuples.
docsSrc/​index.md Updates the collection overview.
docsSrc/​Explanations/​Background.md Updates the collection support table.
CHANGELOG.md Records additions and breaking changes.

Review effort: Balanced

Comment thread src/FSharp.Collections.Immutable/HashMap/HashMap.fs Outdated
Comment thread src/FSharp.Collections.Immutable/HashMap/HashMap.fs Outdated
Comment thread src/FSharp.Collections.Immutable/HashMap/HashMap.fs Outdated
xperiandri added a commit that referenced this pull request Oct 18, 2026
…in ADR 0001

The review of the HashMap pull request (#30) changes three parts of the design:

- D2: a file with one module declares it as the root module of the file; the type
  abbreviations of a folder move to `XTypes.fsi`/`XTypes.fs`, because a root module
  cannot declare a type of its namespace. Modules carry no `[<CompiledName>]`.
- D12 (new): the comparers that a map or a set keeps come only through one
  `Comparable*` struct per collection, whose members are the creation functions
  with the comparers applied. It replaces the `*WithComparer(s)`, `*WithKeyComparer`
  and `with*Comparer(s)` functions.
- D13 (new): `ofArray`, `ofList` and `ofSeq` of the maps are static extension members
  in an `[<AutoOpen>]` `XExtensions` module, overloaded for `KeyValuePair`, reference
  tuples and struct tuples. `ofKeyValuePairs` and the `Tuple` creation functions go;
  `setItems` takes struct tuples like `addRange`.

The ADR also records what the implementation found: the explicit type parameters of
the alternate lookups (FS0687), the `comparison` constraint and the cached default
comparer of the sorted collections, the O(n) `maxKeyValue` of `SortedMap`, and why
`keys`/`values` return different types.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri added a commit that referenced this pull request Oct 18, 2026
…in ADR 0001

The review of the HashMap pull request (#30) changes three parts of the design:

- D2: a file with one module declares it as the root module of the file; the type
  abbreviations of a folder move to `XTypes.fsi`/`XTypes.fs`, because a root module
  cannot declare a type of its namespace. Modules carry no `[<CompiledName>]`.
- D12 (new): the comparers that a map or a set keeps come only through one
  `Comparable*` struct per collection, whose members are the creation functions
  with the comparers applied. It replaces the `*WithComparer(s)`, `*WithKeyComparer`
  and `with*Comparer(s)` functions.
- D13 (new): `ofArray`, `ofList` and `ofSeq` of the maps are static extension members
  in an `[<AutoOpen>]` `XExtensions` module, overloaded for `KeyValuePair`, reference
  tuples and struct tuples. `ofKeyValuePairs` and the `Tuple` creation functions go;
  `setItems` takes struct tuples like `addRange`.

The ADR also records what the implementation found: the explicit type parameters of
the alternate lookups (FS0687), the `comparison` constraint and the cached default
comparer of the sorted collections, the O(n) `maxKeyValue` of `SortedMap`, and why
`keys`/`values` return different types.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@xperiandri
xperiandri force-pushed the feat/flat-list branch 2 times, most recently from 2b5179a to 9d2526e Compare October 18, 2026 15:35
@xperiandri
xperiandri requested a balanced review from Copilot October 18, 2026 15:42

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

Custom value comparers can discard replacement values required by the documented creation, add, and change semantics.

2 open issues

Review effort: Balanced

Comment thread src/FSharp.Collections.Immutable/HashMap/HashMap.fs Outdated
Comment thread src/FSharp.Collections.Immutable/Helper.fs Outdated
xperiandri added a commit that referenced this pull request Oct 18, 2026
…FSharp.Core `Map` module

Follows the reworked ADR 0001 (D2, D12, D13) and the review of #30:

- `HashMap`, `IMap` and their `Option` and `Tuple` modules are the root modules of their
  files. The `HashMap` and `IMap` abbreviations move to `HashMap/HashMapTypes.fs`.
- `CustomComparersHashMap<'Key, 'Value>` holds the key comparer and the value comparer, or
  the key comparer and the default value comparer, and creates hash maps with them through
  the members `empty`, `singleton`, `ofArray`, `ofList`, `ofSeq`, `ofSeqBy` and `builder`.
  A `null` comparer throws `ArgumentNullException` with the name of its parameter, and the
  default value of the struct stands for the default comparers. `ofSeq` gives an existing
  map the comparers of the struct, without a copy when the key comparer stays.
- `HashMap.forComparers` and `HashMap.forKeyComparer` create the struct. They replace
  `emptyWithComparers`, `emptyWithKeyComparer`, `ofKeyValuePairsWithKeyComparer`,
  `ofSeqWithKeyComparer`, `builderWithComparers`, `builderWithKeyComparer`,
  `withComparers` and `withKeyComparer`.
- `HashMapExtensions` extends `ImmutableDictionary<'Key, 'Value>` with static `ofArray`,
  `ofList` and `ofSeq`, each overloaded for `KeyValuePair`s, reference tuples and struct
  tuples: the members of the struct with the default comparers. The module loses `ofArray`,
  `ofList`, `ofSeq` and `ofKeyValuePairs`, so that `HashMap.ofSeq pairs` reaches the
  overloads, also in a pipeline. Every shape keeps the last binding of a key, as `Map.ofSeq`
  does.
- `addRange` of `HashMap` and `IMap` takes `KeyValuePair`s, as `setItems` does; the nested
  modules `Tuple` and `ValueTuple` hold both functions for reference and struct tuples. The
  modules of the `Tuple` namespace keep `partition`, `toArray`, `toList` and `toSeq`.
- `Dictionaries` in `Helper.fs` gets the deconstructors of the binding shapes,
  `splitKeyValuePair` and `splitTuple`, for every map module and comparer struct.
- `HashMapCE` rejects a `null` key comparer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@xperiandri
xperiandri requested a balanced review from Copilot October 18, 2026 18:48

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Needs a closer look

The broad breaking API rewrite needs human compatibility review and build/test validation, particularly for public signatures and overload resolution.

No open issues 🎉

2 resolved since last review

Review effort: Balanced

xperiandri added a commit that referenced this pull request Oct 18, 2026
…FSharp.Core `Map` module

Follows the reworked ADR 0001 (D2, D12, D13) and the review of #30:

- `HashMap`, `IMap` and their `Option` and `Tuple` modules are the root modules of their
  files. The `HashMap` and `IMap` abbreviations move to `HashMap/HashMapTypes.fs`.
- `CustomComparersHashMap<'Key, 'Value>` holds the key comparer and the value comparer, or
  the key comparer and the default value comparer, and creates hash maps with them through
  the members `empty`, `singleton`, `ofArray`, `ofList`, `ofSeq`, `ofSeqBy` and `builder`.
  A `null` comparer throws `ArgumentNullException` with the name of its parameter, and the
  default value of the struct stands for the default comparers. `ofSeq` gives an existing
  map the comparers of the struct, without a copy when the key comparer stays.
- `HashMap.forComparers` and `HashMap.forKeyComparer` create the struct. They replace
  `emptyWithComparers`, `emptyWithKeyComparer`, `ofKeyValuePairsWithKeyComparer`,
  `ofSeqWithKeyComparer`, `builderWithComparers`, `builderWithKeyComparer`,
  `withComparers` and `withKeyComparer`.
- `HashMapExtensions` extends `ImmutableDictionary<'Key, 'Value>` with static `ofArray`,
  `ofList` and `ofSeq`, each overloaded for `KeyValuePair`s, reference tuples and struct
  tuples: the members of the struct with the default comparers. The module loses `ofArray`,
  `ofList`, `ofSeq` and `ofKeyValuePairs`, so that `HashMap.ofSeq pairs` reaches the
  overloads, also in a pipeline. Every shape keeps the last binding of a key, as `Map.ofSeq`
  does.
- `addRange` of `HashMap` and `IMap` takes `KeyValuePair`s, as `setItems` does; the nested
  modules `Tuple` and `ValueTuple` hold both functions for reference and struct tuples. The
  modules of the `Tuple` namespace keep `partition`, `toArray`, `toList` and `toSeq`.
- `Dictionaries` in `Helper.fs` gets the deconstructors of the binding shapes,
  `splitKeyValuePair` and `splitTuple`, for every map module and comparer struct.
- `HashMapCE` rejects a `null` key comparer.

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

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 redesign needs human confirmation of compatibility and build/test results across both target frameworks.

No open issues 🎉

Review effort: Balanced

Base automatically changed from feat/flat-list to main October 18, 2026 22:34
xperiandri added a commit that referenced this pull request Oct 18, 2026
…FSharp.Core `Map` module

Follows the reworked ADR 0001 (D2, D12, D13) and the review of #30:

- `HashMap`, `IMap` and their `Option` and `Tuple` modules are the root modules of their
  files. The `HashMap` and `IMap` abbreviations move to `HashMap/HashMapTypes.fs`.
- `CustomComparersHashMap<'Key, 'Value>` holds the key comparer and the value comparer, or
  the key comparer and the default value comparer, and creates hash maps with them through
  the members `empty`, `singleton`, `ofArray`, `ofList`, `ofSeq`, `ofSeqBy` and `builder`.
  A `null` comparer throws `ArgumentNullException` with the name of its parameter, and the
  default value of the struct stands for the default comparers. `ofSeq` gives an existing
  map the comparers of the struct, without a copy when the key comparer stays.
- `HashMap.forComparers` and `HashMap.forKeyComparer` create the struct. They replace
  `emptyWithComparers`, `emptyWithKeyComparer`, `ofKeyValuePairsWithKeyComparer`,
  `ofSeqWithKeyComparer`, `builderWithComparers`, `builderWithKeyComparer`,
  `withComparers` and `withKeyComparer`.
- `HashMapExtensions` extends `ImmutableDictionary<'Key, 'Value>` with static `ofArray`,
  `ofList` and `ofSeq`, each overloaded for `KeyValuePair`s, reference tuples and struct
  tuples: the members of the struct with the default comparers. The module loses `ofArray`,
  `ofList`, `ofSeq` and `ofKeyValuePairs`, so that `HashMap.ofSeq pairs` reaches the
  overloads, also in a pipeline. Every shape keeps the last binding of a key, as `Map.ofSeq`
  does.
- `addRange` of `HashMap` and `IMap` takes `KeyValuePair`s, as `setItems` does; the nested
  modules `Tuple` and `ValueTuple` hold both functions for reference and struct tuples. The
  modules of the `Tuple` namespace keep `partition`, `toArray`, `toList` and `toSeq`.
- `Dictionaries` in `Helper.fs` gets the deconstructors of the binding shapes,
  `splitKeyValuePair` and `splitTuple`, for every map module and comparer struct.
- `HashMapCE` rejects a `null` key comparer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Needs a closer look

The broad breaking API rewrite needs human compatibility review and confirmation of multi-target build and test results.

No open issues 🎉

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

Specialized yield! overloads bypass null validation and produce inconsistent exceptions.

No open issues 🎉

1 previously missed issue

In code that hasn't changed since last review

Medium severity Add null checks to specialized YieldFrom overloads

src/​FSharp.Collections.Immutable/​HashMap/​HashMapCE.fs:50

Both specialized YieldFrom overloads—the struct-pair array here and the HashMap overload below—skip the null checks performed by the sequence overloads. A null input therefore throws NullReferenceException during enumeration instead of a named ArgumentNullException; changing only its static type to a sequence changes the exception. Check pairs and table before returning their buffers, and add both overloads to the null-input tests.

Review effort: Balanced

xperiandri added a commit that referenced this pull request Oct 19, 2026
…FSharp.Core `Map` module

Follows the reworked ADR 0001 (D2, D12, D13) and the review of #30:

- `HashMap`, `IMap` and their `Option` and `Tuple` modules are the root modules of their
  files. The `HashMap` and `IMap` abbreviations move to `HashMap/HashMapTypes.fs`.
- `CustomComparersHashMap<'Key, 'Value>` holds the key comparer and the value comparer, or
  the key comparer and the default value comparer, and creates hash maps with them through
  the members `empty`, `singleton`, `ofArray`, `ofList`, `ofSeq`, `ofSeqBy` and `builder`.
  A `null` comparer throws `ArgumentNullException` with the name of its parameter, and the
  default value of the struct stands for the default comparers. `ofSeq` gives an existing
  map the comparers of the struct, without a copy when the key comparer stays.
- `HashMap.forComparers` and `HashMap.forKeyComparer` create the struct. They replace
  `emptyWithComparers`, `emptyWithKeyComparer`, `ofKeyValuePairsWithKeyComparer`,
  `ofSeqWithKeyComparer`, `builderWithComparers`, `builderWithKeyComparer`,
  `withComparers` and `withKeyComparer`.
- `HashMapExtensions` extends `ImmutableDictionary<'Key, 'Value>` with static `ofArray`,
  `ofList` and `ofSeq`, each overloaded for `KeyValuePair`s, reference tuples and struct
  tuples: the members of the struct with the default comparers. The module loses `ofArray`,
  `ofList`, `ofSeq` and `ofKeyValuePairs`, so that `HashMap.ofSeq pairs` reaches the
  overloads, also in a pipeline. Every shape keeps the last binding of a key, as `Map.ofSeq`
  does.
- `addRange` of `HashMap` and `IMap` takes `KeyValuePair`s, as `setItems` does; the nested
  modules `Tuple` and `ValueTuple` hold both functions for reference and struct tuples. The
  modules of the `Tuple` namespace keep `partition`, `toArray`, `toList` and `toSeq`.
- `Dictionaries` in `Helper.fs` gets the deconstructors of the binding shapes,
  `splitKeyValuePair` and `splitTuple`, for every map module and comparer struct.
- `HashMapCE` rejects a `null` key comparer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@xperiandri
xperiandri changed the base branch from main to build/assembly-info-props October 19, 2026 14:44
@xperiandri
xperiandri force-pushed the build/assembly-info-props branch from 758443d to 4562f5a Compare October 19, 2026 14:46
xperiandri added a commit that referenced this pull request Oct 19, 2026
…FSharp.Core `Map` module

Follows the reworked ADR 0001 (D2, D12, D13) and the review of #30:

- `HashMap`, `IMap` and their `Option` and `Tuple` modules are the root modules of their
  files. The `HashMap` and `IMap` abbreviations move to `HashMap/HashMapTypes.fs`.
- `CustomComparersHashMap<'Key, 'Value>` holds the key comparer and the value comparer, or
  the key comparer and the default value comparer, and creates hash maps with them through
  the members `empty`, `singleton`, `ofArray`, `ofList`, `ofSeq`, `ofSeqBy` and `builder`.
  A `null` comparer throws `ArgumentNullException` with the name of its parameter, and the
  default value of the struct stands for the default comparers. `ofSeq` gives an existing
  map the comparers of the struct, without a copy when the key comparer stays.
- `HashMap.forComparers` and `HashMap.forKeyComparer` create the struct. They replace
  `emptyWithComparers`, `emptyWithKeyComparer`, `ofKeyValuePairsWithKeyComparer`,
  `ofSeqWithKeyComparer`, `builderWithComparers`, `builderWithKeyComparer`,
  `withComparers` and `withKeyComparer`.
- `HashMapExtensions` extends `ImmutableDictionary<'Key, 'Value>` with static `ofArray`,
  `ofList` and `ofSeq`, each overloaded for `KeyValuePair`s, reference tuples and struct
  tuples: the members of the struct with the default comparers. The module loses `ofArray`,
  `ofList`, `ofSeq` and `ofKeyValuePairs`, so that `HashMap.ofSeq pairs` reaches the
  overloads, also in a pipeline. Every shape keeps the last binding of a key, as `Map.ofSeq`
  does.
- `addRange` of `HashMap` and `IMap` takes `KeyValuePair`s, as `setItems` does; the nested
  modules `Tuple` and `ValueTuple` hold both functions for reference and struct tuples. The
  modules of the `Tuple` namespace keep `partition`, `toArray`, `toList` and `toSeq`.
- `Dictionaries` in `Helper.fs` gets the deconstructors of the binding shapes,
  `splitKeyValuePair` and `splitTuple`, for every map module and comparer struct.
- `HashMapCE` rejects a `null` key comparer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri added a commit that referenced this pull request Oct 19, 2026
…FSharp.Core `Map` module

Follows the reworked ADR 0001 (D2, D12, D13) and the review of #30:

- `HashMap`, `IMap` and their `Option` and `Tuple` modules are the root modules of their
  files. The `HashMap` and `IMap` abbreviations move to `HashMap/HashMapTypes.fs`.
- `CustomComparersHashMap<'Key, 'Value>` holds the key comparer and the value comparer, or
  the key comparer and the default value comparer, and creates hash maps with them through
  the members `empty`, `singleton`, `ofArray`, `ofList`, `ofSeq`, `ofSeqBy` and `builder`.
  A `null` comparer throws `ArgumentNullException` with the name of its parameter, and the
  default value of the struct stands for the default comparers. `ofSeq` gives an existing
  map the comparers of the struct, without a copy when the key comparer stays.
- `HashMap.forComparers` and `HashMap.forKeyComparer` create the struct. They replace
  `emptyWithComparers`, `emptyWithKeyComparer`, `ofKeyValuePairsWithKeyComparer`,
  `ofSeqWithKeyComparer`, `builderWithComparers`, `builderWithKeyComparer`,
  `withComparers` and `withKeyComparer`.
- `HashMapExtensions` extends `ImmutableDictionary<'Key, 'Value>` with static `ofArray`,
  `ofList` and `ofSeq`, each overloaded for `KeyValuePair`s, reference tuples and struct
  tuples: the members of the struct with the default comparers. The module loses `ofArray`,
  `ofList`, `ofSeq` and `ofKeyValuePairs`, so that `HashMap.ofSeq pairs` reaches the
  overloads, also in a pipeline. Every shape keeps the last binding of a key, as `Map.ofSeq`
  does.
- `addRange` of `HashMap` and `IMap` takes `KeyValuePair`s, as `setItems` does; the nested
  modules `Tuple` and `ValueTuple` hold both functions for reference and struct tuples. The
  modules of the `Tuple` namespace keep `partition`, `toArray`, `toList` and `toSeq`.
- `Dictionaries` in `Helper.fs` gets the deconstructors of the binding shapes,
  `splitKeyValuePair` and `splitTuple`, for every map module and comparer struct.
- `HashMapCE` rejects a `null` key comparer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Base automatically changed from build/assembly-info-props to main October 19, 2026 15:14
xperiandri added a commit that referenced this pull request Oct 19, 2026
…FSharp.Core `Map` module

Follows the reworked ADR 0001 (D2, D12, D13) and the review of #30:

- `HashMap`, `IMap` and their `Option` and `Tuple` modules are the root modules of their
  files. The `HashMap` and `IMap` abbreviations move to `HashMap/HashMapTypes.fs`.
- `CustomComparersHashMap<'Key, 'Value>` holds the key comparer and the value comparer, or
  the key comparer and the default value comparer, and creates hash maps with them through
  the members `empty`, `singleton`, `ofArray`, `ofList`, `ofSeq`, `ofSeqBy` and `builder`.
  A `null` comparer throws `ArgumentNullException` with the name of its parameter, and the
  default value of the struct stands for the default comparers. `ofSeq` gives an existing
  map the comparers of the struct, without a copy when the key comparer stays.
- `HashMap.forComparers` and `HashMap.forKeyComparer` create the struct. They replace
  `emptyWithComparers`, `emptyWithKeyComparer`, `ofKeyValuePairsWithKeyComparer`,
  `ofSeqWithKeyComparer`, `builderWithComparers`, `builderWithKeyComparer`,
  `withComparers` and `withKeyComparer`.
- `HashMapExtensions` extends `ImmutableDictionary<'Key, 'Value>` with static `ofArray`,
  `ofList` and `ofSeq`, each overloaded for `KeyValuePair`s, reference tuples and struct
  tuples: the members of the struct with the default comparers. The module loses `ofArray`,
  `ofList`, `ofSeq` and `ofKeyValuePairs`, so that `HashMap.ofSeq pairs` reaches the
  overloads, also in a pipeline. Every shape keeps the last binding of a key, as `Map.ofSeq`
  does.
- `addRange` of `HashMap` and `IMap` takes `KeyValuePair`s, as `setItems` does; the nested
  modules `Tuple` and `ValueTuple` hold both functions for reference and struct tuples. The
  modules of the `Tuple` namespace keep `partition`, `toArray`, `toList` and `toSeq`.
- `Dictionaries` in `Helper.fs` gets the deconstructors of the binding shapes,
  `splitKeyValuePair` and `splitTuple`, for every map module and comparer struct.
- `HashMapCE` rejects a `null` key comparer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri added a commit that referenced this pull request Oct 19, 2026
…FSharp.Core `Map` module

Follows the reworked ADR 0001 (D2, D12, D13) and the review of #30:

- `HashMap`, `IMap` and their `Option` and `Tuple` modules are the root modules of their
  files. The `HashMap` and `IMap` abbreviations move to `HashMap/HashMapTypes.fs`.
- `CustomComparersHashMap<'Key, 'Value>` holds the key comparer and the value comparer, or
  the key comparer and the default value comparer, and creates hash maps with them through
  the members `empty`, `singleton`, `ofArray`, `ofList`, `ofSeq`, `ofSeqBy` and `builder`.
  A `null` comparer throws `ArgumentNullException` with the name of its parameter, and the
  default value of the struct stands for the default comparers. `ofSeq` gives an existing
  map the comparers of the struct, without a copy when the key comparer stays.
- `HashMap.forComparers` and `HashMap.forKeyComparer` create the struct. They replace
  `emptyWithComparers`, `emptyWithKeyComparer`, `ofKeyValuePairsWithKeyComparer`,
  `ofSeqWithKeyComparer`, `builderWithComparers`, `builderWithKeyComparer`,
  `withComparers` and `withKeyComparer`.
- `HashMapExtensions` extends `ImmutableDictionary<'Key, 'Value>` with static `ofArray`,
  `ofList` and `ofSeq`, each overloaded for `KeyValuePair`s, reference tuples and struct
  tuples: the members of the struct with the default comparers. The module loses `ofArray`,
  `ofList`, `ofSeq` and `ofKeyValuePairs`, so that `HashMap.ofSeq pairs` reaches the
  overloads, also in a pipeline. Every shape keeps the last binding of a key, as `Map.ofSeq`
  does.
- `addRange` of `HashMap` and `IMap` takes `KeyValuePair`s, as `setItems` does; the nested
  modules `Tuple` and `ValueTuple` hold both functions for reference and struct tuples. The
  modules of the `Tuple` namespace keep `partition`, `toArray`, `toList` and `toSeq`.
- `Dictionaries` in `Helper.fs` gets the deconstructors of the binding shapes,
  `splitKeyValuePair` and `splitTuple`, for every map module and comparer struct.
- `HashMapCE` rejects a `null` key comparer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri added a commit that referenced this pull request Oct 19, 2026
…FSharp.Core `Map` module

Follows the reworked ADR 0001 (D2, D12, D13) and the review of #30:

- `HashMap`, `IMap` and their `Option` and `Tuple` modules are the root modules of their
  files. The `HashMap` and `IMap` abbreviations move to `HashMap/HashMapTypes.fs`.
- `CustomComparersHashMap<'Key, 'Value>` holds the key comparer and the value comparer, or
  the key comparer and the default value comparer, and creates hash maps with them through
  the members `empty`, `singleton`, `ofArray`, `ofList`, `ofSeq`, `ofSeqBy` and `builder`.
  A `null` comparer throws `ArgumentNullException` with the name of its parameter, and the
  default value of the struct stands for the default comparers. `ofSeq` gives an existing
  map the comparers of the struct, without a copy when the key comparer stays.
- `HashMap.forComparers` and `HashMap.forKeyComparer` create the struct. They replace
  `emptyWithComparers`, `emptyWithKeyComparer`, `ofKeyValuePairsWithKeyComparer`,
  `ofSeqWithKeyComparer`, `builderWithComparers`, `builderWithKeyComparer`,
  `withComparers` and `withKeyComparer`.
- `HashMapExtensions` extends `ImmutableDictionary<'Key, 'Value>` with static `ofArray`,
  `ofList` and `ofSeq`, each overloaded for `KeyValuePair`s, reference tuples and struct
  tuples: the members of the struct with the default comparers. The module loses `ofArray`,
  `ofList`, `ofSeq` and `ofKeyValuePairs`, so that `HashMap.ofSeq pairs` reaches the
  overloads, also in a pipeline. Every shape keeps the last binding of a key, as `Map.ofSeq`
  does.
- `addRange` of `HashMap` and `IMap` takes `KeyValuePair`s, as `setItems` does; the nested
  modules `Tuple` and `ValueTuple` hold both functions for reference and struct tuples. The
  modules of the `Tuple` namespace keep `partition`, `toArray`, `toList` and `toSeq`.
- `Dictionaries` in `Helper.fs` gets the deconstructors of the binding shapes,
  `splitKeyValuePair` and `splitTuple`, for every map module and comparer struct.
- `HashMapCE` rejects a `null` key comparer.

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

Rewrites `HashMap` after ADR 0001 in `HashMap/HashMap.fsi` and
`HashMap/HashMap.fs`, and adds `IMap` for any
`IImmutableDictionary<'Key, 'Value>` in `HashMap/IMap.fsi` and
`HashMap/IMap.fs`, each function documented in the signature file.

- The functions with a name of `Map` have its semantics and
  exceptions: `add` replaces the value of a key, `filter` and `map`
  return maps, `find` and `pick` throw the `KeyNotFoundException` of
  `Map`, and `findKey` the one of `Seq.pick`. The wrappers of
  `ImmutableDictionary` members keep the semantics of the BCL, so
  `addRange`, `ofKeyValuePairs`, `ofSeqBy` and `ofSeqWithKeyComparer`
  reject a repeated key with another value.
- Every transformation keeps the key comparer, the ones that keep the
  type of the values keep the value comparer, and `map` and `choose`
  give the new values the default comparer. `IMap` keeps the
  implementation of the map through `Clear` and `AddRange`; its `map`
  and `choose` return a `HashMap` with the default comparers.
- `try*` functions return `voption`, tuples are struct tuples, and the
  `Option` and `Tuple` namespaces repeat those functions with options
  and reference tuples.
- `hashMap { }` collects `yield struct (key, value)` and
  `yield KeyValuePair` in a builder; `HashMapCE` takes a key comparer.
- `Helper.fs` gets `Raise.keyNotFound` and the `Dictionaries` module:
  the algorithms over the bindings that the other map modules will
  share.
- The legacy `HashMap` module and the `HashMap`, `HashMapBuilder` and
  `IMap` abbreviations leave `Maps.fs`, which keeps `SortedMap`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- `MapOps`, `MapTransformOps`, `MapMutationOps` and `MapCreationOps`
  record the functions of a map module, and `MapContract`,
  `MapTransformContract`, `MapMutationContract` and
  `MapCreationContract` are the facets on `MapContractBase`.
- Expected values come from the FSharp.Core `Map` module and compare as
  bindings sorted by key, because a hash map enumerates in an
  unspecified order; the functions that visit the bindings one by one
  are checked against the order in which the map enumerates them.
- Expected exceptions come from `Map` through `ExceptionAssert.same`,
  so `Mirror` stays about the sequence-like modules.
- `HashMap` derives all four facets and `IMap` all but the creation
  one, over hash maps. Their own tests cover null inputs, comparers,
  results that share the input, the builder functions, the computation
  expression and the `Option` and `Tuple` namespaces.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the tutorial

The tutorial creates its hash map from struct tuples, which `HashMap.ofSeq`
takes now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants