You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
feat: HashMap and IMap modules with the vocabulary of the FSharp.Core Map module - #30
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.
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:
addRange and setItems take the same shape, KeyValuePairs, and the nested modules Tuple and ValueTuple take the tuples.
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.
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).
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.
…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>
…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>
…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>
…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>
…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>
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.
…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>
…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>
…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>
…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>
…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>
…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>
…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 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
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
HashMap(ImmutableDictionary<'Key, 'Value>) and adds the interface moduleIMap(IImmutableDictionary<'Key, 'Value>) after ADR 0001 (#23). It builds on #26 (FlatList), which is merged intomain.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 abbreviationsHashMap<'Key, 'Value>andIMap<'Key, 'Value>.HashMap,IMapand theirOptionandTuplemodules are the root modules of their files (D2), and a root module cannot declare a type of its namespace.HashMap/HashMap.fsiandHashMap.fs: 48 functions, in the canonical category order, and the nested modulesTupleandValueTuple. They are theHashMapcolumn of ADR Appendix B, with the parameter names of the FSharp.CoreMapmodule (key,value,table,elements,fand so on) andspanfor a span, as inFlatList.partition,toArray,toListandtoSeqreturn struct tuples (D3).build,builder,ofBuilder,toBuilderandupdate,add,addRange,containsBinding,containsValue,ofReadOnlySpan,ofSeqBy,removeRange,setItem,setItemsandtryGetKey.addis the BCLAdd, 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 throwsArgumentException, an equal binding returns the input itself, and a new key is added.Map.addreplaces the value, andsetItemis the function that does;changeadds a binding only when the key has none. There is noaddNew.containsBinding key value tablewrapsContainsof aKeyValuePair, so the value comparer of the map decides whether the values are equal;tryFindfollowed by=compares with F# equality instead.IMaphas it too, where the comparers are out of reach.ofReadOnlySpan spanandofSpan spancreate a map from aReadOnlySpanor aSpanofKeyValuePair<_, _>(CreateRangeWithOverwrite, which takes a read-only span only); a repeated key never throws. The pair exists because F# converts aSpanto the parameter ofofReadOnlySpanonly when it has inferred the types of the call (otherwise FS0001), asFlatListhasofReadOnlySpanandofSpan. An array converts to either parameter under the same condition.build actionandupdate action tablefill or change a builder with an action, asFlatList.buildandupdatedo.updatereturns the input itself when the action changes nothing.addRangeandsetItemstakeKeyValuePairs. The nested modules hold them for reference and struct tuples (D13):HashMap.Tuple.addRange pairs table,HashMap.ValueTuple.setItems pairs table.forComparersandforKeyComparercreate the comparer struct.chooseis an extra.HashMap/HashMap.CustomComparers.fsiand.fs(D12): the structCustomComparersHashMap<'Key, 'Value>holds the key comparer and the value comparer,(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,ofReadOnlySpan,ofSpan,ofSeqBy,buildandbuilder.HashMap.forComparers keyComparer valueComparerandHashMap.forKeyComparer keyComparercreate it:(HashMap.forKeyComparer StringComparer.OrdinalIgnoreCase).ofSeq pairs.nullcomparer throwsArgumentNullExceptionwith the name of its parameter, in the constructors and in thefor*functions. The default value of the struct stands for the default comparers.ofReadOnlySpanwrapsCreateRangeWithOverwritefor 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.ofSpanpasses its span toofReadOnlySpan.ofSeqgives an existing map the comparers of the struct, which is how to change the comparers of a map. It differs fromWithComparersin 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 whereWithComparersthrows, andforKeyComparerresets the value comparer.HashMap/HashMap.Extensions.fsiand.fs(D13): the auto-opened moduleHashMapExtensionsextendsImmutableDictionary<'Key, 'Value>with staticofArray,ofListandofSeq, each overloaded forKeyValuePairs, reference tuples and struct tuples.HashMapmodule has no function with their names, soHashMap.ofSeq pairsreaches the overloads, also aspairs |> HashMap.ofSeq. Lists, arrays,dict, an F#Mapand an existing hash map resolve.ImmutableDictionary. It calls the public static methodsHashMapExtensions.OfArray,OfListandOfSeq(checked by reflection) or uses the struct. A reflection over theHashMapmodule does not list them.CreateRangerejects another value of it. The rejecting form isHashMap.empty |> HashMap.addRange pairs.DependentUponin the project file, the.fsionHashMap.fsiand the.fsonHashMap.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.fsiandIMap.fs: 33 functions overIImmutableDictionary, and the nested modulesTupleandValueTuplewithaddRangeandsetItems.addis theAddof the interface andcontainsBindingitsContainsof aKeyValuePair; the latter is the one member of the interface that no other function reaches. The interface declares no creation, builder, comparer orcontainsValuemembers, so the module has no such functions.filterandpartitionkeep the implementation of the map (Clear ()plusAddRange). When every binding stays, they return the input itself.mapandchoosereturn aHashMapwith the default comparers.ImmutableDictionary.SetItem,Add,AddRange,ToImmutableDictionaryand the indexer of the builder do. This holds foradd,change,setItem,setItems,addRange, theirTuple,ValueTupleandOptionvariants,IMap, the creation functions and members,ofReadOnlySpan,ofSpan,buildandhashMap { }. The functions that set one binding then return the input itself;addthrows for another value. The remarks of every affected function and member say so,IMap.mapandIMap.chooseincluded, where keys that only the default comparer finds equal keep the first of equal results.ofArray,ofList,ofSeq,ofReadOnlySpanandofSpankeep 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).add,ofSeqByandaddRangekeep the behavior ofImmutableDictionary. A repeated key with an equal value is accepted and keeps the first binding; a repeated key with another value throwsArgumentException.setItemandsetItemsreplace the value.findandpickthrowKeyNotFoundExceptionasMapdoes.findKeythrows the message thatMap.findKeygets fromSeq.pick.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:ofSeqBy,addRange,removeRangeandsetItemsofHashMapand ofIMap, theTupleandValueTuplemodules of both, theofSeqandofSeqBymembers of the struct (the extension overloads call them), andyield!andforofhashMap { }, which check when the member runs and not when the buffer does;yield!of an array and of a hash map checks fornullthe same way, withArgumentNullExceptionthat namespairsortable. Arrays keep their null check.foldBack:foldBackvisits the bindings in the reverse order of their enumeration. Its remarks say so.HashMapOptionandIMapOptioncontainchange,choose,pick,tryFind,tryFindKey,tryGetKeyandtryPickwith options.HashMapTupleandIMapTuplecontainpartition,toArray,toListandtoSeqwith reference tuples. The functions that take bindings need no counterpart there, and the nested modulesTupleandValueTupleresolve afteropen FSharp.Collections.Immutable.Tupletoo.hashMap { }yieldsstruct (key, value)orKeyValuePair, and a later binding of a key wins unless its value is equal (D14).HashMapCE keyComparer { }takes a key comparer, and anullone throwsArgumentNullException.HashMapCE<'Key>is generic in the key type, because its comparer has that type, sohashMapis a type function:[<GeneralizableValue>] let hashMap<'Key when 'Key : not null> = HashMapCE<'Key> ().HashMap/HashMapCE.fsi, compiled right beforeHashMapCE.fs, carries the documentation of the builder;Usingtakes'Resource :> IDisposable | null.ComputationExpressions.fsideclareshashMapafterflatList, under theCreationbanner.mainthat the struct replaces:emptyWithComparers,emptyWithKeyComparer,ofKeyValuePairs,ofKeyValuePairsWithKeyComparer,ofSeqWithKeyComparer,builderWithComparers,builderWithKeyComparer,withComparersandwithKeyComparer. The published 1.0.0 did not have them, so the changelog does not list them as removed. The struct replaces them, andofKeyValuePairsis theKeyValuePairoverload ofofSeq.addRangeandsetItemstake the same shape,KeyValuePairs, and the nested modulesTupleandValueTupletake the tuples.ofSeqBykeeps the behavior of the BCL.HashMap.keysandvaluesreturnICollection<_>, asMap.keysandMap.valuesdo.IMap.keysandvaluesreturnseq<_>, because the interface only exposesIReadOnlyDictionary.Keys(ADR Appendix B).addworks as in the BCL, as it did in 1.0.0: it throws for a key bound to another value, andsetItemreplaces. The first version of this rewrite madeaddreplace, which was a mistake; the changelog no longer says thataddchanged.Helper/:Raise.keyNotFoundis new (Helper/Raise.fs).Dictionaries, inHelper/Dictionaries.fs, holds the algorithms overseq<KeyValuePair>andIDictionaryand the deconstructors of the binding shapes,splitKeyValuePairandsplitTuple, so thatSortedMap,FrozenMapand the interface modules can reuse them.Maps.fs: only the legacySortedMapremains. The legacyHashMapmodule and theHashMap,HashMapBuilderandIMapabbreviations are removed.docsSrc/Tutorials/Getting_Started.mdcreates its hash map from reference tuples and showsHashMap.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:ofSeqof the struct andWithComparers, with an example, and a pointer fromforComparersandforKeyComparer;HashMap.empty |> HashMap.addRange pairs, and the recipe forToImmutableDictionarywith an element selector;TryGetValue(tryFind),GetValueOrDefault(tryFindfollowed byValueOption.defaultValue) andTryAdd(change);IMaphas only what the interface declares;length,append,except,checkandHashMapBuilder), each with its replacement, and the three functions whose behavior differs from 1.0.0 (tryFind,ofSeqandfilter); both lists were checked against the published 1.0.0 package.Tests: 4057 per target framework, 1297 of them for
HashMap(805) andIMap(492).DataRowandDynamicDatarows, 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 oftoArray, and the comparers ofmapandchooseare a row each.Contracts/(MapOps.fs,MapInputs.fs,MapContract,MapTransformContract,MapMutationContractandMapCreationContract) compare every function with FSharp.CoreMap.MapInputsis a static class whose properties build new arrays on each read.findKey,tryFindKey,pick,tryPick,fold,foldBack,iter,toArray,toSeq,keysandvalues;toArrayis compared with the map's own enumeration of key/value pairs, because a hash map does not keep the order of its input.Map, except that ofadd, which is compared with the BCL member that it wraps.change, binding shapes and the implementations behindIMapare union cases, and the tables of functions are F# maps whose keys are the rows.ofArray,ofListandofSeqin the three shapes on every input, the bindings that repeat a key included. It takesdict, an F#Mapand an existing map as sources ofKeyValuePairs, and checks that every created map finds its keys with its key comparer.addRangeandsetItemsin the three shapes: the module forKeyValuePairs, the nested modules for tuples. It comparesaddwithMap.addonly for keys without a binding; for the keys of the input it checks the BCL result, the input itself for an equal binding andArgumentExceptionfor another value.containsBindingon 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, wheretryFindand=disagree with the BCL, on both implementations ofIMap, and check that a null key is left to the key comparer.HashMapderives all four facets, and the creation facet a second time forCustomComparersHashMapwith a key comparer.IMapderives all of them except creation.IMap, every member of the struct, every shape of the extension members and every member ofhashMap { }that sets a binding, with a value comparer that finds distinct values equal or with copies of strings that the default comparer finds equal.addhas a row for the equal value (the input itself) and for another value (it throws).ofReadOnlySpanandofSpanare 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.buildandupdateare tested for the bindings, the comparers, the input that stays unchanged and the null table.nulland for a default flat list, for the module,IMap, the members of the struct andhashMap { }; 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 asseq<obj>has a row per module (FlatListOracles.defaultStringsAsObjects), and a flat list that is not default is checked to pass.forKeyComparer StringComparer.OrdinalIgnoreCase, the last binding in every shape, pipelines and the nested modules afteropen FSharp.Collections.Immutable.Tuple.Types of changes
What types of changes does your code introduce to FSharp.Collections.Immutable?
Put an
xin the boxes that applyThese changes break existing code; the list compares with the published 1.0.0, as the changelog does:
tryFindreturns avoption, where it returned anoption.ofSeqkeeps the last binding of a repeated key instead of throwing.filterreturns the bindings that satisfy the predicate, where it returned all the bindings.length,append,except,checkand theHashMapBuilderabbreviation are gone; the Removed section of the changelog names their replacements.HashMapExtensionsor through the struct.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
ADR 0001, D14 says that a sorted map also keeps the stored key for an equal value. A hash map keeps it too:
ImmutableDictionary.SetItemand the indexer of its builder leave the whole binding as it is, so the documentation ofHashMapsays "its key included" as well.🤖 Generated with Claude Code