From 5361e79e0b2612c328c8e52a4c67eaed16d3a6ac Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Sun, 18 Oct 2026 13:02:00 +0200 Subject: [PATCH 1/9] feat: `HashMap` and `IMap` modules with the vocabulary of the FSharp.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 --- .../ComputationExpressions.fs | 3 + .../ComputationExpressions.fsi | 22 + .../FSharp.Collections.Immutable.fsproj | 28 + .../HashMap/HashMap.CustomComparers.fs | 147 +++ .../HashMap/HashMap.CustomComparers.fsi | 338 +++++++ .../HashMap/HashMap.Extensions.fs | 42 + .../HashMap/HashMap.Extensions.fsi | 163 +++ .../HashMap/HashMap.fs | 346 +++++++ .../HashMap/HashMap.fsi | 951 ++++++++++++++++++ .../HashMap/HashMapCE.fs | 123 +++ .../HashMap/HashMapCE.fsi | 143 +++ .../HashMap/HashMapOption.fs | 48 + .../HashMap/HashMapOption.fsi | 120 +++ .../HashMap/HashMapTuple.fs | 31 + .../HashMap/HashMapTuple.fsi | 69 ++ .../HashMap/HashMapTypes.fs | 24 + .../HashMap/IMap.fs | 286 ++++++ .../HashMap/IMap.fsi | 646 ++++++++++++ .../HashMap/IMapOption.fs | 45 + .../HashMap/IMapOption.fsi | 123 +++ .../HashMap/IMapTuple.fs | 31 + .../HashMap/IMapTuple.fsi | 67 ++ .../Helper/Dictionaries.fs | 176 ++++ .../Helper/Raise.fs | 4 + src/FSharp.Collections.Immutable/Maps.fs | 161 --- 25 files changed, 3976 insertions(+), 161 deletions(-) create mode 100644 src/FSharp.Collections.Immutable/HashMap/HashMap.CustomComparers.fs create mode 100644 src/FSharp.Collections.Immutable/HashMap/HashMap.CustomComparers.fsi create mode 100644 src/FSharp.Collections.Immutable/HashMap/HashMap.Extensions.fs create mode 100644 src/FSharp.Collections.Immutable/HashMap/HashMap.Extensions.fsi create mode 100644 src/FSharp.Collections.Immutable/HashMap/HashMap.fs create mode 100644 src/FSharp.Collections.Immutable/HashMap/HashMap.fsi create mode 100644 src/FSharp.Collections.Immutable/HashMap/HashMapCE.fs create mode 100644 src/FSharp.Collections.Immutable/HashMap/HashMapCE.fsi create mode 100644 src/FSharp.Collections.Immutable/HashMap/HashMapOption.fs create mode 100644 src/FSharp.Collections.Immutable/HashMap/HashMapOption.fsi create mode 100644 src/FSharp.Collections.Immutable/HashMap/HashMapTuple.fs create mode 100644 src/FSharp.Collections.Immutable/HashMap/HashMapTuple.fsi create mode 100644 src/FSharp.Collections.Immutable/HashMap/HashMapTypes.fs create mode 100644 src/FSharp.Collections.Immutable/HashMap/IMap.fs create mode 100644 src/FSharp.Collections.Immutable/HashMap/IMap.fsi create mode 100644 src/FSharp.Collections.Immutable/HashMap/IMapOption.fs create mode 100644 src/FSharp.Collections.Immutable/HashMap/IMapOption.fsi create mode 100644 src/FSharp.Collections.Immutable/HashMap/IMapTuple.fs create mode 100644 src/FSharp.Collections.Immutable/HashMap/IMapTuple.fsi create mode 100644 src/FSharp.Collections.Immutable/Helper/Dictionaries.fs diff --git a/src/FSharp.Collections.Immutable/ComputationExpressions.fs b/src/FSharp.Collections.Immutable/ComputationExpressions.fs index 6db0b059..5a17dfba 100644 --- a/src/FSharp.Collections.Immutable/ComputationExpressions.fs +++ b/src/FSharp.Collections.Immutable/ComputationExpressions.fs @@ -4,3 +4,6 @@ module FSharp.Collections.Immutable.ComputationExpressions // ---- Creation ---- let flatList = FlatListCE () + +[] +let hashMap<'Key when 'Key : not null> = HashMapCE<'Key>() diff --git a/src/FSharp.Collections.Immutable/ComputationExpressions.fsi b/src/FSharp.Collections.Immutable/ComputationExpressions.fsi index c0086e56..7cfa666f 100644 --- a/src/FSharp.Collections.Immutable/ComputationExpressions.fsi +++ b/src/FSharp.Collections.Immutable/ComputationExpressions.fsi @@ -19,3 +19,25 @@ module FSharp.Collections.Immutable.ComputationExpressions /// Evaluates to a flat list of 0, 1, 4, 9, 100 and 200. /// val flatList : FlatListCE + +/// +/// Builds a hash map with the default comparers. +/// +/// +/// A later binding of a key replaces an earlier one, unless the default comparer of the values finds the two values +/// equal: then the earlier binding stays, its key included (ADR 0001, D14). The instance is generic in the type of the +/// keys, which each expression infers; takes a key comparer. +/// +/// +/// +/// let squares = hashMap { +/// for i in 1..3 do +/// yield struct (i, i * i) +/// yield KeyValuePair (10, 100) +/// yield struct (1, -1) +/// } +/// +/// Evaluates to a hash map of 1 to -1, 2 to 4, 3 to 9 and 10 to 100. +/// +[] +val hashMap<'Key when 'Key : not null> : HashMapCE<'Key> diff --git a/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj b/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj index 293de773..2ed46392 100644 --- a/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj +++ b/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj @@ -33,6 +33,7 @@ + @@ -44,6 +45,33 @@ + + + HashMap.fsi + + + HashMap.fs + + + + + + + + + HashMap.fsi + + + HashMap.fs + + + + + + + + + diff --git a/src/FSharp.Collections.Immutable/HashMap/HashMap.CustomComparers.fs b/src/FSharp.Collections.Immutable/HashMap/HashMap.CustomComparers.fs new file mode 100644 index 00000000..0692d731 --- /dev/null +++ b/src/FSharp.Collections.Immutable/HashMap/HashMap.CustomComparers.fs @@ -0,0 +1,147 @@ +namespace FSharp.Collections.Immutable + +open System +open System.Collections.Generic +open System.Collections.Immutable + +// The struct holds the comparers and creates hash maps with them (ADR 0001, D12). A struct cannot run code in a primary +// constructor, so the constructors that check the comparers are explicit. Every struct also has a default value that no +// constructor checks; its fields are null, and the properties give it the default comparers, which the members use. +[] +type CustomComparersHashMap<'Key, 'Value when 'Key : not null> = + + val private keyComparer : IEqualityComparer<'Key> | null + val private valueComparer : IEqualityComparer<'Value> | null + + new (keyComparer : IEqualityComparer<'Key>, valueComparer : IEqualityComparer<'Value>) + = + checkNotNull (nameof keyComparer) keyComparer + checkNotNull (nameof valueComparer) valueComparer + { keyComparer = keyComparer; valueComparer = valueComparer } + + new (keyComparer : IEqualityComparer<'Key>) + = + checkNotNull (nameof keyComparer) keyComparer + { keyComparer = keyComparer; valueComparer = EqualityComparer<'Value>.Default } + + /// Sets the bindings, split into keys and values, one by one in a builder with the comparers, so that a later binding + /// of a key replaces an earlier one, as in `Map.ofSeq`, unless the value comparer finds the two values equal: the + /// indexer of the builder then keeps the earlier binding, as .NET does (D14). Every shape of the bindings creates its + /// maps here (D13). + member inline private this.Build ([] split : 'Binding -> struct ('Key * 'Value), elements : seq<'Binding>) = + let builder = this.builder () + Dictionaries.setAll split builder elements + builder.ToImmutable () + + // ---- Creation ---- + + [] + member this.empty : HashMap<'Key, 'Value> = + ImmutableDictionary.Create<'Key, 'Value>(this.KeyComparer, this.ValueComparer) + + [] + member this.ofArray (elements : KeyValuePair<'Key, 'Value> array) = + checkNotNull (nameof elements) elements + this.Build (Dictionaries.splitKeyValuePair, elements) + + [] + member this.ofArray (elements : ('Key * 'Value) array) = + checkNotNull (nameof elements) elements + this.Build (Dictionaries.splitTuple, elements) + + [] + member this.ofArray (elements : struct ('Key * 'Value) array) = + checkNotNull (nameof elements) elements + this.Build (id, elements) + + [] + member this.ofList (elements : KeyValuePair<'Key, 'Value> list) = this.Build (Dictionaries.splitKeyValuePair, elements) + + [] + member this.ofList (elements : ('Key * 'Value) list) = this.Build (Dictionaries.splitTuple, elements) + + [] + member this.ofList (elements : struct ('Key * 'Value) list) = this.Build (id, elements) + + [] + member this.ofReadOnlySpan (span : ReadOnlySpan>) : HashMap<'Key, 'Value> = + // The overload of CreateRangeWithOverwrite that takes a key comparer has no value comparer, so it can only create a + // map for the default one. Any other value comparer sets the bindings in a builder with the comparers, which gives + // the same result: the later binding of a key wins, and a value that the comparer finds equal keeps the earlier + // binding (D14) + if obj.ReferenceEquals (this.ValueComparer, EqualityComparer<'Value>.Default) then + ImmutableDictionary.CreateRangeWithOverwrite (this.KeyComparer, span) + else + let builder = this.builder () + + for pair in span do + builder[pair.Key] <- pair.Value + + builder.ToImmutable () + + [] + member this.ofSeq (elements : seq>) = + checkNotNullOrDefaultSeq (nameof elements) elements + + match elements with + // The keys of a hash map stay distinct in its own key comparer, so WithComparers cannot meet a collision and + // returns the map itself or the map with the value comparer, without a copy; another key comparer can make keys + // equal, and WithComparers would reject them where a later binding has to win + | :? ImmutableDictionary<'Key, 'Value> as table when obj.ReferenceEquals (table.KeyComparer, this.KeyComparer) -> + table.WithComparers (this.KeyComparer, this.ValueComparer) + | _ -> this.Build (Dictionaries.splitKeyValuePair, elements) + + [] + member this.ofSeq (elements : seq<'Key * 'Value>) = + checkNotNullOrDefaultSeq (nameof elements) elements + this.Build (Dictionaries.splitTuple, elements) + + [] + member this.ofSeq (elements : seq) = + checkNotNullOrDefaultSeq (nameof elements) elements + this.Build (id, elements) + + [] + member this.ofSeqBy (projection : 'Value -> 'Key) (source : seq<'Value>) : HashMap<'Key, 'Value> = + checkNotNullOrDefaultSeq (nameof source) source + + ImmutableDictionary.ToImmutableDictionary ( + source, + Func<'Value, 'Key> projection, + Func<'Value, 'Value> id, + this.KeyComparer, + this.ValueComparer + ) + + [] + member this.ofSpan (span : Span>) : HashMap<'Key, 'Value> = + // The BCL creates from a read-only span only; F# converts the span at this call, because the types of the member + // are known + this.ofReadOnlySpan span + + [] + member this.singleton (key : 'Key) (value : 'Value) : HashMap<'Key, 'Value> = this.empty.Add (key, value) + + // ---- Builder ---- + + [] + member this.build (action : HashMap<'Key, 'Value>.Builder -> unit) : HashMap<'Key, 'Value> = + let builder = this.builder () + action builder + builder.ToImmutable () + + [] + member this.builder () : HashMap<'Key, 'Value>.Builder = + ImmutableDictionary.CreateBuilder<'Key, 'Value>(this.KeyComparer, this.ValueComparer) + + // ---- Properties ---- + + member this.KeyComparer : IEqualityComparer<'Key> = + match this.keyComparer with + | null -> EqualityComparer<'Key>.Default + | comparer -> comparer + + member this.ValueComparer : IEqualityComparer<'Value> = + match this.valueComparer with + | null -> EqualityComparer<'Value>.Default + | comparer -> comparer diff --git a/src/FSharp.Collections.Immutable/HashMap/HashMap.CustomComparers.fsi b/src/FSharp.Collections.Immutable/HashMap/HashMap.CustomComparers.fsi new file mode 100644 index 00000000..c56e6b34 --- /dev/null +++ b/src/FSharp.Collections.Immutable/HashMap/HashMap.CustomComparers.fsi @@ -0,0 +1,338 @@ +namespace FSharp.Collections.Immutable + +open System +open System.Collections.Generic + +/// +/// The equality comparers of the keys and the values of hash maps, with the creation functions of +/// that create hash maps with them (ADR 0001, D12). +/// +/// +/// +/// +/// and +/// create the struct. Its members have the names of the creation functions of the module and the comparers of the struct. +/// +/// +/// ofArray, ofList and ofSeq take the bindings as key/value pairs, reference tuples or struct +/// tuples (ADR 0001, D13). A later binding of a key replaces an earlier one, as in +/// , +/// unless the value comparer finds the two values equal: then the earlier binding stays, its key included, as in the +/// builder of (ADR 0001, D14). A repeated key never +/// throws, where +/// rejects another value of it: to reject it, add the bindings to an empty map, +/// (HashMap.forComparers keyComparer valueComparer).empty |> HashMap.addRange pairs. +/// +/// +/// ofSeq also takes an existing map, because a map is a sequence of key/value pairs, and gives its bindings the +/// comparers of the struct. This is how to change the comparers of a map, with three differences from +/// : +/// the cost, the keys that become equal and the value comparer. The remarks of the ofSeq member describe them. +/// +/// +/// The struct only holds the comparers, so creating it allocates nothing. Its default value holds no comparers and stands +/// for the default comparers, . +/// +/// +/// +/// +/// let table = (HashMap.forKeyComparer StringComparer.OrdinalIgnoreCase).ofSeq [ "one", 1; "ONE", 2; "two", 3 ] +/// +/// Evaluates to a hash map whose keys ignore the case, with the bindings of "ONE" to 2 and "two" to 3. +/// +/// +/// +/// let ignoreCase = (HashMap.forKeyComparer StringComparer.OrdinalIgnoreCase).ofSeq table +/// let keepValues = (HashMap.forComparers StringComparer.OrdinalIgnoreCase (HashMap.valueComparer table)).ofSeq table +/// +/// Both evaluate to a hash map whose keys ignore the case: the first compares the values with the default comparer, the +/// second keeps the value comparer of table. +/// +[] +type CustomComparersHashMap<'Key, 'Value when 'Key : not null> = + + val private keyComparer : IEqualityComparer<'Key> | null + val private valueComparer : IEqualityComparer<'Value> | null + + /// Holds the equality comparers of the keys and the values. + /// The equality comparer of the keys. + /// The equality comparer of the values. + /// + /// Thrown when or is . + /// + new : + keyComparer : IEqualityComparer<'Key> * valueComparer : IEqualityComparer<'Value> -> CustomComparersHashMap<'Key, 'Value> + + /// + /// Holds the equality comparer of the keys, and the default comparer of the values. + /// + /// The equality comparer of the keys. + /// Thrown when is . + /// + /// The values compare with . + /// + new : keyComparer : IEqualityComparer<'Key> -> CustomComparersHashMap<'Key, 'Value> + + // ---- Creation ---- + + /// + /// The empty hash map with the comparers. + /// + /// + /// O(1). The property wraps + /// . + /// + [] + member empty : HashMap<'Key, 'Value> + + /// + /// Creates a hash map with the comparers from an array of key/value pairs. + /// + /// The input array of key/value pairs. + /// The hash map of the bindings. + /// Thrown when is . + /// + /// O(n log n). A later binding of a key replaces an earlier one, unless the value comparer finds the two values equal: + /// then the earlier binding stays, its key included (ADR 0001, D14). + /// + [] + member ofArray : elements : KeyValuePair<'Key, 'Value> array -> HashMap<'Key, 'Value> + + /// + /// Creates a hash map with the comparers from an array of reference tuples. + /// + /// The input array of key/value tuples. + /// The hash map of the bindings. + /// Thrown when is . + /// + /// O(n log n). A later binding of a key replaces an earlier one, unless the value comparer finds the two values equal: + /// then the earlier binding stays, its key included (ADR 0001, D14). + /// + [] + member ofArray : elements : ('Key * 'Value) array -> HashMap<'Key, 'Value> + + /// + /// Creates a hash map with the comparers from an array of struct tuples. + /// + /// The input array of key/value tuples. + /// The hash map of the bindings. + /// Thrown when is . + /// + /// O(n log n). A later binding of a key replaces an earlier one, unless the value comparer finds the two values equal: + /// then the earlier binding stays, its key included (ADR 0001, D14). + /// + [] + member ofArray : elements : struct ('Key * 'Value) array -> HashMap<'Key, 'Value> + + /// + /// Creates a hash map with the comparers from a list of key/value pairs. + /// + /// The input list of key/value pairs. + /// The hash map of the bindings. + /// + /// O(n log n). A later binding of a key replaces an earlier one, unless the value comparer finds the two values equal: + /// then the earlier binding stays, its key included (ADR 0001, D14). + /// + [] + member ofList : elements : KeyValuePair<'Key, 'Value> list -> HashMap<'Key, 'Value> + + /// + /// Creates a hash map with the comparers from a list of reference tuples. + /// + /// The input list of key/value tuples. + /// The hash map of the bindings. + /// + /// O(n log n). A later binding of a key replaces an earlier one, unless the value comparer finds the two values equal: + /// then the earlier binding stays, its key included (ADR 0001, D14). + /// + [] + member ofList : elements : ('Key * 'Value) list -> HashMap<'Key, 'Value> + + /// + /// Creates a hash map with the comparers from a list of struct tuples. + /// + /// The input list of key/value tuples. + /// The hash map of the bindings. + /// + /// O(n log n). A later binding of a key replaces an earlier one, unless the value comparer finds the two values equal: + /// then the earlier binding stays, its key included (ADR 0001, D14). + /// + [] + member ofList : elements : struct ('Key * 'Value) list -> HashMap<'Key, 'Value> + + /// + /// Creates a hash map with the comparers from a read-only span of key/value pairs. + /// + /// The input span of key/value pairs, which the member copies into the hash map. + /// The hash map of the bindings. + /// + /// + /// O(n log n). A later binding of a key replaces an earlier one, unless the value comparer finds the two values equal: + /// then the earlier binding stays, its key included (ADR 0001, D14). A repeated key never throws. + /// + /// + /// With the default comparer of the values the member wraps + /// . + /// That overload has no value comparer, so any other value comparer sets the bindings one by one in a builder with the + /// comparers, which gives the same result. + /// + /// + /// A goes to + /// . + /// An array converts to the parameter when the types of the struct are known; otherwise F# reports a type mismatch, + /// and the argument must be a . + /// + /// + [] + member ofReadOnlySpan : span : ReadOnlySpan> -> HashMap<'Key, 'Value> + + /// + /// Creates a hash map with the comparers from a sequence of key/value pairs, such as a dictionary or another map. + /// + /// The input sequence of key/value pairs. + /// + /// The hash map of the bindings. A hash map with the key comparer of the struct comes back itself when it also has the + /// value comparer, and with the value comparer otherwise. + /// + /// Thrown when is or a default flat list. + /// + /// + /// O(n log n); O(1) for a hash map whose key comparer is the instance that the struct holds, whose keys stay distinct, + /// through + /// . + /// A hash map with another key comparer is rebuilt, even when the comparer is equivalent. + /// + /// + /// A later binding of a key replaces an earlier one, unless the value comparer finds the two values equal: then the + /// earlier binding stays, its key included (ADR 0001, D14). Keys that only the comparer of the struct considers equal + /// merge by the same rule, in the order of enumeration of the map, where WithComparers throws + /// for another value of them. + /// + /// + /// The value comparer of the struct replaces the one of the map. A struct from + /// + /// compares the values with the default comparer, where WithComparers with a key comparer keeps the value + /// comparer of the map: HashMap.forComparers keyComparer (HashMap.valueComparer table) keeps it. + /// + /// + [] + member ofSeq : elements : seq> -> HashMap<'Key, 'Value> + + /// + /// Creates a hash map with the comparers from a sequence of reference tuples. + /// + /// The input sequence of key/value tuples. + /// The hash map of the bindings. + /// Thrown when is or a default flat list. + /// + /// O(n log n). A later binding of a key replaces an earlier one, unless the value comparer finds the two values equal: + /// then the earlier binding stays, its key included (ADR 0001, D14). + /// + [] + member ofSeq : elements : seq<'Key * 'Value> -> HashMap<'Key, 'Value> + + /// + /// Creates a hash map with the comparers from a sequence of struct tuples. + /// + /// The input sequence of key/value tuples. + /// The hash map of the bindings. + /// Thrown when is or a default flat list. + /// + /// O(n log n). A later binding of a key replaces an earlier one, unless the value comparer finds the two values equal: + /// then the earlier binding stays, its key included (ADR 0001, D14). + /// + [] + member ofSeq : elements : seq -> HashMap<'Key, 'Value> + + /// + /// Creates a hash map with the comparers of the elements of a sequence under the keys that a projection returns for them. + /// + /// The function that returns the key of an element. + /// The input sequence, whose elements become the values. + /// The hash map of the elements under their keys. + /// Thrown when is or a default flat list. + /// + /// Thrown when two elements have equal keys and are not equal in the value comparer. + /// + /// + /// O(n log n). The member wraps + /// , + /// which rejects a repeated key with another element and accepts one with an element that the value comparer finds + /// equal: the first element stays, its key included (ADR 0001, D14). The value of a binding is the element itself; + /// with an element selector, (HashMap.forComparers keyComparer valueComparer).empty |> HashMap.addRange (source |> Seq.map (fun x -> KeyValuePair (key x, element x))) + /// rejects a repeated key like ToImmutableDictionary does, and ofSeq of the mapped pairs lets the last + /// binding win. + /// + [] + member ofSeqBy : projection : ('Value -> 'Key) -> source : seq<'Value> -> HashMap<'Key, 'Value> + + /// + /// Creates a hash map with the comparers from a span of key/value pairs. + /// + /// The input span of key/value pairs, which the member copies into the hash map. + /// The hash map of the bindings. + /// + /// The BCL creates from a read-only span only, so the member passes the span to + /// , + /// which has the cost, the rule of a repeated key and the rule of equal values of that member (ADR 0001, D14). An array + /// converts to the parameter when the types of the struct are known. + /// + [] + member ofSpan : span : Span> -> HashMap<'Key, 'Value> + + /// Creates a hash map of one binding with the comparers. + /// The key. + /// The value. + /// The hash map of the binding. + /// O(1). + [] + member singleton : key : 'Key -> value : 'Value -> HashMap<'Key, 'Value> + + // ---- Builder ---- + + /// + /// Creates a hash map with the comparers with a builder that an action fills. + /// + /// The action that sets the bindings in the builder, which has the comparers. + /// The hash map of the bindings that the action set. + /// + /// O(1) plus the cost of the action, which sets each binding of the builder in O(log n), plus freezing the nodes that + /// the action changed: the hash map shares the nodes of the builder instead of copying the entries. The builder is the + /// one that creates, and the member + /// returns it with . The indexer + /// of the builder keeps the earlier binding of a key for a value that the value comparer finds equal, its key included + /// (ADR 0001, D14). + /// + [] + member build : action : (HashMap<'Key, 'Value>.Builder -> unit) -> HashMap<'Key, 'Value> + + /// + /// Creates an empty builder with the comparers. + /// + /// The builder. + /// + /// O(1). The member wraps + /// . + /// + [] + member builder : unit -> HashMap<'Key, 'Value>.Builder + + // ---- Properties ---- + + /// + /// The equality comparer of the keys. + /// + /// + /// O(1). The default value of the struct holds no comparer and returns + /// . + /// + member KeyComparer : IEqualityComparer<'Key> + + /// + /// The equality comparer of the values. + /// + /// + /// O(1). The default value of the struct holds no comparer and returns + /// . + /// + member ValueComparer : IEqualityComparer<'Value> diff --git a/src/FSharp.Collections.Immutable/HashMap/HashMap.Extensions.fs b/src/FSharp.Collections.Immutable/HashMap/HashMap.Extensions.fs new file mode 100644 index 00000000..8fc290f1 --- /dev/null +++ b/src/FSharp.Collections.Immutable/HashMap/HashMap.Extensions.fs @@ -0,0 +1,42 @@ +[] +module FSharp.Collections.Immutable.HashMapExtensions + +open System.Collections.Generic +open System.Collections.Immutable + +// The members are the members of CustomComparersHashMap with the default comparers, so that every shape of the bindings +// has one implementation (ADR 0001, D13). + +/// The comparer struct with the default comparers of the keys and the values (D5). +let private defaultComparers<'Key, 'Value when 'Key : not null> () = + CustomComparersHashMap<'Key, 'Value> EqualityComparer<'Key>.Default + +// The extended type repeats the constraint of the BCL type (FS0957) +type ImmutableDictionary<'Key, 'Value when 'Key : not null> with + + [] + static member ofArray (elements : KeyValuePair<'Key, 'Value> array) = (defaultComparers ()).ofArray elements + + [] + static member ofArray (elements : ('Key * 'Value) array) = (defaultComparers ()).ofArray elements + + [] + static member ofArray (elements : struct ('Key * 'Value) array) = (defaultComparers ()).ofArray elements + + [] + static member ofList (elements : KeyValuePair<'Key, 'Value> list) = (defaultComparers ()).ofList elements + + [] + static member ofList (elements : ('Key * 'Value) list) = (defaultComparers ()).ofList elements + + [] + static member ofList (elements : struct ('Key * 'Value) list) = (defaultComparers ()).ofList elements + + [] + static member ofSeq (elements : seq>) = (defaultComparers ()).ofSeq elements + + [] + static member ofSeq (elements : seq<'Key * 'Value>) = (defaultComparers ()).ofSeq elements + + [] + static member ofSeq (elements : seq) = (defaultComparers ()).ofSeq elements diff --git a/src/FSharp.Collections.Immutable/HashMap/HashMap.Extensions.fsi b/src/FSharp.Collections.Immutable/HashMap/HashMap.Extensions.fsi new file mode 100644 index 00000000..577588aa --- /dev/null +++ b/src/FSharp.Collections.Immutable/HashMap/HashMap.Extensions.fsi @@ -0,0 +1,163 @@ +/// +/// The creation functions of that take bindings in every shape: static +/// members of , each overloaded for key/value pairs, +/// reference tuples and struct tuples (ADR 0001, D13). +/// +/// +/// +/// Through the abbreviation , HashMap.ofSeq pairs reaches these +/// members, because the module has no functions with their names, and +/// overload resolution picks the shape of the bindings; pairs |> HashMap.ofSeq works too. Inside generic code, the +/// bindings need a type annotation (FS0041). +/// +/// +/// The members create hash maps with the default comparers, +/// : they are the members of +/// with those comparers. A later binding of a key +/// replaces an earlier one, as in +/// , +/// unless the default comparer of the values finds the two values equal: then the earlier binding stays, its key included, +/// as in the builder of (ADR 0001, D14). +/// +/// +/// A repeated key never throws, where +/// +/// and ToImmutableDictionary reject another value of it. To reject it, add the bindings to an empty map: +/// HashMap.empty |> HashMap.addRange pairs, or (HashMap.forComparers keyComparer valueComparer).empty |> HashMap.addRange pairs +/// for other comparers; it throws like the BCL. +/// +/// +/// Static extension members are an F# feature: C# does not see them as members of +/// , and a reflection over the module +/// lists none of ofArray, ofList and ofSeq. +/// They compile to public static methods of this module, HashMapExtensions.OfArray, OfList and OfSeq, +/// with one overload per shape. C# can call those, or the members of +/// , or the BCL. +/// +/// +[] +module FSharp.Collections.Immutable.HashMapExtensions + +open System.Collections.Generic +open System.Collections.Immutable + +// The extended type repeats the constraint of the BCL type (FS0957) +type ImmutableDictionary<'Key, 'Value when 'Key : not null> with + + /// + /// Creates a hash map from an array of key/value pairs. + /// + /// The input array of key/value pairs. + /// The hash map of the bindings, with the default comparers. + /// Thrown when is . + /// + /// O(n log n). A later binding of a key replaces an earlier one, unless the default comparer of the values finds the + /// two values equal: then the earlier binding stays, its key included (ADR 0001, D14). + /// + [] + static member ofArray : elements : KeyValuePair<'Key, 'Value> array -> HashMap<'Key, 'Value> + + /// + /// Creates a hash map from an array of reference tuples. + /// + /// The input array of key/value tuples. + /// The hash map of the bindings, with the default comparers. + /// Thrown when is . + /// + /// O(n log n). A later binding of a key replaces an earlier one, unless the default comparer of the values finds the + /// two values equal: then the earlier binding stays, its key included (ADR 0001, D14). + /// + [] + static member ofArray : elements : ('Key * 'Value) array -> HashMap<'Key, 'Value> + + /// + /// Creates a hash map from an array of struct tuples. + /// + /// The input array of key/value tuples. + /// The hash map of the bindings, with the default comparers. + /// Thrown when is . + /// + /// O(n log n). A later binding of a key replaces an earlier one, unless the default comparer of the values finds the + /// two values equal: then the earlier binding stays, its key included (ADR 0001, D14). + /// + [] + static member ofArray : elements : struct ('Key * 'Value) array -> HashMap<'Key, 'Value> + + /// + /// Creates a hash map from a list of key/value pairs. + /// + /// The input list of key/value pairs. + /// The hash map of the bindings, with the default comparers. + /// + /// O(n log n). A later binding of a key replaces an earlier one, unless the default comparer of the values finds the + /// two values equal: then the earlier binding stays, its key included (ADR 0001, D14). + /// + [] + static member ofList : elements : KeyValuePair<'Key, 'Value> list -> HashMap<'Key, 'Value> + + /// + /// Creates a hash map from a list of reference tuples. + /// + /// The input list of key/value tuples. + /// The hash map of the bindings, with the default comparers. + /// + /// O(n log n). A later binding of a key replaces an earlier one, unless the default comparer of the values finds the + /// two values equal: then the earlier binding stays, its key included (ADR 0001, D14). + /// + [] + static member ofList : elements : ('Key * 'Value) list -> HashMap<'Key, 'Value> + + /// + /// Creates a hash map from a list of struct tuples. + /// + /// The input list of key/value tuples. + /// The hash map of the bindings, with the default comparers. + /// + /// O(n log n). A later binding of a key replaces an earlier one, unless the default comparer of the values finds the + /// two values equal: then the earlier binding stays, its key included (ADR 0001, D14). + /// + [] + static member ofList : elements : struct ('Key * 'Value) list -> HashMap<'Key, 'Value> + + /// + /// Creates a hash map from a sequence of key/value pairs, such as a dictionary or another map. + /// + /// The input sequence of key/value pairs. + /// + /// The hash map of the bindings, with the default comparers. A hash map with the default comparers comes back itself. + /// + /// Thrown when is or a default flat list. + /// + /// O(n log n); O(1) for a hash map with the default key comparer. A later binding of a key replaces an earlier one, + /// unless the default comparer of the values finds the two values equal: then the earlier binding stays, its key + /// included (ADR 0001, D14). Keys that only the default comparer considers equal follow the same rule in the order of + /// enumeration. + /// + [] + static member ofSeq : elements : seq> -> HashMap<'Key, 'Value> + + /// + /// Creates a hash map from a sequence of reference tuples. + /// + /// The input sequence of key/value tuples. + /// The hash map of the bindings, with the default comparers. + /// Thrown when is or a default flat list. + /// + /// O(n log n). A later binding of a key replaces an earlier one, unless the default comparer of the values finds the + /// two values equal: then the earlier binding stays, its key included (ADR 0001, D14). + /// + [] + static member ofSeq : elements : seq<'Key * 'Value> -> HashMap<'Key, 'Value> + + /// + /// Creates a hash map from a sequence of struct tuples. + /// + /// The input sequence of key/value tuples. + /// The hash map of the bindings, with the default comparers. + /// Thrown when is or a default flat list. + /// + /// O(n log n). A later binding of a key replaces an earlier one, unless the default comparer of the values finds the + /// two values equal: then the earlier binding stays, its key included (ADR 0001, D14). + /// + [] + static member ofSeq : elements : seq -> HashMap<'Key, 'Value> diff --git a/src/FSharp.Collections.Immutable/HashMap/HashMap.fs b/src/FSharp.Collections.Immutable/HashMap/HashMap.fs new file mode 100644 index 00000000..65bec871 --- /dev/null +++ b/src/FSharp.Collections.Immutable/HashMap/HashMap.fs @@ -0,0 +1,346 @@ +[] +module FSharp.Collections.Immutable.HashMap + +open System +open System.Collections.Generic +open System.Collections.Immutable + +// The functions call the members of ImmutableDictionary where it has them (ADR 0001, D5) and visit the bindings with the +// algorithms that the map modules share (`Dictionaries` in Helper/Dictionaries.fs). The transformations fill a builder +// that has the comparers of the input, so that they keep them (D5, comparers). The functions that create a map from +// bindings are the members of CustomComparersHashMap (D12) and the static members of HashMapExtensions (D13); the module +// has none of their names, so that `HashMap.ofSeq` reaches the overloads of the extensions. + +// ---- Creation ---- + +[] +[] +let empty<'Key, 'Value when 'Key : not null> : HashMap<'Key, 'Value> = + ImmutableDictionary<'Key, 'Value>.Empty + +[] +let forComparers (keyComparer : IEqualityComparer<'Key>) (valueComparer : IEqualityComparer<'Value>) = + CustomComparersHashMap<'Key, 'Value>(keyComparer, valueComparer) + +[] +let forKeyComparer (keyComparer : IEqualityComparer<'Key>) = CustomComparersHashMap<'Key, 'Value> keyComparer + +// A span has one shape, so there are no extension overloads (D13); the member of the struct with the default comparers +// wraps ImmutableDictionary.CreateRangeWithOverwrite (D12) +[] +let ofReadOnlySpan (span : ReadOnlySpan>) : HashMap<'Key, 'Value> = + (CustomComparersHashMap<'Key, 'Value> EqualityComparer<'Key>.Default).ofReadOnlySpan span + +[] +let ofSeqBy (projection : 'Value -> 'Key) (source : seq<'Value>) : HashMap<'Key, 'Value> = + checkNotNullOrDefaultSeq (nameof source) source + ImmutableDictionary.ToImmutableDictionary (source, Func<'Value, 'Key> projection) + +// The BCL creates from a read-only span only; the member of the struct converts the span, which F# does not do at a call +// of a function whose type arguments it has not inferred yet +[] +let ofSpan (span : Span>) : HashMap<'Key, 'Value> = + (CustomComparersHashMap<'Key, 'Value> EqualityComparer<'Key>.Default).ofSpan span + +[] +let singleton (key : 'Key) (value : 'Value) : HashMap<'Key, 'Value> = ImmutableDictionary<'Key, 'Value>.Empty.Add(key, value) + +// ---- Builder ---- + +[] +let build (action : HashMap<'Key, 'Value>.Builder -> unit) : HashMap<'Key, 'Value> = + (CustomComparersHashMap<'Key, 'Value> EqualityComparer<'Key>.Default).build action + +[] +let builder () : HashMap<'Key, 'Value>.Builder = ImmutableDictionary.CreateBuilder<'Key, 'Value>() + +[] +let ofBuilder (builder : HashMap<'Key, 'Value>.Builder) = + checkNotNull (nameof builder) builder + builder.ToImmutable () + +[] +let toBuilder (table : HashMap<'Key, 'Value>) = + checkNotNull (nameof table) table + table.ToBuilder () + +[] +let update (action : HashMap<'Key, 'Value>.Builder -> unit) (table : HashMap<'Key, 'Value>) : HashMap<'Key, 'Value> = + checkNotNull (nameof table) table + // The builder starts with the bindings and the comparers of the map, and ToImmutable returns the map itself when the + // action changed nothing + let builder = table.ToBuilder () + action builder + builder.ToImmutable () + +// ---- Properties ---- + +[] +let count (table : HashMap<'Key, 'Value>) = + checkNotNull (nameof table) table + table.Count + +[] +let isEmpty (table : HashMap<'Key, 'Value>) = + checkNotNull (nameof table) table + table.IsEmpty + +[] +let keyComparer (table : HashMap<'Key, 'Value>) = + checkNotNull (nameof table) table + table.KeyComparer + +[] +let keys (table : HashMap<'Key, 'Value>) = + checkNotNull (nameof table) table + // The explicit implementation of IDictionary is a view whose count and lookup ask the map, as Map.keys is + (table :> IDictionary<'Key, 'Value>).Keys + +[] +let valueComparer (table : HashMap<'Key, 'Value>) = + checkNotNull (nameof table) table + table.ValueComparer + +[] +let values (table : HashMap<'Key, 'Value>) = + checkNotNull (nameof table) table + (table :> IDictionary<'Key, 'Value>).Values + +// ---- Access ---- + +[] +let find (key : 'Key) (table : HashMap<'Key, 'Value>) = + checkNotNull (nameof table) table + + match table.TryGetValue key with + | true, value -> value + | false, _ -> Raise.keyNotFound () + +[] +let tryFind (key : 'Key) (table : HashMap<'Key, 'Value>) = + checkNotNull (nameof table) table + + match table.TryGetValue key with + | true, value -> ValueSome value + | false, _ -> ValueNone + +[] +let tryGetKey (key : 'Key) (table : HashMap<'Key, 'Value>) = + checkNotNull (nameof table) table + + match table.TryGetKey key with + | true, actualKey -> ValueSome actualKey + | false, _ -> ValueNone + +// ---- Search ---- + +[] +let containsBinding (key : 'Key) (value : 'Value) (table : HashMap<'Key, 'Value>) = + checkNotNull (nameof table) table + // The value comparer of the map decides whether the stored value equals the value, which no other function can ask + table.Contains (KeyValuePair (key, value)) + +[] +let containsKey (key : 'Key) (table : HashMap<'Key, 'Value>) = + checkNotNull (nameof table) table + table.ContainsKey key + +[] +let containsValue (value : 'Value) (table : HashMap<'Key, 'Value>) = + checkNotNull (nameof table) table + table.ContainsValue value + +[] +let exists predicate (table : HashMap<'Key, 'Value>) = + checkNotNull (nameof table) table + Dictionaries.exists predicate table + +[] +let findKey predicate (table : HashMap<'Key, 'Value>) = + checkNotNull (nameof table) table + + // Map.findKey searches with Seq.pick, whose message differs from the one of Map.find + match Dictionaries.tryFindKey predicate table with + | ValueSome key -> key + | ValueNone -> Raise.indexNotFound () + +[] +let forall predicate (table : HashMap<'Key, 'Value>) = + checkNotNull (nameof table) table + Dictionaries.forall predicate table + +[] +let pick (chooser : 'Key -> 'Value -> 'U voption) (table : HashMap<'Key, 'Value>) = + checkNotNull (nameof table) table + + match Dictionaries.tryPick chooser table with + | ValueSome value -> value + | ValueNone -> Raise.keyNotFound () + +[] +let tryFindKey predicate (table : HashMap<'Key, 'Value>) = + checkNotNull (nameof table) table + Dictionaries.tryFindKey predicate table + +[] +let tryPick (chooser : 'Key -> 'Value -> 'U voption) (table : HashMap<'Key, 'Value>) = + checkNotNull (nameof table) table + Dictionaries.tryPick chooser table + +// ---- Transformation ---- + +// add is ImmutableDictionary.Add, as it was in 1.0.0: it throws for a key bound to another value and returns the map itself +// for an equal binding. Map.add replaces the value, which setItem does (ADR 0001, D14) +[] +let add (key : 'Key) (value : 'Value) (table : HashMap<'Key, 'Value>) = + checkNotNull (nameof table) table + table.Add (key, value) + +[] +let addRange (pairs : seq>) (table : HashMap<'Key, 'Value>) = + checkNotNullOrDefaultSeq (nameof pairs) pairs + checkNotNull (nameof table) table + table.AddRange pairs + +[] +let change (key : 'Key) (f : 'Value voption -> 'Value voption) (table : HashMap<'Key, 'Value>) = + checkNotNull (nameof table) table + + let current = + match table.TryGetValue key with + | true, value -> ValueSome value + | false, _ -> ValueNone + + match f current with + | ValueSome value -> table.SetItem (key, value) + | ValueNone -> table.Remove key + +[] +let choose (chooser : 'Key -> 'Value -> 'U voption) (table : HashMap<'Key, 'Value>) = + checkNotNull (nameof table) table + // The keys keep their type and comparer; the new values get the default comparer of their type + let builder = ImmutableDictionary.CreateBuilder<'Key, 'U> table.KeyComparer + Dictionaries.choose chooser builder table + builder.ToImmutable () + +[] +let clear (table : HashMap<'Key, 'Value>) = + checkNotNull (nameof table) table + table.Clear () + +[] +let filter predicate (table : HashMap<'Key, 'Value>) = + checkNotNull (nameof table) table + // A builder over the input shares the bindings that stay and returns the input itself when every binding stays + let builder = table.ToBuilder () + Dictionaries.filter predicate builder table + builder.ToImmutable () + +[] +let map (mapping : 'Key -> 'Value -> 'U) (table : HashMap<'Key, 'Value>) = + checkNotNull (nameof table) table + let builder = ImmutableDictionary.CreateBuilder<'Key, 'U> table.KeyComparer + Dictionaries.map mapping builder table + builder.ToImmutable () + +[] +let remove (key : 'Key) (table : HashMap<'Key, 'Value>) = + checkNotNull (nameof table) table + table.Remove key + +[] +let removeRange (keys : seq<'Key>) (table : HashMap<'Key, 'Value>) = + checkNotNullOrDefaultSeq (nameof keys) keys + checkNotNull (nameof table) table + table.RemoveRange keys + +[] +let setItem (key : 'Key) (value : 'Value) (table : HashMap<'Key, 'Value>) = + checkNotNull (nameof table) table + table.SetItem (key, value) + +[] +let setItems (pairs : seq>) (table : HashMap<'Key, 'Value>) = + checkNotNullOrDefaultSeq (nameof pairs) pairs + checkNotNull (nameof table) table + table.SetItems pairs + +// ---- Folding ---- + +[] +let fold (folder : 'State -> 'Key -> 'Value -> 'State) (state : 'State) (table : HashMap<'Key, 'Value>) = + checkNotNull (nameof table) table + Dictionaries.fold folder state table + +[] +let foldBack (folder : 'Key -> 'Value -> 'State -> 'State) (table : HashMap<'Key, 'Value>) (state : 'State) = + checkNotNull (nameof table) table + Dictionaries.foldBack folder table state + +[] +let iter action (table : HashMap<'Key, 'Value>) = + checkNotNull (nameof table) table + Dictionaries.iter action table + +// ---- Partitioning ---- + +[] +let partition predicate (table : HashMap<'Key, 'Value>) = + checkNotNull (nameof table) table + let accepted = table.ToBuilder () + let rejected = table.Clear().ToBuilder() + Dictionaries.partition predicate accepted rejected table + struct (accepted.ToImmutable (), rejected.ToImmutable ()) + +// ---- Conversion ---- + +[] +let toArray (table : HashMap<'Key, 'Value>) = + checkNotNull (nameof table) table + Dictionaries.toArray structPair table + +[] +let toList (table : HashMap<'Key, 'Value>) = + checkNotNull (nameof table) table + Dictionaries.toList structPair table + +[] +let toSeq (table : HashMap<'Key, 'Value>) = + checkNotNull (nameof table) table + Dictionaries.toSeq structPair table + +// The submodules take the bindings as tuples, convert them to key/value pairs on demand and call the members that the +// functions of the module call, so that every shape has the semantics of the BCL member (D13). +[] +module Tuple = + + // ---- Transformation ---- + + [] + let addRange (pairs : seq<'Key * 'Value>) (table : HashMap<'Key, 'Value>) = + checkNotNullOrDefaultSeq (nameof pairs) pairs + checkNotNull (nameof table) table + table.AddRange (Dictionaries.toKeyValuePairs Dictionaries.splitTuple pairs) + + [] + let setItems (pairs : seq<'Key * 'Value>) (table : HashMap<'Key, 'Value>) = + checkNotNullOrDefaultSeq (nameof pairs) pairs + checkNotNull (nameof table) table + table.SetItems (Dictionaries.toKeyValuePairs Dictionaries.splitTuple pairs) + +[] +module ValueTuple = + + // ---- Transformation ---- + + [] + let addRange (pairs : seq) (table : HashMap<'Key, 'Value>) = + checkNotNullOrDefaultSeq (nameof pairs) pairs + checkNotNull (nameof table) table + table.AddRange (Dictionaries.toKeyValuePairs id pairs) + + [] + let setItems (pairs : seq) (table : HashMap<'Key, 'Value>) = + checkNotNullOrDefaultSeq (nameof pairs) pairs + checkNotNull (nameof table) table + table.SetItems (Dictionaries.toKeyValuePairs id pairs) diff --git a/src/FSharp.Collections.Immutable/HashMap/HashMap.fsi b/src/FSharp.Collections.Immutable/HashMap/HashMap.fsi new file mode 100644 index 00000000..b7e8c7a3 --- /dev/null +++ b/src/FSharp.Collections.Immutable/HashMap/HashMap.fsi @@ -0,0 +1,951 @@ +/// +/// Functions for , the immutable hash map. +/// +/// +/// +/// The module mirrors : each function with a name of that module has +/// its semantics, its argument checks and its exceptions (ADR 0001), except +/// . +/// It is the of the BCL, as it was in 1.0.0: +/// it throws for a key that is bound to another value, where Map.add replaces the value, and +/// +/// replaces it (ADR 0001, D14). The other functions wrap a member of +/// and keep its semantics and its exceptions. +/// +/// +/// A hash map enumerates its bindings in an unspecified order, which stays the same for the same map. The functions that +/// visit the bindings one by one follow that order, where follows +/// the order of the keys. +/// +/// +/// A hash map compares its keys and its values with the equality comparers that it was created with. The functions of the +/// module create hash maps with the default comparers, ; +/// +/// and +/// return a , which creates hash maps with other +/// comparers (ADR 0001, D12). Every transformation keeps the key comparer. The transformations that keep the type of the +/// values keep the value comparer, and the transformations that change it give the new values the default comparer of +/// their type. +/// +/// +/// 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, in the functions that set bindings and in the creation functions, as in +/// (ADR 0001, D14). +/// +/// +/// The single-key idioms of the BCL are functions of the module or compositions of them. TryGetValue is +/// , +/// and GetValueOrDefault is HashMap.tryFind key table |> ValueOption.defaultValue defaultValue. Adding a +/// binding only when the key has none, as ImmutableInterlocked.TryAdd does, is +/// +/// with a function that keeps an existing value. The other members of ImmutableInterlocked replace a field +/// atomically and stay direct calls with a reference to the field. +/// +/// +/// A builder is the BCL builder: the module creates it (builder, toBuilder, build and update) +/// and turns it into a hash map (ofBuilder), and the members of the builder, such as Add, AddRange, +/// Remove, RemoveRange and the indexer, are called on it. There is no builder module for maps (ADR 0001, D2). +/// +/// +/// A binding comes as a key/value pair, a reference tuple or a struct tuple (ADR 0001, D13). HashMap.ofArray, +/// HashMap.ofList and HashMap.ofSeq take every shape: they are the static members of +/// , which the module leaves visible by having no functions +/// with their names. +/// and +/// take key/value pairs, and the nested modules and +/// take reference and struct tuples. +/// +/// +/// A map, sequence, builder or comparer throws , and so +/// does a default flat list that is given as a sequence: it fails when a function enumerates it, so it is as good as a +/// sequence (ADR 0001, D6). +/// +/// +/// Functions that return an optional value return a , and +/// functions that return tuples return struct tuples. The namespaces +/// and contain a module with the same name whose functions use options +/// and reference tuples instead. +/// +/// +[] +module FSharp.Collections.Immutable.HashMap + +open System +open System.Collections.Generic +open System.Collections.Immutable + +// ---- Creation ---- + +/// +/// The empty hash map, with the default comparers. +/// +/// +/// O(1). The value is . +/// +[] +[] +val empty<'Key, 'Value when 'Key : not null> : HashMap<'Key, 'Value> + +/// +/// Holds equality comparers of the keys and the values, to create hash maps with them. +/// +/// The equality comparer of the keys. +/// The equality comparer of the values. +/// The struct whose members create hash maps with the comparers. +/// +/// Thrown when or is . +/// +/// +/// +/// O(1); the struct allocates nothing. Its members have the names of the creation functions of the module (ADR 0001, D12). +/// +/// +/// The ofSeq member of the struct also gives an existing hash map these comparers, which is how to change the +/// comparers of a map. Unlike +/// it rebuilds the map unless the key comparer is the same instance, and it merges keys that become equal instead of +/// throwing; the remarks of have the details. +/// +/// +/// +/// +/// let comparers = HashMap.forComparers StringComparer.OrdinalIgnoreCase StringComparer.Ordinal +/// let table = comparers.ofSeq [ "one", "uno"; "two", "dos" ] +/// +/// Evaluates to a hash map whose keys ignore the case and whose values compare ordinally. +/// +/// +/// +/// let ignoreCase = (HashMap.forComparers StringComparer.OrdinalIgnoreCase (HashMap.valueComparer table)).ofSeq table +/// +/// Evaluates to a hash map with the bindings of table whose keys ignore the case and whose values keep the value +/// comparer of table. +/// +[] +val forComparers : + keyComparer : IEqualityComparer<'Key> -> valueComparer : IEqualityComparer<'Value> -> CustomComparersHashMap<'Key, 'Value> + +/// +/// Holds an equality comparer of the keys, to create hash maps with it. +/// +/// The equality comparer of the keys. +/// +/// The struct whose members create hash maps with the key comparer, and with the default comparer of the values. +/// +/// Thrown when is . +/// +/// +/// O(1); the struct allocates nothing. Its members have the names of the creation functions of the module (ADR 0001, D12). +/// +/// +/// The ofSeq member of the struct also gives an existing hash map this key comparer, with the default comparer of +/// the values: the value comparer of the map is replaced, where +/// +/// keeps it. Use +/// +/// with the value comparer of the map to keep it. +/// +/// +/// +/// +/// let table = (HashMap.forKeyComparer StringComparer.OrdinalIgnoreCase).ofSeq [ "one", 1; "ONE", 2 ] +/// +/// Evaluates to a hash map whose keys ignore the case, with the binding of "ONE" to 2. +/// +[] +val forKeyComparer : keyComparer : IEqualityComparer<'Key> -> CustomComparersHashMap<'Key, 'Value> + +/// +/// Creates a hash map from a copy of a read-only span of key/value pairs. +/// +/// The input span of key/value pairs. +/// The hash map of the bindings, with the default comparers. +/// +/// +/// O(n log n). A later binding of a key replaces an earlier one, unless the default comparer of the values finds the two +/// values equal: then the earlier binding stays, its key included (ADR 0001, D14). A repeated key never throws. +/// +/// +/// The function wraps +/// +/// through . +/// A span has one shape, so there are no overloads for tuples (ADR 0001, D13). +/// +/// +/// A goes to +/// , +/// which needs no annotation. An array converts to the parameter only when the types of the call are known, from an +/// annotation of the result or from explicit type arguments; F# then reports the conversion with the warning FS3391. +/// Otherwise F# reports a type mismatch, and the argument must be a . The function +/// cannot be used as a value or with the pipeline operator, because a span cannot be a type argument. +/// +/// +[] +val ofReadOnlySpan : span : ReadOnlySpan> -> HashMap<'Key, 'Value> + +/// +/// Creates a hash map of the elements of a sequence under the keys that a projection returns for them. +/// +/// The function that returns the key of an element. +/// The input sequence, whose elements become the values. +/// The hash map of the elements under their keys. +/// Thrown when is or a default flat list. +/// +/// Thrown when two elements have the same key and the default comparer of the values finds them different. +/// +/// +/// O(n log n). The function wraps +/// , +/// which rejects a repeated key with another element and accepts one with an element that the default comparer of the +/// values finds equal: the first element stays, its key included (ADR 0001, D14). The value of a binding is the element +/// itself; with an element selector, as ToImmutableDictionary takes one, +/// HashMap.empty |> HashMap.addRange (source |> Seq.map (fun x -> KeyValuePair (key x, element x))) rejects a +/// repeated key like the BCL does, and HashMap.ofSeq of the mapped pairs lets the last binding win. +/// +[] +val ofSeqBy : projection : ('Value -> 'Key) -> source : seq<'Value> -> HashMap<'Key, 'Value> + +/// +/// Creates a hash map from a copy of a span of key/value pairs. +/// +/// The input span of key/value pairs. +/// The hash map of the bindings, with the default comparers. +/// +/// +/// O(n log n). The BCL creates from a read-only span only, so the function converts the span without a copy and creates the +/// hash map as +/// +/// does: a later binding of a key replaces an earlier one, unless the default comparer of the values finds the two values +/// equal, and a repeated key never throws (ADR 0001, D14). +/// +/// +/// An array converts to the parameter only when the types of the call are known, from an annotation of the result or from +/// explicit type arguments. The function cannot be used as a value or with the pipeline operator, because a span cannot be +/// a type argument. +/// +/// +[] +val ofSpan : span : Span> -> HashMap<'Key, 'Value> + +/// Creates a hash map of one binding. +/// The key. +/// The value. +/// The hash map of the binding, with the default comparers. +/// O(1). +[] +val singleton : key : 'Key -> value : 'Value -> HashMap<'Key, 'Value> + +// ---- Builder ---- + +/// +/// Creates a hash map with a builder that an action fills. +/// +/// The action that sets the bindings in the builder, which has the default comparers. +/// The hash map of the bindings that the action set, with the default comparers. +/// +/// +/// O(1) plus the cost of the action, which sets each binding of the builder in O(log n), plus freezing the nodes that the +/// action changed: the hash map shares the nodes of the builder instead of copying the entries. The function creates the +/// builder with and returns it with +/// , as +/// +/// does for other comparers. +/// +/// +/// The indexer of the builder keeps the earlier binding of a key for a value that the value comparer finds equal, its key +/// included (ADR 0001, D14). +/// +/// +/// +/// +/// let table = HashMap.build (fun builder -> for i in 1..3 do builder[i] <- i * i) +/// +/// Evaluates to a hash map of 1 to 1, 2 to 4 and 3 to 9. +/// +[] +val build : action : (HashMap<'Key, 'Value>.Builder -> unit) -> HashMap<'Key, 'Value> + +/// +/// Creates an empty builder with the default comparers. +/// +/// The builder. +/// +/// O(1). The function wraps . +/// +[] +val builder : unit -> HashMap<'Key, 'Value>.Builder + +/// +/// Returns the contents of a builder as a hash map. +/// +/// The builder, which stays usable. +/// The hash map of the bindings of the builder, with its comparers. +/// Thrown when is . +/// +/// O(1) apart from freezing the nodes that changed since the builder last returned a hash map: the hash map shares the nodes +/// of the builder instead of copying the entries. The function wraps +/// . +/// +[] +val ofBuilder : builder : HashMap<'Key, 'Value>.Builder -> HashMap<'Key, 'Value> + +/// +/// Creates a builder that contains the bindings of a hash map. +/// +/// The input hash map, which stays unchanged. +/// The builder, with the comparers of the hash map. +/// Thrown when is . +/// +/// O(1). The function wraps . +/// +[] +val toBuilder : table : HashMap<'Key, 'Value> -> HashMap<'Key, 'Value>.Builder + +/// +/// Creates a hash map from a builder over the bindings of a hash map, after an action changes the builder. +/// +/// The action that changes the builder, which has the comparers of the hash map. +/// The input hash map, which stays unchanged. +/// +/// The hash map of the changed builder, with the comparers of the input; the input itself when the action changed nothing. +/// +/// Thrown when is . +/// +/// O(1) plus the cost of the action, which changes each binding of the builder in O(log n), plus freezing the nodes that +/// the action changed: the hash map shares the nodes that the action left unchanged with the input instead of copying the +/// entries. The function creates the builder with +/// and returns it with +/// . +/// +/// +/// +/// let squares = HashMap.build (fun builder -> for i in 1..3 do builder[i] <- i * i) +/// let table = squares |> HashMap.update (fun builder -> builder.Remove 1 |> ignore; builder[4] <- 16) +/// +/// Evaluates to a hash map of 2 to 4, 3 to 9 and 4 to 16, and squares stays unchanged. +/// +[] +val update : action : (HashMap<'Key, 'Value>.Builder -> unit) -> table : HashMap<'Key, 'Value> -> HashMap<'Key, 'Value> + +// ---- Properties ---- + +/// Returns the number of bindings of the hash map. +/// The input hash map. +/// The number of bindings. +/// Thrown when is . +/// O(1). +[] +val count : table : HashMap<'Key, 'Value> -> int + +/// Returns whether the hash map has no bindings. +/// The input hash map. +/// when the hash map is empty. +/// Thrown when is . +/// O(1). +[] +val isEmpty : table : HashMap<'Key, 'Value> -> bool + +/// +/// Returns the equality comparer of the keys. +/// +/// The input hash map. +/// The key comparer. +/// Thrown when is . +/// +/// O(1). The function wraps . +/// +[] +val keyComparer : table : HashMap<'Key, 'Value> -> IEqualityComparer<'Key> + +/// Returns the keys of the hash map. +/// The input hash map. +/// +/// A read-only view of the keys in the order of enumeration of the hash map, whose count takes O(1) and whose lookup +/// takes O(log n) in the key comparer. +/// +/// Thrown when is . +/// O(1). +[] +val keys : table : HashMap<'Key, 'Value> -> ICollection<'Key> + +/// +/// Returns the equality comparer of the values. +/// +/// The input hash map. +/// The value comparer. +/// Thrown when is . +/// +/// O(1). The function wraps . +/// +[] +val valueComparer : table : HashMap<'Key, 'Value> -> IEqualityComparer<'Value> + +/// +/// Returns the values of the hash map, including the duplicates. +/// +/// The input hash map. +/// +/// A read-only view of the values in the order of enumeration of the hash map, whose count takes O(1) and whose lookup +/// takes O(n) in the value comparer. +/// +/// Thrown when is . +/// +/// O(1). +/// +[] +val values : table : HashMap<'Key, 'Value> -> ICollection<'Value> + +// ---- Access ---- + +/// Returns the value of a key. +/// The key to find. +/// The input hash map. +/// The value of the key. +/// Thrown when is . +/// Thrown when the hash map has no binding of the key. +/// O(log n). +[] +val find : key : 'Key -> table : HashMap<'Key, 'Value> -> 'Value + +/// +/// Returns the value of a key, or ValueNone when the hash map has no binding of the key. +/// +/// The key to find. +/// The input hash map. +/// The value of the key, or ValueNone. +/// Thrown when is . +/// +/// O(log n). The function is the TryGetValue of the BCL with a voption instead of the out parameter. +/// GetValueOrDefault is HashMap.tryFind key table |> ValueOption.defaultValue defaultValue: one lookup, +/// and a voption is a struct. +/// +[] +val tryFind : key : 'Key -> table : HashMap<'Key, 'Value> -> 'Value voption + +/// +/// Returns the key of the hash map that equals a key in the key comparer, or ValueNone. +/// +/// The key to find. +/// The input hash map. +/// The key that the hash map holds, which can differ from the given one, or ValueNone. +/// Thrown when is . +/// +/// O(log n). The function wraps . +/// +[] +val tryGetKey : key : 'Key -> table : HashMap<'Key, 'Value> -> 'Key voption + +// ---- Search ---- + +/// +/// Returns whether the hash map binds a key to a value, comparing the values with the value comparer. +/// +/// The key to find. +/// The value that the key must be bound to. +/// The input hash map. +/// +/// when the hash map has a binding of the key whose value the value comparer finds equal to +/// . +/// +/// Thrown when is . +/// +/// +/// O(log n). The function wraps +/// . +/// A key without a binding gives . The member does not check a key, as +/// +/// does, so the key comparer decides: the default comparer finds no binding and the function gives +/// , and a comparer that rejects , such as +/// , throws . +/// +/// +/// The value comparer of the map decides whether the values are equal. No composition of the other functions asks it: +/// HashMap.tryFind followed by = compares with F# equality, which for a comparer that ignores the case finds +/// "Hello" and "HELLO" different. +/// +/// +/// +/// +/// let table = (HashMap.forComparers StringComparer.Ordinal StringComparer.OrdinalIgnoreCase).ofSeq [ "one", "Uno"; "two", "Dos" ] +/// let found = table |> HashMap.containsBinding "one" "UNO" +/// +/// Evaluates to true, where HashMap.tryFind "one" table is ValueSome "Uno". +/// +[] +val containsBinding : key : 'Key -> value : 'Value -> table : HashMap<'Key, 'Value> -> bool + +/// Returns whether the hash map has a binding of a key. +/// The key to find. +/// The input hash map. +/// when the hash map has a binding of the key. +/// Thrown when is . +/// O(log n). +[] +val containsKey : key : 'Key -> table : HashMap<'Key, 'Value> -> bool + +/// +/// Returns whether the hash map has a binding of a value, in the value comparer. +/// +/// The value to find. +/// The input hash map. +/// when a value of the hash map equals the given one. +/// Thrown when is . +/// +/// O(n). The function wraps . +/// +[] +val containsValue : value : 'Value -> table : HashMap<'Key, 'Value> -> bool + +/// Returns whether any binding satisfies the predicate. +/// The function to test the bindings. +/// The input hash map. +/// when a binding satisfies the predicate. +/// Thrown when is . +/// O(n). +[] +val exists : predicate : ('Key -> 'Value -> bool) -> table : HashMap<'Key, 'Value> -> bool + +/// +/// Returns the key of the first binding, in the order of enumeration, that satisfies the predicate. +/// +/// The function to test the bindings. +/// The input hash map. +/// The key of the first binding that satisfies the predicate. +/// Thrown when is . +/// Thrown when no binding satisfies the predicate. +/// +/// O(n). +/// +[] +val findKey : predicate : ('Key -> 'Value -> bool) -> table : HashMap<'Key, 'Value> -> 'Key + +/// Returns whether all bindings satisfy the predicate. +/// The function to test the bindings. +/// The input hash map. +/// when every binding satisfies the predicate. +/// Thrown when is . +/// O(n). +[] +val forall : predicate : ('Key -> 'Value -> bool) -> table : HashMap<'Key, 'Value> -> bool + +/// +/// Returns the first value, in the order of enumeration, that the chooser returns for the bindings. +/// +/// The function that returns a value, or ValueNone to go on. +/// The input hash map. +/// The first chosen value. +/// Thrown when is . +/// Thrown when the chooser returns ValueNone for every binding. +/// +/// O(n). +/// +[] +val pick : chooser : ('Key -> 'Value -> 'U voption) -> table : HashMap<'Key, 'Value> -> 'U + +/// +/// Returns the key of the first binding, in the order of enumeration, that satisfies the predicate, or ValueNone. +/// +/// The function to test the bindings. +/// The input hash map. +/// The key of the first binding that satisfies the predicate, or ValueNone. +/// Thrown when is . +/// +/// O(n). +/// +[] +val tryFindKey : predicate : ('Key -> 'Value -> bool) -> table : HashMap<'Key, 'Value> -> 'Key voption + +/// +/// Returns the first value, in the order of enumeration, that the chooser returns for the bindings, or ValueNone. +/// +/// The function that returns a value, or ValueNone to go on. +/// The input hash map. +/// The first chosen value, or ValueNone. +/// Thrown when is . +/// +/// O(n). +/// +[] +val tryPick : chooser : ('Key -> 'Value -> 'U voption) -> table : HashMap<'Key, 'Value> -> 'U voption + +// ---- Transformation ---- + +/// +/// Returns a hash map with a binding of a key to a value, for a key that is not bound to another value. +/// +/// The key. +/// The value. +/// The input hash map. +/// The hash map with the binding; the input itself when it already has the binding. +/// Thrown when is . +/// +/// Thrown when the hash map binds the key to a value that the value comparer finds different. +/// +/// +/// +/// O(log n). The function wraps and +/// behaves as it does (ADR 0001, D14), as the 1.0.0 function did: it does not replace the value of a key, where +/// Map.add does. When the value comparer of the hash map finds the value equal to the current value of the key, the +/// binding stays as it is, its key included, and the input itself comes back. +/// +/// +/// Use +/// to replace the value of a key, and +/// +/// to add a binding only when the key has none, or to compute the new value from the current one. +/// +/// +[] +val add : key : 'Key -> value : 'Value -> table : HashMap<'Key, 'Value> -> HashMap<'Key, 'Value> + +/// +/// Returns a hash map with the bindings of key/value pairs added. +/// +/// The key/value pairs to add. +/// The input hash map. +/// The hash map with the bindings. +/// Thrown when is or a default flat list, or is . +/// +/// Thrown when a pair has the key of a binding of the hash map or of another pair, and a value that the value comparer +/// finds different. +/// +/// +/// O(m log(n + m)). The function wraps +/// , +/// which rejects another value of a key and accepts a pair whose value the value comparer finds equal to the value of the +/// key, leaving that binding as it is, its key included (ADR 0001, D14). Use +/// +/// to replace the values, and and +/// to add tuples. +/// +[] +val addRange : pairs : seq> -> table : HashMap<'Key, 'Value> -> HashMap<'Key, 'Value> + +/// +/// Returns a hash map with the binding of a key changed by a function of its current value. +/// +/// The key. +/// +/// The function that takes the current value or ValueNone, and returns the new value or ValueNone to remove +/// the binding. +/// +/// The input hash map. +/// The hash map with the changed binding; the input itself when nothing changes. +/// Thrown when is . +/// +/// +/// O(log n). When returns a value that the value comparer of the hash map finds equal to the current +/// value, the binding stays as it is, its key included, as in +/// (ADR 0001, D14). +/// +/// +/// Adding a binding only when the key has none is HashMap.change key (function ValueNone -> ValueSome value | existing -> existing), +/// which the BCL offers as ImmutableInterlocked.TryAdd for a field. +/// +/// +[] +val change : key : 'Key -> f : ('Value voption -> 'Value voption) -> table : HashMap<'Key, 'Value> -> HashMap<'Key, 'Value> + +/// +/// Returns a hash map of the values that the chooser returns for the bindings, under their keys. +/// +/// The function that returns the new value, or ValueNone to drop the binding. +/// The input hash map. +/// +/// The hash map of the chosen values, with the key comparer of the input and the default comparer of the new values. +/// +/// Thrown when is . +/// +/// O(n log n). +/// +[] +val choose : chooser : ('Key -> 'Value -> 'U voption) -> table : HashMap<'Key, 'Value> -> HashMap<'Key, 'U> + +/// +/// Returns an empty hash map with the comparers of a hash map. +/// +/// The input hash map. +/// The empty hash map; the input itself when it is empty. +/// Thrown when is . +/// +/// O(1). The function wraps . +/// +[] +val clear : table : HashMap<'Key, 'Value> -> HashMap<'Key, 'Value> + +/// Returns the bindings that satisfy the predicate. +/// The function to test the bindings. +/// The input hash map. +/// The hash map of the bindings that satisfy the predicate, with the comparers of the input; the input itself when every binding does. +/// Thrown when is . +/// O(n log n). +[] +val filter : predicate : ('Key -> 'Value -> bool) -> table : HashMap<'Key, 'Value> -> HashMap<'Key, 'Value> + +/// +/// Returns the results of a function applied to each binding, under the keys of the bindings. +/// +/// The function to transform the bindings. +/// The input hash map. +/// +/// The hash map of the results, with the key comparer of the input and the default comparer of the new values. +/// +/// Thrown when is . +/// +/// O(n log n). +/// +[] +val map : mapping : ('Key -> 'Value -> 'U) -> table : HashMap<'Key, 'Value> -> HashMap<'Key, 'U> + +/// Returns a hash map without the binding of a key. +/// The key to remove. +/// The input hash map. +/// The hash map without the binding; the input itself when it has no binding of the key. +/// Thrown when is . +/// O(log n). +[] +val remove : key : 'Key -> table : HashMap<'Key, 'Value> -> HashMap<'Key, 'Value> + +/// +/// Returns a hash map without the bindings of keys. +/// +/// The keys to remove; a key without a binding is skipped. +/// The input hash map. +/// The hash map without the bindings. +/// Thrown when is or a default flat list, or is . +/// +/// O(m log n). The function wraps +/// . +/// +[] +val removeRange : keys : seq<'Key> -> table : HashMap<'Key, 'Value> -> HashMap<'Key, 'Value> + +/// +/// Returns a hash map with a binding of a key to a value, which replaces an existing binding of the key. +/// +/// The key. +/// The value. +/// The input hash map. +/// The hash map with the binding; the input itself when the binding stays as it is. +/// Thrown when is . +/// +/// O(log n). The function wraps and +/// replaces the value of a key, as Map.add does, where +/// +/// throws for a key that is bound to another value. When the value comparer of the hash map finds the value equal to the +/// current value of the key, the binding stays as it is, its key included (ADR 0001, D14). +/// +[] +val setItem : key : 'Key -> value : 'Value -> table : HashMap<'Key, 'Value> -> HashMap<'Key, 'Value> + +/// +/// Returns a hash map with the bindings of key/value pairs, which replace the existing bindings of their keys. +/// +/// The key/value pairs to set; a later pair of a key replaces an earlier one. +/// The input hash map. +/// The hash map with the bindings. +/// Thrown when is or a default flat list, or is . +/// +/// O(m log(n + m)). The function wraps +/// . +/// A pair whose value the value comparer finds equal to the current value of its key, or to the value of an earlier pair +/// of the key, leaves that binding as it is, its key included (ADR 0001, D14). +/// and +/// set tuples. +/// +[] +val setItems : pairs : seq> -> table : HashMap<'Key, 'Value> -> HashMap<'Key, 'Value> + +// ---- Folding ---- + +/// +/// Applies a function to a state and each binding, in the order of enumeration. +/// +/// The function that computes the next state. +/// The initial state. +/// The input hash map. +/// The final state. +/// Thrown when is . +/// +/// O(n). +/// +[] +val fold : folder : ('State -> 'Key -> 'Value -> 'State) -> state : 'State -> table : HashMap<'Key, 'Value> -> 'State + +/// +/// Applies a function to each binding and a state, in the reverse order of enumeration. +/// +/// The function that computes the next state. +/// The input hash map. +/// The initial state. +/// The final state. +/// Thrown when is . +/// +/// O(n). The function visits the bindings from the last one that the hash map enumerates to the first one, so it sees +/// them in the reverse order of +/// , +/// not in the order of the keys. It copies the bindings first. +/// +[] +val foldBack : folder : ('Key -> 'Value -> 'State -> 'State) -> table : HashMap<'Key, 'Value> -> state : 'State -> 'State + +/// +/// Applies an action to each binding, in the order of enumeration. +/// +/// The action. +/// The input hash map. +/// Thrown when is . +/// +/// O(n). +/// +[] +val iter : action : ('Key -> 'Value -> unit) -> table : HashMap<'Key, 'Value> -> unit + +// ---- Partitioning ---- + +/// Splits the bindings by a predicate. +/// The function to test the bindings. +/// The input hash map. +/// +/// The hash map of the bindings that satisfy the predicate and the hash map of the other ones, both with the comparers of +/// the input. +/// +/// Thrown when is . +/// O(n log n). +[] +val partition : + predicate : ('Key -> 'Value -> bool) -> + table : HashMap<'Key, 'Value> -> + struct (HashMap<'Key, 'Value> * HashMap<'Key, 'Value>) + +// ---- Conversion ---- + +/// +/// Returns a new array of the bindings, in the order of enumeration. +/// +/// The input hash map. +/// The array of key/value pairs. +/// Thrown when is . +/// +/// O(n). +/// +[] +val toArray : table : HashMap<'Key, 'Value> -> struct ('Key * 'Value) array + +/// Returns a list of the bindings, in the order of enumeration. +/// The input hash map. +/// The list of key/value pairs. +/// Thrown when is . +/// O(n). +[] +val toList : table : HashMap<'Key, 'Value> -> struct ('Key * 'Value) list + +/// +/// Returns the bindings as a sequence, in the order of enumeration. +/// +/// The input hash map. +/// The sequence of key/value pairs, computed on demand. +/// Thrown when is . +/// +/// O(1); O(n) to enumerate. +/// +[] +val toSeq : table : HashMap<'Key, 'Value> -> seq + +/// +/// The functions of that take bindings, with reference tuples instead of +/// key/value pairs. +/// +/// +/// A function of the module takes one shape of the bindings, so the shape of the tuples of most F# code has a module of its +/// own (ADR 0001, D13): HashMap.Tuple.addRange pairs table. The functions resolve after +/// open FSharp.Collections.Immutable.Tuple too, whose HashMap module has no nested module of this name. +/// +[] +module Tuple = + + // ---- Transformation ---- + + /// + /// Returns a hash map with the bindings of key/value tuples added. + /// + /// The key/value tuples to add. + /// The input hash map. + /// The hash map with the bindings. + /// Thrown when is or a default flat list, or is . + /// + /// Thrown when a tuple has the key of a binding of the hash map or of another tuple, and a value that the value + /// comparer finds different. + /// + /// + /// O(m log(n + m)). The function wraps + /// , + /// which rejects another value of a key and accepts a tuple whose value the value comparer finds equal to the value of + /// the key, leaving that binding as it is, its key included (ADR 0001, D14). + /// + [] + val addRange : pairs : seq<'Key * 'Value> -> table : HashMap<'Key, 'Value> -> HashMap<'Key, 'Value> + + /// + /// Returns a hash map with the bindings of key/value tuples, which replace the existing bindings of their keys. + /// + /// The key/value tuples to set; a later tuple of a key replaces an earlier one. + /// The input hash map. + /// The hash map with the bindings. + /// Thrown when is or a default flat list, or is . + /// + /// O(m log(n + m)). The function wraps + /// . + /// A tuple whose value the value comparer finds equal to the current value of its key, or to the value of an earlier + /// tuple of the key, leaves that binding as it is, its key included (ADR 0001, D14). + /// + [] + val setItems : pairs : seq<'Key * 'Value> -> table : HashMap<'Key, 'Value> -> HashMap<'Key, 'Value> + +/// +/// The functions of that take bindings, with struct tuples instead of +/// key/value pairs. +/// +/// +/// A function of the module takes one shape of the bindings, so the struct tuples of ADR 0001, D3 have a module of their own +/// (D13): HashMap.ValueTuple.setItems pairs table. +/// +[] +module ValueTuple = + + // ---- Transformation ---- + + /// + /// Returns a hash map with the bindings of key/value tuples added. + /// + /// The key/value tuples to add. + /// The input hash map. + /// The hash map with the bindings. + /// Thrown when is or a default flat list, or is . + /// + /// Thrown when a tuple has the key of a binding of the hash map or of another tuple, and a value that the value + /// comparer finds different. + /// + /// + /// O(m log(n + m)). The function wraps + /// , + /// which rejects another value of a key and accepts a tuple whose value the value comparer finds equal to the value of + /// the key, leaving that binding as it is, its key included (ADR 0001, D14). + /// + [] + val addRange : pairs : seq -> table : HashMap<'Key, 'Value> -> HashMap<'Key, 'Value> + + /// + /// Returns a hash map with the bindings of key/value tuples, which replace the existing bindings of their keys. + /// + /// The key/value tuples to set; a later tuple of a key replaces an earlier one. + /// The input hash map. + /// The hash map with the bindings. + /// Thrown when is or a default flat list, or is . + /// + /// O(m log(n + m)). The function wraps + /// . + /// A tuple whose value the value comparer finds equal to the current value of its key, or to the value of an earlier + /// tuple of the key, leaves that binding as it is, its key included (ADR 0001, D14). + /// + [] + val setItems : pairs : seq -> table : HashMap<'Key, 'Value> -> HashMap<'Key, 'Value> diff --git a/src/FSharp.Collections.Immutable/HashMap/HashMapCE.fs b/src/FSharp.Collections.Immutable/HashMap/HashMapCE.fs new file mode 100644 index 00000000..85921c00 --- /dev/null +++ b/src/FSharp.Collections.Immutable/HashMap/HashMapCE.fs @@ -0,0 +1,123 @@ +namespace FSharp.Collections.Immutable + +open System +open System.Collections.Generic +open System.Collections.Immutable + +type HashMapBuffer<'Key, 'Value when 'Key : not null> = HashMap<'Key, 'Value>.Builder -> unit + +[] +type HashMapCE<'Key when 'Key : not null> (keyComparer : IEqualityComparer<'Key>) = + + do checkNotNull (nameof keyComparer) keyComparer + + new () = HashMapCE<'Key>(EqualityComparer<'Key>.Default) + + member inline _.Zero () : HashMapBuffer<'Key, 'Value> = ignore + + // Every binding goes through the indexer of the builder, which keeps the earlier binding of a key for a value that the + // value comparer finds equal (ADR 0001, D14) + member inline _.Yield (pair : struct ('Key * 'Value)) : HashMapBuffer<'Key, 'Value> = + fun builder -> + let struct (key, value) = pair + builder[key] <- value + + member inline _.Yield (pair : KeyValuePair<'Key, 'Value>) : HashMapBuffer<'Key, 'Value> = + fun builder -> builder[pair.Key] <- pair.Value + + member inline _.YieldFrom (pairs : seq) : HashMapBuffer<'Key, 'Value> = + // A default flat list given as a sequence fails when it is enumerated; the expression rejects it, as the functions + // of the module do (ADR 0001, D6), when yield! runs and not when the buffer does + checkNotNullOrDefaultSeq (nameof pairs) pairs + + fun builder -> + for struct (key, value) in pairs do + builder[key] <- value + + member inline _.YieldFrom (pairs : struct ('Key * 'Value) array) : HashMapBuffer<'Key, 'Value> = + // A null array fails when the buffer runs; the expression rejects it when yield! runs, as it rejects a sequence + checkNotNull (nameof pairs) pairs + + fun builder -> + for struct (key, value) in pairs do + builder[key] <- value + + member inline _.YieldFrom (pairs : seq>) : HashMapBuffer<'Key, 'Value> = + checkNotNullOrDefaultSeq (nameof pairs) pairs + + fun builder -> + for pair in pairs do + builder[pair.Key] <- pair.Value + + member inline _.YieldFrom (table : HashMap<'Key, 'Value>) : HashMapBuffer<'Key, 'Value> = + // A null hash map fails when the buffer runs; the expression rejects it when yield! runs, as it rejects a sequence + checkNotNull (nameof table) table + + fun builder -> + for pair in table do + builder[pair.Key] <- pair.Value + + member inline _.YieldFrom (buffer : HashMapBuffer<'Key, 'Value>) : HashMapBuffer<'Key, 'Value> = buffer + + member inline _.Combine + (first : HashMapBuffer<'Key, 'Value>, second : HashMapBuffer<'Key, 'Value>) + : HashMapBuffer<'Key, 'Value> + = + fun builder -> + first builder + second builder + + member inline _.Delay ([] delayed : unit -> HashMapBuffer<'Key, 'Value>) : HashMapBuffer<'Key, 'Value> = + fun builder -> delayed () builder + + member inline _.For + (source : seq<'T>, [] body : 'T -> HashMapBuffer<'Key, 'Value>) + : HashMapBuffer<'Key, 'Value> + = + // A default flat list is a sequence that fails when it is enumerated; the expression rejects it as yield! does + checkNotNullOrDefaultSeq (nameof source) source + + fun builder -> + for item in source do + body item builder + + member inline _.While + ([] guard : unit -> bool, body : HashMapBuffer<'Key, 'Value>) + : HashMapBuffer<'Key, 'Value> + = + fun builder -> + while guard () do + body builder + + member inline _.TryWith + (body : HashMapBuffer<'Key, 'Value>, [] handler : exn -> HashMapBuffer<'Key, 'Value>) + : HashMapBuffer<'Key, 'Value> + = + fun builder -> + try + body builder + with ex -> + handler ex builder + + member inline _.TryFinally + (body : HashMapBuffer<'Key, 'Value>, [] compensation : unit -> unit) + : HashMapBuffer<'Key, 'Value> + = + fun builder -> + try + body builder + finally + compensation () + + member inline _.Using + (resource : 'Resource, [] body : 'Resource -> HashMapBuffer<'Key, 'Value>) + : HashMapBuffer<'Key, 'Value> + = + fun builder -> + use resource = resource + body resource builder + + member _.Run (buffer : HashMapBuffer<'Key, 'Value>) : HashMap<'Key, 'Value> = + let builder = ImmutableDictionary.CreateBuilder<'Key, 'Value> keyComparer + buffer builder + builder.ToImmutable () diff --git a/src/FSharp.Collections.Immutable/HashMap/HashMapCE.fsi b/src/FSharp.Collections.Immutable/HashMap/HashMapCE.fsi new file mode 100644 index 00000000..bdcf7b5e --- /dev/null +++ b/src/FSharp.Collections.Immutable/HashMap/HashMapCE.fsi @@ -0,0 +1,143 @@ +namespace FSharp.Collections.Immutable + +open System +open System.Collections.Generic + +/// +/// The body of a hashMap { } expression: a function that sets the bindings in a builder. +/// +type HashMapBuffer<'Key, 'Value when 'Key : not null> = HashMap<'Key, 'Value>.Builder -> unit + +/// +/// The builder of the hashMap { } computation expression. +/// +/// +/// +/// The expression collects its bindings in an +/// (ADR 0001, D7). It yields a binding as a struct pair, yield struct (key, value), or as a +/// . A later binding of a key replaces an earlier one, as in +/// , +/// unless the default comparer of the values finds the two values equal: then the earlier binding stays, its key +/// included, as the indexer of the builder keeps it (ADR 0001, D14). +/// +/// +/// The keys compare with the key comparer that the constructor takes, or with +/// when it is omitted; the values compare with the +/// default comparer. The builder is generic in the type of the keys, so that it can hold a comparer of them, and each +/// hashMap { } expression infers the type of its keys. HashMapCE StringComparer.OrdinalIgnoreCase { yield struct ("a", 1) } +/// builds a map whose keys ignore the case. +/// +/// +[] +type HashMapCE<'Key when 'Key : not null> = + + /// + /// Creates the builder of hash maps whose keys compare with a key comparer. + /// + /// The equality comparer of the keys of the hash map. + /// Thrown when is . + new : keyComparer : IEqualityComparer<'Key> -> HashMapCE<'Key> + + /// + /// Creates the builder of hash maps whose keys compare with . + /// + new : unit -> HashMapCE<'Key> + + /// An expression without bindings. + member inline Zero : unit -> HashMapBuffer<'Key, 'Value> + + /// + /// Sets a binding given as a struct pair of a key and a value. + /// + /// + /// A value that the default comparer of the values finds equal to the value of an earlier binding of the key keeps the + /// earlier binding, its key included (ADR 0001, D14). + /// + member inline Yield : pair : struct ('Key * 'Value) -> HashMapBuffer<'Key, 'Value> + + /// + /// Sets a binding given as a key/value pair. + /// + /// + /// A value that the default comparer of the values finds equal to the value of an earlier binding of the key keeps the + /// earlier binding, its key included (ADR 0001, D14). + /// + member inline Yield : pair : KeyValuePair<'Key, 'Value> -> HashMapBuffer<'Key, 'Value> + + /// + /// Sets the bindings of a sequence of struct pairs. + /// + /// Thrown when the sequence is or a default flat list. + /// + /// A value that the default comparer of the values finds equal to the value of an earlier binding of the key keeps the + /// earlier binding, its key included (ADR 0001, D14). + /// + member inline YieldFrom : pairs : seq -> HashMapBuffer<'Key, 'Value> + + /// + /// Sets the bindings of an array of struct pairs. + /// + /// Thrown when the array is . + /// + /// A value that the default comparer of the values finds equal to the value of an earlier binding of the key keeps the + /// earlier binding, its key included (ADR 0001, D14). + /// + member inline YieldFrom : pairs : struct ('Key * 'Value) array -> HashMapBuffer<'Key, 'Value> + + /// + /// Sets the bindings of a sequence of key/value pairs, such as another dictionary. + /// + /// Thrown when the sequence is or a default flat list. + /// + /// A value that the default comparer of the values finds equal to the value of an earlier binding of the key keeps the + /// earlier binding, its key included (ADR 0001, D14). + /// + member inline YieldFrom : pairs : seq> -> HashMapBuffer<'Key, 'Value> + + /// + /// Sets the bindings of a hash map. + /// + /// Thrown when the hash map is . + /// + /// A value that the default comparer of the values finds equal to the value of an earlier binding of the key keeps the + /// earlier binding, its key included (ADR 0001, D14). + /// + member inline YieldFrom : table : HashMap<'Key, 'Value> -> HashMapBuffer<'Key, 'Value> + + /// Sets the bindings of another expression body. + member inline YieldFrom : buffer : HashMapBuffer<'Key, 'Value> -> HashMapBuffer<'Key, 'Value> + + /// Sets the bindings of two parts of the expression, in order. + member inline Combine : + first : HashMapBuffer<'Key, 'Value> * second : HashMapBuffer<'Key, 'Value> -> HashMapBuffer<'Key, 'Value> + + /// + /// Runs the body of the expression each time the bindings are collected. + /// + member inline Delay : [] delayed : (unit -> HashMapBuffer<'Key, 'Value>) -> HashMapBuffer<'Key, 'Value> + + /// Sets the bindings of the body for each item of a sequence. + /// Thrown when the sequence is or a default flat list. + member inline For : + source : seq<'T> * [] body : ('T -> HashMapBuffer<'Key, 'Value>) -> HashMapBuffer<'Key, 'Value> + + /// Sets the bindings of the body while the guard holds. + member inline While : + [] guard : (unit -> bool) * body : HashMapBuffer<'Key, 'Value> -> HashMapBuffer<'Key, 'Value> + + /// Handles the exceptions of the body. + member inline TryWith : + body : HashMapBuffer<'Key, 'Value> * [] handler : (exn -> HashMapBuffer<'Key, 'Value>) -> + HashMapBuffer<'Key, 'Value> + + /// Runs the compensation after the body. + member inline TryFinally : + body : HashMapBuffer<'Key, 'Value> * [] compensation : (unit -> unit) -> HashMapBuffer<'Key, 'Value> + + /// Disposes a resource after the body. + member inline Using : + resource : 'Resource * [] body : ('Resource -> HashMapBuffer<'Key, 'Value>) -> HashMapBuffer<'Key, 'Value> + when 'Resource :> IDisposable | null + + /// Collects the bindings of the expression. + member Run : buffer : HashMapBuffer<'Key, 'Value> -> HashMap<'Key, 'Value> diff --git a/src/FSharp.Collections.Immutable/HashMap/HashMapOption.fs b/src/FSharp.Collections.Immutable/HashMap/HashMapOption.fs new file mode 100644 index 00000000..90d29890 --- /dev/null +++ b/src/FSharp.Collections.Immutable/HashMap/HashMapOption.fs @@ -0,0 +1,48 @@ +[] +module FSharp.Collections.Immutable.Option.HashMap + +open FSharp.Collections.Immutable + +// FSharp.Core has no function over an immutable dictionary, so the functions delegate to the default module and convert +// the options at its boundary: the functions that it calls get value options, and its results become options. + +// ---- Access ---- + +[] +let tryFind (key : 'Key) (table : HashMap<'Key, 'Value>) = + FSharp.Collections.Immutable.HashMap.tryFind key table + |> ValueOption.toOption + +[] +let tryGetKey (key : 'Key) (table : HashMap<'Key, 'Value>) = + FSharp.Collections.Immutable.HashMap.tryGetKey key table + |> ValueOption.toOption + +// ---- Search ---- + +[] +let pick (chooser : 'Key -> 'Value -> 'U option) (table : HashMap<'Key, 'Value>) = + FSharp.Collections.Immutable.HashMap.pick (fun key value -> chooser key value |> ValueOption.ofOption) table + +[] +let tryFindKey predicate (table : HashMap<'Key, 'Value>) = + FSharp.Collections.Immutable.HashMap.tryFindKey predicate table + |> ValueOption.toOption + +[] +let tryPick (chooser : 'Key -> 'Value -> 'U option) (table : HashMap<'Key, 'Value>) = + FSharp.Collections.Immutable.HashMap.tryPick (fun key value -> chooser key value |> ValueOption.ofOption) table + |> ValueOption.toOption + +// ---- Transformation ---- + +[] +let change (key : 'Key) (f : 'Value option -> 'Value option) (table : HashMap<'Key, 'Value>) = + FSharp.Collections.Immutable.HashMap.change + key + (fun current -> f (ValueOption.toOption current) |> ValueOption.ofOption) + table + +[] +let choose (chooser : 'Key -> 'Value -> 'U option) (table : HashMap<'Key, 'Value>) = + FSharp.Collections.Immutable.HashMap.choose (fun key value -> chooser key value |> ValueOption.ofOption) table diff --git a/src/FSharp.Collections.Immutable/HashMap/HashMapOption.fsi b/src/FSharp.Collections.Immutable/HashMap/HashMapOption.fsi new file mode 100644 index 00000000..7d0690cf --- /dev/null +++ b/src/FSharp.Collections.Immutable/HashMap/HashMapOption.fsi @@ -0,0 +1,120 @@ +/// +/// The functions of that take or return an optional +/// value, with instead of +/// . +/// +/// +/// After open FSharp.Collections.Immutable.Option, these functions shadow the default ones, and the other functions +/// of the default module stay available (ADR 0001, D3). Each function has the signature of the function with the same +/// name in where that module has one. +/// +[] +module FSharp.Collections.Immutable.Option.HashMap + +open FSharp.Collections.Immutable + +// ---- Access ---- + +/// +/// Returns the value of a key, or None when the hash map has no binding of the key. +/// +/// The key to find. +/// The input hash map. +/// The value of the key, or None. +/// Thrown when is . +/// +/// O(log n). +/// +[] +val tryFind : key : 'Key -> table : HashMap<'Key, 'Value> -> 'Value option + +/// +/// Returns the key of the hash map that equals a key in the key comparer, or None. +/// +/// The key to find. +/// The input hash map. +/// The key that the hash map holds, which can differ from the given one, or None. +/// Thrown when is . +/// +/// O(log n). The function wraps . +/// +[] +val tryGetKey : key : 'Key -> table : HashMap<'Key, 'Value> -> 'Key option + +// ---- Search ---- + +/// +/// Returns the first value, in the order of enumeration, that the chooser returns for the bindings. +/// +/// The function that returns a value, or None to go on. +/// The input hash map. +/// The first chosen value. +/// Thrown when is . +/// Thrown when the chooser returns None for every binding. +/// +/// O(n). +/// +[] +val pick : chooser : ('Key -> 'Value -> 'U option) -> table : HashMap<'Key, 'Value> -> 'U + +/// +/// Returns the key of the first binding, in the order of enumeration, that satisfies the predicate, or None. +/// +/// The function to test the bindings. +/// The input hash map. +/// The key of the first binding that satisfies the predicate, or None. +/// Thrown when is . +/// +/// O(n). +/// +[] +val tryFindKey : predicate : ('Key -> 'Value -> bool) -> table : HashMap<'Key, 'Value> -> 'Key option + +/// +/// Returns the first value, in the order of enumeration, that the chooser returns for the bindings, or None. +/// +/// The function that returns a value, or None to go on. +/// The input hash map. +/// The first chosen value, or None. +/// Thrown when is . +/// +/// O(n). +/// +[] +val tryPick : chooser : ('Key -> 'Value -> 'U option) -> table : HashMap<'Key, 'Value> -> 'U option + +// ---- Transformation ---- + +/// +/// Returns a hash map with the binding of a key changed by a function of its current value. +/// +/// The key. +/// +/// The function that takes the current value or None, and returns the new value or None to remove the +/// binding. +/// +/// The input hash map. +/// The hash map with the changed binding; the input itself when nothing changes. +/// Thrown when is . +/// +/// O(log n). When returns a value that the value comparer of the hash map finds equal to the current +/// value, the binding stays as it is, its key included, as in +/// (ADR 0001, D14). +/// +[] +val change : key : 'Key -> f : ('Value option -> 'Value option) -> table : HashMap<'Key, 'Value> -> HashMap<'Key, 'Value> + +/// +/// Returns a hash map of the values that the chooser returns for the bindings, under their keys. +/// +/// The function that returns the new value, or None to drop the binding. +/// The input hash map. +/// +/// The hash map of the chosen values, with the key comparer of the input and the default comparer of the new values. +/// +/// Thrown when is . +/// +/// O(n log n). +/// +[] +val choose : chooser : ('Key -> 'Value -> 'U option) -> table : HashMap<'Key, 'Value> -> HashMap<'Key, 'U> diff --git a/src/FSharp.Collections.Immutable/HashMap/HashMapTuple.fs b/src/FSharp.Collections.Immutable/HashMap/HashMapTuple.fs new file mode 100644 index 00000000..c3172dca --- /dev/null +++ b/src/FSharp.Collections.Immutable/HashMap/HashMapTuple.fs @@ -0,0 +1,31 @@ +[] +module FSharp.Collections.Immutable.Tuple.HashMap + +open FSharp.Collections.Immutable + +// The conversions share their algorithm with the default module and only pass another tuple constructor; `partition` +// delegates to the default module and repacks its pair. + +// ---- Partitioning ---- + +[] +let partition predicate (table : HashMap<'Key, 'Value>) = + let struct (accepted, rejected) = FSharp.Collections.Immutable.HashMap.partition predicate table + accepted, rejected + +// ---- Conversion ---- + +[] +let toArray (table : HashMap<'Key, 'Value>) = + checkNotNull (nameof table) table + Dictionaries.toArray referencePair table + +[] +let toList (table : HashMap<'Key, 'Value>) = + checkNotNull (nameof table) table + Dictionaries.toList referencePair table + +[] +let toSeq (table : HashMap<'Key, 'Value>) = + checkNotNull (nameof table) table + Dictionaries.toSeq referencePair table diff --git a/src/FSharp.Collections.Immutable/HashMap/HashMapTuple.fsi b/src/FSharp.Collections.Immutable/HashMap/HashMapTuple.fsi new file mode 100644 index 00000000..fd91e298 --- /dev/null +++ b/src/FSharp.Collections.Immutable/HashMap/HashMapTuple.fsi @@ -0,0 +1,69 @@ +/// +/// The functions of that return tuples, with reference tuples instead of +/// struct tuples. +/// +/// +/// +/// After open FSharp.Collections.Immutable.Tuple, these functions shadow the default ones, and the other functions +/// of the default module stay available (ADR 0001, D3). Each function has the signature of the function with the same +/// name in . +/// +/// +/// The functions that take bindings take every shape without this module (D13): HashMap.ofSeq is overloaded for +/// reference tuples, and the nested module of the default module +/// holds the functions that change a map by reference tuples. +/// +/// +[] +module FSharp.Collections.Immutable.Tuple.HashMap + +open FSharp.Collections.Immutable + +// ---- Partitioning ---- + +/// Splits the bindings by a predicate. +/// The function to test the bindings. +/// The input hash map. +/// +/// The hash map of the bindings that satisfy the predicate and the hash map of the other ones, both with the comparers of +/// the input. +/// +/// Thrown when is . +/// O(n log n). +[] +val partition : + predicate : ('Key -> 'Value -> bool) -> table : HashMap<'Key, 'Value> -> HashMap<'Key, 'Value> * HashMap<'Key, 'Value> + +// ---- Conversion ---- + +/// +/// Returns a new array of the bindings, in the order of enumeration. +/// +/// The input hash map. +/// The array of key/value pairs. +/// Thrown when is . +/// +/// O(n). +/// +[] +val toArray : table : HashMap<'Key, 'Value> -> ('Key * 'Value) array + +/// Returns a list of the bindings, in the order of enumeration. +/// The input hash map. +/// The list of key/value pairs. +/// Thrown when is . +/// O(n). +[] +val toList : table : HashMap<'Key, 'Value> -> ('Key * 'Value) list + +/// +/// Returns the bindings as a sequence, in the order of enumeration. +/// +/// The input hash map. +/// The sequence of key/value pairs, computed on demand. +/// Thrown when is . +/// +/// O(1); O(n) to enumerate. +/// +[] +val toSeq : table : HashMap<'Key, 'Value> -> seq<'Key * 'Value> diff --git a/src/FSharp.Collections.Immutable/HashMap/HashMapTypes.fs b/src/FSharp.Collections.Immutable/HashMap/HashMapTypes.fs new file mode 100644 index 00000000..829fd2a6 --- /dev/null +++ b/src/FSharp.Collections.Immutable/HashMap/HashMapTypes.fs @@ -0,0 +1,24 @@ +namespace FSharp.Collections.Immutable + +open System.Collections.Immutable + +// The abbreviations have their own file because the modules of the folder are root modules, which cannot declare a type +// of the namespace; a file of abbreviations needs no signature file (ADR 0001, D2). + +/// +/// An immutable hash map, . +/// +/// +/// The map finds a key by its hash code in an equality comparer. It enumerates its bindings in an order that depends on the +/// hash codes: the order stays the same for the same map, but it is not the order of the keys. +/// +type HashMap<'Key, 'Value when 'Key : not null> = ImmutableDictionary<'Key, 'Value> + +/// +/// An immutable map of any implementation, . +/// +/// +/// and +/// implement the interface. +/// +type IMap<'Key, 'Value> = IImmutableDictionary<'Key, 'Value> diff --git a/src/FSharp.Collections.Immutable/HashMap/IMap.fs b/src/FSharp.Collections.Immutable/HashMap/IMap.fs new file mode 100644 index 00000000..98307ecc --- /dev/null +++ b/src/FSharp.Collections.Immutable/HashMap/IMap.fs @@ -0,0 +1,286 @@ +[] +module FSharp.Collections.Immutable.IMap + +open System.Collections.Generic +open System.Collections.Immutable + +// The functions call the members of the interface where it has them (ADR 0001, D5) and visit the bindings with the +// algorithms that the map modules share (`Dictionaries` in Helper/Dictionaries.fs). The interface hides the comparers of +// a map, so the transformations that keep the type of the values build their result with `Clear` and `AddRange` of the +// map itself, which keep its implementation and its comparers; the ones that change the type of the values build a hash +// map with the default comparers (D5, comparers). + +// ---- Properties ---- + +[] +let count (table : IMap<'Key, 'Value>) = + checkNotNull (nameof table) table + table.Count + +[] +let isEmpty (table : IMap<'Key, 'Value>) = + checkNotNull (nameof table) table + table.Count = 0 + +[] +let keys (table : IMap<'Key, 'Value>) = + checkNotNull (nameof table) table + table.Keys + +[] +let values (table : IMap<'Key, 'Value>) = + checkNotNull (nameof table) table + table.Values + +// ---- Access ---- + +[] +let find (key : 'Key) (table : IMap<'Key, 'Value>) = + checkNotNull (nameof table) table + + match table.TryGetValue key with + | true, value -> value + | false, _ -> Raise.keyNotFound () + +[] +let tryFind (key : 'Key) (table : IMap<'Key, 'Value>) = + checkNotNull (nameof table) table + + match table.TryGetValue key with + | true, value -> ValueSome value + | false, _ -> ValueNone + +[] +let tryGetKey (key : 'Key) (table : IMap<'Key, 'Value>) = + checkNotNull (nameof table) table + + match table.TryGetKey key with + | true, actualKey -> ValueSome actualKey + | false, _ -> ValueNone + +// ---- Search ---- + +[] +let containsBinding (key : 'Key) (value : 'Value) (table : IMap<'Key, 'Value>) = + checkNotNull (nameof table) table + // The interface hides the value comparer of the map, so only the member of the interface can decide whether the stored + // value equals the value + table.Contains (KeyValuePair (key, value)) + +[] +let containsKey (key : 'Key) (table : IMap<'Key, 'Value>) = + checkNotNull (nameof table) table + table.ContainsKey key + +[] +let exists predicate (table : IMap<'Key, 'Value>) = + checkNotNull (nameof table) table + Dictionaries.exists predicate table + +[] +let findKey predicate (table : IMap<'Key, 'Value>) = + checkNotNull (nameof table) table + + // Map.findKey searches with Seq.pick, whose message differs from the one of Map.find + match Dictionaries.tryFindKey predicate table with + | ValueSome key -> key + | ValueNone -> Raise.indexNotFound () + +[] +let forall predicate (table : IMap<'Key, 'Value>) = + checkNotNull (nameof table) table + Dictionaries.forall predicate table + +[] +let pick (chooser : 'Key -> 'Value -> 'U voption) (table : IMap<'Key, 'Value>) = + checkNotNull (nameof table) table + + match Dictionaries.tryPick chooser table with + | ValueSome value -> value + | ValueNone -> Raise.keyNotFound () + +[] +let tryFindKey predicate (table : IMap<'Key, 'Value>) = + checkNotNull (nameof table) table + Dictionaries.tryFindKey predicate table + +[] +let tryPick (chooser : 'Key -> 'Value -> 'U voption) (table : IMap<'Key, 'Value>) = + checkNotNull (nameof table) table + Dictionaries.tryPick chooser table + +// ---- Transformation ---- + +// add is IImmutableDictionary.Add, as HashMap.add is ImmutableDictionary.Add: it throws for a key bound to another value +// and returns the map itself for an equal binding. Map.add replaces the value, which setItem does (ADR 0001, D14) +[] +let add (key : 'Key) (value : 'Value) (table : IMap<'Key, 'Value>) = + checkNotNull (nameof table) table + table.Add (key, value) + +[] +let addRange (pairs : seq>) (table : IMap<'Key, 'Value>) = + checkNotNullOrDefaultSeq (nameof pairs) pairs + checkNotNull (nameof table) table + table.AddRange pairs + +[] +let change (key : 'Key) (f : 'Value voption -> 'Value voption) (table : IMap<'Key, 'Value>) = + checkNotNull (nameof table) table + + let current = + match table.TryGetValue key with + | true, value -> ValueSome value + | false, _ -> ValueNone + + match f current with + | ValueSome value -> table.SetItem (key, value) + | ValueNone -> table.Remove key + +[] +let choose (chooser : 'Key -> 'Value -> 'U voption) (table : IMap<'Key, 'Value>) : HashMap<'Key, 'U> = + checkNotNull (nameof table) table + // The interface hides the key comparer of the map, so the new map compares with the default comparers + let builder = ImmutableDictionary.CreateBuilder<'Key, 'U>() + Dictionaries.choose chooser builder table + builder.ToImmutable () + +[] +let clear (table : IMap<'Key, 'Value>) = + checkNotNull (nameof table) table + table.Clear () + +[] +let filter predicate (table : IMap<'Key, 'Value>) = + checkNotNull (nameof table) table + let accepted = ResizeArray>() + + for pair in table do + if predicate pair.Key pair.Value then + accepted.Add pair + + if accepted.Count = table.Count then + table + else + table.Clear().AddRange accepted + +[] +let map (mapping : 'Key -> 'Value -> 'U) (table : IMap<'Key, 'Value>) : HashMap<'Key, 'U> = + checkNotNull (nameof table) table + let builder = ImmutableDictionary.CreateBuilder<'Key, 'U>() + Dictionaries.map mapping builder table + builder.ToImmutable () + +[] +let remove (key : 'Key) (table : IMap<'Key, 'Value>) = + checkNotNull (nameof table) table + table.Remove key + +[] +let removeRange (keys : seq<'Key>) (table : IMap<'Key, 'Value>) = + checkNotNullOrDefaultSeq (nameof keys) keys + checkNotNull (nameof table) table + table.RemoveRange keys + +[] +let setItem (key : 'Key) (value : 'Value) (table : IMap<'Key, 'Value>) = + checkNotNull (nameof table) table + table.SetItem (key, value) + +[] +let setItems (pairs : seq>) (table : IMap<'Key, 'Value>) = + checkNotNullOrDefaultSeq (nameof pairs) pairs + checkNotNull (nameof table) table + table.SetItems pairs + +// ---- Folding ---- + +[] +let fold (folder : 'State -> 'Key -> 'Value -> 'State) (state : 'State) (table : IMap<'Key, 'Value>) = + checkNotNull (nameof table) table + Dictionaries.fold folder state table + +[] +let foldBack (folder : 'Key -> 'Value -> 'State -> 'State) (table : IMap<'Key, 'Value>) (state : 'State) = + checkNotNull (nameof table) table + Dictionaries.foldBack folder table state + +[] +let iter action (table : IMap<'Key, 'Value>) = + checkNotNull (nameof table) table + Dictionaries.iter action table + +// ---- Partitioning ---- + +[] +let partition predicate (table : IMap<'Key, 'Value>) = + checkNotNull (nameof table) table + let accepted = ResizeArray>() + let rejected = ResizeArray>() + + for pair in table do + if predicate pair.Key pair.Value then + accepted.Add pair + else + rejected.Add pair + + let empty = table.Clear () + let acceptedTable = + if rejected.Count = 0 then + table + else + empty.AddRange accepted + struct (acceptedTable, empty.AddRange rejected) + +// ---- Conversion ---- + +[] +let toArray (table : IMap<'Key, 'Value>) = + checkNotNull (nameof table) table + Dictionaries.toArray structPair table + +[] +let toList (table : IMap<'Key, 'Value>) = + checkNotNull (nameof table) table + Dictionaries.toList structPair table + +[] +let toSeq (table : IMap<'Key, 'Value>) = + checkNotNull (nameof table) table + Dictionaries.toSeq structPair table + +// The submodules take the bindings as tuples, convert them to key/value pairs on demand and call the members that the +// functions of the module call, so that every shape has the semantics of the member of the map (D13). +[] +module Tuple = + + // ---- Transformation ---- + + [] + let addRange (pairs : seq<'Key * 'Value>) (table : IMap<'Key, 'Value>) = + checkNotNullOrDefaultSeq (nameof pairs) pairs + checkNotNull (nameof table) table + table.AddRange (Dictionaries.toKeyValuePairs Dictionaries.splitTuple pairs) + + [] + let setItems (pairs : seq<'Key * 'Value>) (table : IMap<'Key, 'Value>) = + checkNotNullOrDefaultSeq (nameof pairs) pairs + checkNotNull (nameof table) table + table.SetItems (Dictionaries.toKeyValuePairs Dictionaries.splitTuple pairs) + +[] +module ValueTuple = + + // ---- Transformation ---- + + [] + let addRange (pairs : seq) (table : IMap<'Key, 'Value>) = + checkNotNullOrDefaultSeq (nameof pairs) pairs + checkNotNull (nameof table) table + table.AddRange (Dictionaries.toKeyValuePairs id pairs) + + [] + let setItems (pairs : seq) (table : IMap<'Key, 'Value>) = + checkNotNullOrDefaultSeq (nameof pairs) pairs + checkNotNull (nameof table) table + table.SetItems (Dictionaries.toKeyValuePairs id pairs) diff --git a/src/FSharp.Collections.Immutable/HashMap/IMap.fsi b/src/FSharp.Collections.Immutable/HashMap/IMap.fsi new file mode 100644 index 00000000..a59440f5 --- /dev/null +++ b/src/FSharp.Collections.Immutable/HashMap/IMap.fsi @@ -0,0 +1,646 @@ +/// +/// Functions for , an immutable map of any implementation. +/// +/// +/// +/// The module mirrors : each function with a name of that module has +/// its semantics, its argument checks and its exceptions (ADR 0001), except +/// . +/// It is the of the interface: it throws for +/// a key that is bound to another value, where Map.add replaces the value, and +/// +/// replaces it (ADR 0001, D14). The other functions wrap a member of +/// and keep its semantics and its exceptions. +/// +/// +/// The functions that visit the bindings one by one follow the order in which the map enumerates them, where +/// follows the order of the keys. +/// +/// +/// The interface does not expose the comparers of a map. The transformations that keep the type of the values call the +/// members of the map, so that the result keeps its implementation and its comparers. The transformations that change the +/// type of the values return a with the default comparers. +/// +/// +/// The immutable dictionaries leave a binding as it is, its key included, when their value comparer finds a new value of +/// its key equal to the current one (ADR 0001, D14). +/// +/// +/// The module has the functions that the interface supports. It has no creation, builder or comparer functions and no +/// containsValue, because declares none of the +/// members behind them; use or the module of another implementation. +/// The one member of the interface that the pair-wise functions cannot reach, +/// , +/// is . +/// GetValueOrDefault is IMap.tryFind key table |> ValueOption.defaultValue defaultValue, and adding a +/// binding only when the key has none is +/// +/// with a function that keeps an existing value. +/// +/// +/// +/// and +/// take the bindings as key/value pairs, and the nested modules and +/// take reference and struct tuples (ADR 0001, D13). +/// +/// +/// A map or sequence throws , and so does a default flat +/// list that is given as a sequence: it fails when a function enumerates it, so it is as good as a +/// sequence (ADR 0001, D6). +/// +/// +/// Functions that return an optional value return a , and +/// functions that return tuples return struct tuples. The namespaces +/// and contain a module with the same name whose functions use options +/// and reference tuples instead. +/// +/// +[] +module FSharp.Collections.Immutable.IMap + +open System.Collections.Generic +open System.Collections.Immutable + +// ---- Properties ---- + +/// Returns the number of bindings of the map. +/// The input map. +/// The number of bindings. +/// Thrown when is . +/// O(1) for the immutable dictionaries. +[] +val count : table : IMap<'Key, 'Value> -> int + +/// Returns whether the map has no bindings. +/// The input map. +/// when the map is empty. +/// Thrown when is . +/// O(1) for the immutable dictionaries. +[] +val isEmpty : table : IMap<'Key, 'Value> -> bool + +/// +/// Returns the keys of the map. +/// +/// The input map. +/// The keys, in the order of enumeration of the map. +/// Thrown when is . +/// +/// O(1); O(n) to enumerate. The function returns +/// . +/// +[] +val keys : table : IMap<'Key, 'Value> -> seq<'Key> + +/// +/// Returns the values of the map, including the duplicates. +/// +/// The input map. +/// The values, in the order of enumeration of the map. +/// Thrown when is . +/// +/// O(1); O(n) to enumerate. The function returns +/// . +/// +[] +val values : table : IMap<'Key, 'Value> -> seq<'Value> + +// ---- Access ---- + +/// Returns the value of a key. +/// The key to find. +/// The input map. +/// The value of the key. +/// Thrown when is . +/// Thrown when the map has no binding of the key. +/// O(log n) for the immutable dictionaries. +[] +val find : key : 'Key -> table : IMap<'Key, 'Value> -> 'Value + +/// +/// Returns the value of a key, or ValueNone when the map has no binding of the key. +/// +/// The key to find. +/// The input map. +/// The value of the key, or ValueNone. +/// Thrown when is . +/// +/// O(log n) for the immutable dictionaries. The function is the TryGetValue of the interface with a voption +/// instead of the out parameter. +/// +[] +val tryFind : key : 'Key -> table : IMap<'Key, 'Value> -> 'Value voption + +/// +/// Returns the key of the map that equals a key in the key comparer of the map, or ValueNone. +/// +/// The key to find. +/// The input map. +/// The key that the map holds, which can differ from the given one, or ValueNone. +/// Thrown when is . +/// +/// O(log n) for the immutable dictionaries. The function wraps +/// . +/// +[] +val tryGetKey : key : 'Key -> table : IMap<'Key, 'Value> -> 'Key voption + +// ---- Search ---- + +/// +/// Returns whether the map binds a key to a value, comparing the values with the value comparer of the map. +/// +/// The key to find. +/// The value that the key must be bound to. +/// The input map. +/// +/// when the map has a binding of the key whose value its value comparer finds equal to +/// . +/// +/// Thrown when is . +/// +/// +/// O(log n) for the immutable dictionaries. The function wraps +/// . +/// A key without a binding gives . A key is answered by the implementation: +/// leaves it to its key comparer, which gives +/// for the default comparer and throws for a comparer +/// that rejects , and throws +/// . +/// +/// +/// The interface hides the value comparer of the map, so this is the only function that tests a binding with it: finding +/// the value with IMap.tryFind and comparing with = uses F# equality instead. +/// +/// +[] +val containsBinding : key : 'Key -> value : 'Value -> table : IMap<'Key, 'Value> -> bool + +/// Returns whether the map has a binding of a key. +/// The key to find. +/// The input map. +/// when the map has a binding of the key. +/// Thrown when is . +/// O(log n) for the immutable dictionaries. +[] +val containsKey : key : 'Key -> table : IMap<'Key, 'Value> -> bool + +/// Returns whether any binding satisfies the predicate. +/// The function to test the bindings. +/// The input map. +/// when a binding satisfies the predicate. +/// Thrown when is . +/// O(n). +[] +val exists : predicate : ('Key -> 'Value -> bool) -> table : IMap<'Key, 'Value> -> bool + +/// +/// Returns the key of the first binding, in the order of enumeration, that satisfies the predicate. +/// +/// The function to test the bindings. +/// The input map. +/// The key of the first binding that satisfies the predicate. +/// Thrown when is . +/// Thrown when no binding satisfies the predicate. +/// +/// O(n). +/// +[] +val findKey : predicate : ('Key -> 'Value -> bool) -> table : IMap<'Key, 'Value> -> 'Key + +/// Returns whether all bindings satisfy the predicate. +/// The function to test the bindings. +/// The input map. +/// when every binding satisfies the predicate. +/// Thrown when is . +/// O(n). +[] +val forall : predicate : ('Key -> 'Value -> bool) -> table : IMap<'Key, 'Value> -> bool + +/// +/// Returns the first value, in the order of enumeration, that the chooser returns for the bindings. +/// +/// The function that returns a value, or ValueNone to go on. +/// The input map. +/// The first chosen value. +/// Thrown when is . +/// Thrown when the chooser returns ValueNone for every binding. +/// +/// O(n). +/// +[] +val pick : chooser : ('Key -> 'Value -> 'U voption) -> table : IMap<'Key, 'Value> -> 'U + +/// +/// Returns the key of the first binding, in the order of enumeration, that satisfies the predicate, or ValueNone. +/// +/// The function to test the bindings. +/// The input map. +/// The key of the first binding that satisfies the predicate, or ValueNone. +/// Thrown when is . +/// +/// O(n). +/// +[] +val tryFindKey : predicate : ('Key -> 'Value -> bool) -> table : IMap<'Key, 'Value> -> 'Key voption + +/// +/// Returns the first value, in the order of enumeration, that the chooser returns for the bindings, or ValueNone. +/// +/// The function that returns a value, or ValueNone to go on. +/// The input map. +/// The first chosen value, or ValueNone. +/// Thrown when is . +/// +/// O(n). +/// +[] +val tryPick : chooser : ('Key -> 'Value -> 'U voption) -> table : IMap<'Key, 'Value> -> 'U voption + +// ---- Transformation ---- + +/// +/// Returns a map with a binding of a key to a value, for a key that is not bound to another value. +/// +/// The key. +/// The value. +/// The input map. +/// The map with the binding, of the implementation of the input; the input itself when it already has the binding. +/// Thrown when is . +/// +/// Thrown when the map binds the key to a value that its value comparer finds different. +/// +/// +/// +/// O(log n) for the immutable dictionaries. The function wraps +/// and behaves as it does (ADR 0001, D14), +/// as +/// does: it does not replace the value of a key, where Map.add does. When the value comparer of the map finds the +/// value equal to the current value of the key, the binding stays as it is, its key included, and the input itself comes +/// back. +/// +/// +/// Use +/// to replace the value of a key, and +/// +/// to add a binding only when the key has none, or to compute the new value from the current one. +/// +/// +[] +val add : key : 'Key -> value : 'Value -> table : IMap<'Key, 'Value> -> IMap<'Key, 'Value> + +/// +/// Returns a map with the bindings of key/value pairs added. +/// +/// The key/value pairs to add. +/// The input map. +/// The map with the bindings, of the implementation of the input. +/// Thrown when is or a default flat list, or is . +/// +/// Thrown when a pair has the key of a binding of the map or of another pair, and a value that the value comparer of the +/// map finds different. +/// +/// +/// O(m log(n + m)) for the immutable dictionaries. The function wraps +/// . +/// The immutable dictionaries reject another value of a key and accept a pair whose value their value comparer finds +/// equal to the value of the key, leaving that binding as it is, its key included (ADR 0001, D14). +/// and +/// add tuples. +/// +[] +val addRange : pairs : seq> -> table : IMap<'Key, 'Value> -> IMap<'Key, 'Value> + +/// +/// Returns a map with the binding of a key changed by a function of its current value. +/// +/// The key. +/// +/// The function that takes the current value or ValueNone, and returns the new value or ValueNone to remove +/// the binding. +/// +/// The input map. +/// The map with the changed binding, of the implementation of the input. +/// Thrown when is . +/// +/// O(log n) for the immutable dictionaries. When returns a value that their value comparer finds +/// equal to the current value, they leave the binding as it is, its key included, and return the input itself (ADR 0001, +/// D14). +/// +[] +val change : key : 'Key -> f : ('Value voption -> 'Value voption) -> table : IMap<'Key, 'Value> -> IMap<'Key, 'Value> + +/// +/// Returns a hash map of the values that the chooser returns for the bindings, under their keys. +/// +/// The function that returns the new value, or ValueNone to drop the binding. +/// The input map. +/// +/// The hash map of the chosen values, with the default comparers. Keys that are distinct in the map and equal in the +/// default comparer keep the last chosen value, unless the default comparer finds the chosen values equal: then the first +/// of them stays, its key included (ADR 0001, D14). +/// +/// Thrown when is . +/// +/// O(n log n). +/// +[] +val choose : chooser : ('Key -> 'Value -> 'U voption) -> table : IMap<'Key, 'Value> -> HashMap<'Key, 'U> when 'Key : not null + +/// +/// Returns an empty map of the implementation and with the comparers of a map. +/// +/// The input map. +/// The empty map. +/// Thrown when is . +/// +/// O(1) for the immutable dictionaries. The function wraps . +/// +[] +val clear : table : IMap<'Key, 'Value> -> IMap<'Key, 'Value> + +/// +/// Returns the bindings that satisfy the predicate. +/// +/// The function to test the bindings. +/// The input map. +/// +/// The map of the bindings that satisfy the predicate, of the implementation of the input; the input itself when every +/// binding does. +/// +/// Thrown when is . +/// +/// O(n log n) for the immutable dictionaries. The function adds the bindings that stay to the result of +/// . +/// +[] +val filter : predicate : ('Key -> 'Value -> bool) -> table : IMap<'Key, 'Value> -> IMap<'Key, 'Value> + +/// +/// Returns the results of a function applied to each binding, under the keys of the bindings. +/// +/// The function to transform the bindings. +/// The input map. +/// +/// The hash map of the results, with the default comparers. Keys that are distinct in the map and equal in the default +/// comparer keep the last result, unless the default comparer finds the results equal: then the first of them stays, its +/// key included (ADR 0001, D14). +/// +/// Thrown when is . +/// +/// O(n log n). +/// +[] +val map : mapping : ('Key -> 'Value -> 'U) -> table : IMap<'Key, 'Value> -> HashMap<'Key, 'U> when 'Key : not null + +/// Returns a map without the binding of a key. +/// The key to remove. +/// The input map. +/// The map without the binding, of the implementation of the input. +/// Thrown when is . +/// O(log n) for the immutable dictionaries. +[] +val remove : key : 'Key -> table : IMap<'Key, 'Value> -> IMap<'Key, 'Value> + +/// +/// Returns a map without the bindings of keys. +/// +/// The keys to remove; a key without a binding is skipped. +/// The input map. +/// The map without the bindings, of the implementation of the input. +/// Thrown when is or a default flat list, or is . +/// +/// O(m log n) for the immutable dictionaries. The function wraps +/// . +/// +[] +val removeRange : keys : seq<'Key> -> table : IMap<'Key, 'Value> -> IMap<'Key, 'Value> + +/// +/// Returns a map with a binding of a key to a value, which replaces an existing binding of the key. +/// +/// The key. +/// The value. +/// The input map. +/// The map with the binding, of the implementation of the input. +/// Thrown when is . +/// +/// O(log n) for the immutable dictionaries. The function wraps +/// and replaces the value of a key, as +/// Map.add does, where +/// +/// throws for a key that is bound to another value. When the value comparer of an immutable dictionary finds the value +/// equal to the current value of the key, the dictionary leaves the binding as it is, its key included, and returns the +/// input itself (ADR 0001, D14). +/// +[] +val setItem : key : 'Key -> value : 'Value -> table : IMap<'Key, 'Value> -> IMap<'Key, 'Value> + +/// +/// Returns a map with the bindings of key/value pairs, which replace the existing bindings of their keys. +/// +/// The key/value pairs to set; a later pair of a key replaces an earlier one. +/// The input map. +/// The map with the bindings, of the implementation of the input. +/// Thrown when is or a default flat list, or is . +/// +/// O(m log(n + m)) for the immutable dictionaries. The function wraps +/// . +/// The immutable dictionaries leave a binding as it is, its key included, when their value comparer finds the value of a +/// pair equal to the current value of its key or to the value of an earlier pair of the key (ADR 0001, D14). +/// and +/// set tuples. +/// +[] +val setItems : pairs : seq> -> table : IMap<'Key, 'Value> -> IMap<'Key, 'Value> + +// ---- Folding ---- + +/// +/// Applies a function to a state and each binding, in the order of enumeration. +/// +/// The function that computes the next state. +/// The initial state. +/// The input map. +/// The final state. +/// Thrown when is . +/// +/// O(n). +/// +[] +val fold : folder : ('State -> 'Key -> 'Value -> 'State) -> state : 'State -> table : IMap<'Key, 'Value> -> 'State + +/// +/// Applies a function to each binding and a state, in the reverse order of enumeration. +/// +/// The function that computes the next state. +/// The input map. +/// The initial state. +/// The final state. +/// Thrown when is . +/// +/// O(n). The function visits the bindings from the last one that the map enumerates to the first one; for a hash map +/// that is not the order of the keys. It copies the bindings first. +/// +[] +val foldBack : folder : ('Key -> 'Value -> 'State -> 'State) -> table : IMap<'Key, 'Value> -> state : 'State -> 'State + +/// +/// Applies an action to each binding, in the order of enumeration. +/// +/// The action. +/// The input map. +/// Thrown when is . +/// +/// O(n). +/// +[] +val iter : action : ('Key -> 'Value -> unit) -> table : IMap<'Key, 'Value> -> unit + +// ---- Partitioning ---- + +/// Splits the bindings by a predicate. +/// The function to test the bindings. +/// The input map. +/// +/// The map of the bindings that satisfy the predicate and the map of the other ones, both of the implementation of the +/// input. +/// +/// Thrown when is . +/// O(n log n) for the immutable dictionaries. +[] +val partition : + predicate : ('Key -> 'Value -> bool) -> table : IMap<'Key, 'Value> -> struct (IMap<'Key, 'Value> * IMap<'Key, 'Value>) + +// ---- Conversion ---- + +/// +/// Returns a new array of the bindings, in the order of enumeration. +/// +/// The input map. +/// The array of key/value pairs. +/// Thrown when is . +/// +/// O(n). +/// +[] +val toArray : table : IMap<'Key, 'Value> -> struct ('Key * 'Value) array + +/// Returns a list of the bindings, in the order of enumeration. +/// The input map. +/// The list of key/value pairs. +/// Thrown when is . +/// O(n). +[] +val toList : table : IMap<'Key, 'Value> -> struct ('Key * 'Value) list + +/// +/// Returns the bindings as a sequence, in the order of enumeration. +/// +/// The input map. +/// The sequence of key/value pairs, computed on demand. +/// Thrown when is . +/// +/// O(1); O(n) to enumerate. +/// +[] +val toSeq : table : IMap<'Key, 'Value> -> seq + +/// +/// The functions of that take bindings, with reference tuples instead of +/// key/value pairs. +/// +/// +/// A function of the module takes one shape of the bindings, so the shape of the tuples of most F# code has a module of its +/// own (ADR 0001, D13): IMap.Tuple.addRange pairs table. The functions resolve after +/// open FSharp.Collections.Immutable.Tuple too, whose IMap module has no nested module of this name. +/// +[] +module Tuple = + + // ---- Transformation ---- + + /// + /// Returns a map with the bindings of key/value tuples added. + /// + /// The key/value tuples to add. + /// The input map. + /// The map with the bindings, of the implementation of the input. + /// Thrown when is or a default flat list, or is . + /// + /// Thrown when a tuple has the key of a binding of the map or of another tuple, and a value that the value comparer of + /// the map finds different. + /// + /// + /// O(m log(n + m)) for the immutable dictionaries. The function wraps + /// . + /// The immutable dictionaries reject another value of a key and accept a tuple whose value their value comparer finds + /// equal to the value of the key, leaving that binding as it is, its key included (ADR 0001, D14). + /// + [] + val addRange : pairs : seq<'Key * 'Value> -> table : IMap<'Key, 'Value> -> IMap<'Key, 'Value> + + /// + /// Returns a map with the bindings of key/value tuples, which replace the existing bindings of their keys. + /// + /// The key/value tuples to set; a later tuple of a key replaces an earlier one. + /// The input map. + /// The map with the bindings, of the implementation of the input. + /// Thrown when is or a default flat list, or is . + /// + /// O(m log(n + m)) for the immutable dictionaries. The function wraps + /// . + /// The immutable dictionaries leave a binding as it is, its key included, when their value comparer finds the value of + /// a tuple equal to the current value of its key or to the value of an earlier tuple of the key (ADR 0001, D14). + /// + [] + val setItems : pairs : seq<'Key * 'Value> -> table : IMap<'Key, 'Value> -> IMap<'Key, 'Value> + +/// +/// The functions of that take bindings, with struct tuples instead of +/// key/value pairs. +/// +/// +/// A function of the module takes one shape of the bindings, so the struct tuples of ADR 0001, D3 have a module of their own +/// (D13): IMap.ValueTuple.setItems pairs table. +/// +[] +module ValueTuple = + + // ---- Transformation ---- + + /// + /// Returns a map with the bindings of key/value tuples added. + /// + /// The key/value tuples to add. + /// The input map. + /// The map with the bindings, of the implementation of the input. + /// Thrown when is or a default flat list, or is . + /// + /// Thrown when a tuple has the key of a binding of the map or of another tuple, and a value that the value comparer of + /// the map finds different. + /// + /// + /// O(m log(n + m)) for the immutable dictionaries. The function wraps + /// . + /// The immutable dictionaries reject another value of a key and accept a tuple whose value their value comparer finds + /// equal to the value of the key, leaving that binding as it is, its key included (ADR 0001, D14). + /// + [] + val addRange : pairs : seq -> table : IMap<'Key, 'Value> -> IMap<'Key, 'Value> + + /// + /// Returns a map with the bindings of key/value tuples, which replace the existing bindings of their keys. + /// + /// The key/value tuples to set; a later tuple of a key replaces an earlier one. + /// The input map. + /// The map with the bindings, of the implementation of the input. + /// Thrown when is or a default flat list, or is . + /// + /// O(m log(n + m)) for the immutable dictionaries. The function wraps + /// . + /// The immutable dictionaries leave a binding as it is, its key included, when their value comparer finds the value of + /// a tuple equal to the current value of its key or to the value of an earlier tuple of the key (ADR 0001, D14). + /// + [] + val setItems : pairs : seq -> table : IMap<'Key, 'Value> -> IMap<'Key, 'Value> diff --git a/src/FSharp.Collections.Immutable/HashMap/IMapOption.fs b/src/FSharp.Collections.Immutable/HashMap/IMapOption.fs new file mode 100644 index 00000000..7692dcaa --- /dev/null +++ b/src/FSharp.Collections.Immutable/HashMap/IMapOption.fs @@ -0,0 +1,45 @@ +[] +module FSharp.Collections.Immutable.Option.IMap + +open FSharp.Collections.Immutable + +// The functions delegate to the default module and convert the options at its boundary: the functions that it calls get +// value options, and its results become options. + +// ---- Access ---- + +[] +let tryFind (key : 'Key) (table : IMap<'Key, 'Value>) = + FSharp.Collections.Immutable.IMap.tryFind key table + |> ValueOption.toOption + +[] +let tryGetKey (key : 'Key) (table : IMap<'Key, 'Value>) = + FSharp.Collections.Immutable.IMap.tryGetKey key table + |> ValueOption.toOption + +// ---- Search ---- + +[] +let pick (chooser : 'Key -> 'Value -> 'U option) (table : IMap<'Key, 'Value>) = + FSharp.Collections.Immutable.IMap.pick (fun key value -> chooser key value |> ValueOption.ofOption) table + +[] +let tryFindKey predicate (table : IMap<'Key, 'Value>) = + FSharp.Collections.Immutable.IMap.tryFindKey predicate table + |> ValueOption.toOption + +[] +let tryPick (chooser : 'Key -> 'Value -> 'U option) (table : IMap<'Key, 'Value>) = + FSharp.Collections.Immutable.IMap.tryPick (fun key value -> chooser key value |> ValueOption.ofOption) table + |> ValueOption.toOption + +// ---- Transformation ---- + +[] +let change (key : 'Key) (f : 'Value option -> 'Value option) (table : IMap<'Key, 'Value>) = + FSharp.Collections.Immutable.IMap.change key (fun current -> f (ValueOption.toOption current) |> ValueOption.ofOption) table + +[] +let choose (chooser : 'Key -> 'Value -> 'U option) (table : IMap<'Key, 'Value>) = + FSharp.Collections.Immutable.IMap.choose (fun key value -> chooser key value |> ValueOption.ofOption) table diff --git a/src/FSharp.Collections.Immutable/HashMap/IMapOption.fsi b/src/FSharp.Collections.Immutable/HashMap/IMapOption.fsi new file mode 100644 index 00000000..5479d6c5 --- /dev/null +++ b/src/FSharp.Collections.Immutable/HashMap/IMapOption.fsi @@ -0,0 +1,123 @@ +/// +/// The functions of that take or return an optional +/// value, with instead of +/// . +/// +/// +/// After open FSharp.Collections.Immutable.Option, these functions shadow the default ones, and the other functions +/// of the default module stay available (ADR 0001, D3). Each function has the signature of the function with the same +/// name in where that module has one. +/// +[] +module FSharp.Collections.Immutable.Option.IMap + +open FSharp.Collections.Immutable + +// ---- Access ---- + +/// +/// Returns the value of a key, or None when the map has no binding of the key. +/// +/// The key to find. +/// The input map. +/// The value of the key, or None. +/// Thrown when is . +/// +/// O(log n) for the immutable dictionaries. +/// +[] +val tryFind : key : 'Key -> table : IMap<'Key, 'Value> -> 'Value option + +/// +/// Returns the key of the map that equals a key in the key comparer of the map, or None. +/// +/// The key to find. +/// The input map. +/// The key that the map holds, which can differ from the given one, or None. +/// Thrown when is . +/// +/// O(log n) for the immutable dictionaries. The function wraps +/// . +/// +[] +val tryGetKey : key : 'Key -> table : IMap<'Key, 'Value> -> 'Key option + +// ---- Search ---- + +/// +/// Returns the first value, in the order of enumeration, that the chooser returns for the bindings. +/// +/// The function that returns a value, or None to go on. +/// The input map. +/// The first chosen value. +/// Thrown when is . +/// Thrown when the chooser returns None for every binding. +/// +/// O(n). +/// +[] +val pick : chooser : ('Key -> 'Value -> 'U option) -> table : IMap<'Key, 'Value> -> 'U + +/// +/// Returns the key of the first binding, in the order of enumeration, that satisfies the predicate, or None. +/// +/// The function to test the bindings. +/// The input map. +/// The key of the first binding that satisfies the predicate, or None. +/// Thrown when is . +/// +/// O(n). +/// +[] +val tryFindKey : predicate : ('Key -> 'Value -> bool) -> table : IMap<'Key, 'Value> -> 'Key option + +/// +/// Returns the first value, in the order of enumeration, that the chooser returns for the bindings, or None. +/// +/// The function that returns a value, or None to go on. +/// The input map. +/// The first chosen value, or None. +/// Thrown when is . +/// +/// O(n). +/// +[] +val tryPick : chooser : ('Key -> 'Value -> 'U option) -> table : IMap<'Key, 'Value> -> 'U option + +// ---- Transformation ---- + +/// +/// Returns a map with the binding of a key changed by a function of its current value. +/// +/// The key. +/// +/// The function that takes the current value or None, and returns the new value or None to remove the +/// binding. +/// +/// The input map. +/// The map with the changed binding, of the implementation of the input. +/// Thrown when is . +/// +/// O(log n) for the immutable dictionaries. When returns a value that their value comparer finds +/// equal to the current value, they leave the binding as it is, its key included, and return the input itself (ADR 0001, +/// D14). +/// +[] +val change : key : 'Key -> f : ('Value option -> 'Value option) -> table : IMap<'Key, 'Value> -> IMap<'Key, 'Value> + +/// +/// Returns a hash map of the values that the chooser returns for the bindings, under their keys. +/// +/// The function that returns the new value, or None to drop the binding. +/// The input map. +/// +/// The hash map of the chosen values, with the default comparers. Keys that are distinct in the map and equal in the +/// default comparer keep the last chosen value, unless the default comparer finds the chosen values equal: then the first +/// of them stays, its key included (ADR 0001, D14). +/// +/// Thrown when is . +/// +/// O(n log n). +/// +[] +val choose : chooser : ('Key -> 'Value -> 'U option) -> table : IMap<'Key, 'Value> -> HashMap<'Key, 'U> when 'Key : not null diff --git a/src/FSharp.Collections.Immutable/HashMap/IMapTuple.fs b/src/FSharp.Collections.Immutable/HashMap/IMapTuple.fs new file mode 100644 index 00000000..1b15dfc5 --- /dev/null +++ b/src/FSharp.Collections.Immutable/HashMap/IMapTuple.fs @@ -0,0 +1,31 @@ +[] +module FSharp.Collections.Immutable.Tuple.IMap + +open FSharp.Collections.Immutable + +// The conversions share their algorithm with the default module and only pass another tuple constructor; `partition` +// delegates to the default module and repacks its pair. + +// ---- Partitioning ---- + +[] +let partition predicate (table : IMap<'Key, 'Value>) = + let struct (accepted, rejected) = FSharp.Collections.Immutable.IMap.partition predicate table + accepted, rejected + +// ---- Conversion ---- + +[] +let toArray (table : IMap<'Key, 'Value>) = + checkNotNull (nameof table) table + Dictionaries.toArray referencePair table + +[] +let toList (table : IMap<'Key, 'Value>) = + checkNotNull (nameof table) table + Dictionaries.toList referencePair table + +[] +let toSeq (table : IMap<'Key, 'Value>) = + checkNotNull (nameof table) table + Dictionaries.toSeq referencePair table diff --git a/src/FSharp.Collections.Immutable/HashMap/IMapTuple.fsi b/src/FSharp.Collections.Immutable/HashMap/IMapTuple.fsi new file mode 100644 index 00000000..ac9cdaab --- /dev/null +++ b/src/FSharp.Collections.Immutable/HashMap/IMapTuple.fsi @@ -0,0 +1,67 @@ +/// +/// The functions of that return tuples, with reference tuples instead of +/// struct tuples. +/// +/// +/// +/// After open FSharp.Collections.Immutable.Tuple, these functions shadow the default ones, and the other functions +/// of the default module stay available (ADR 0001, D3). Each function has the signature of the function with the same +/// name in . +/// +/// +/// The nested module of the default module holds the functions that +/// change a map by reference tuples (D13). +/// +/// +[] +module FSharp.Collections.Immutable.Tuple.IMap + +open FSharp.Collections.Immutable + +// ---- Partitioning ---- + +/// Splits the bindings by a predicate. +/// The function to test the bindings. +/// The input map. +/// +/// The map of the bindings that satisfy the predicate and the map of the other ones, both of the implementation of the +/// input. +/// +/// Thrown when is . +/// O(n log n) for the immutable dictionaries. +[] +val partition : predicate : ('Key -> 'Value -> bool) -> table : IMap<'Key, 'Value> -> IMap<'Key, 'Value> * IMap<'Key, 'Value> + +// ---- Conversion ---- + +/// +/// Returns a new array of the bindings, in the order of enumeration. +/// +/// The input map. +/// The array of key/value pairs. +/// Thrown when is . +/// +/// O(n). +/// +[] +val toArray : table : IMap<'Key, 'Value> -> ('Key * 'Value) array + +/// Returns a list of the bindings, in the order of enumeration. +/// The input map. +/// The list of key/value pairs. +/// Thrown when is . +/// O(n). +[] +val toList : table : IMap<'Key, 'Value> -> ('Key * 'Value) list + +/// +/// Returns the bindings as a sequence, in the order of enumeration. +/// +/// The input map. +/// The sequence of key/value pairs, computed on demand. +/// Thrown when is . +/// +/// O(1); O(n) to enumerate. +/// +[] +val toSeq : table : IMap<'Key, 'Value> -> seq<'Key * 'Value> diff --git a/src/FSharp.Collections.Immutable/Helper/Dictionaries.fs b/src/FSharp.Collections.Immutable/Helper/Dictionaries.fs new file mode 100644 index 00000000..9ce38262 --- /dev/null +++ b/src/FSharp.Collections.Immutable/Helper/Dictionaries.fs @@ -0,0 +1,176 @@ +/// +/// Algorithms over the bindings of dictionaries that the map modules share (ADR 0001, D3, D5 and D13). +/// +/// +/// +/// The algorithms visit the bindings in the order in which the dictionary enumerates them, and the callers check their +/// arguments first. They take the interfaces of the dictionaries, so that a map module, its interface module and its +/// Option and Tuple modules share one implementation. +/// +/// +/// The algorithms that build or take tuples take the constructor or the deconstructor of the tuples, so that the default +/// modules and the Tuple modules share them and neither pays for a second pass. +/// +/// +/// A binding comes in three shapes (D13): a key/value pair, a reference tuple and a struct tuple. The algorithms that take +/// bindings take the deconstructor of their shape, splitKeyValuePair, splitTuple or id, so that +/// every map module and every comparer struct creates and changes its maps with one algorithm for the three shapes. +/// +/// +module internal FSharp.Collections.Immutable.Dictionaries + +open System.Collections.Generic + +/// Whether a binding satisfies the predicate; stops at the first one that does. +let exists (predicate : 'Key -> 'Value -> bool) (pairs : seq>) = + use enumerator = pairs.GetEnumerator () + let mutable found = false + + while not found && enumerator.MoveNext () do + let pair = enumerator.Current + found <- predicate pair.Key pair.Value + + found + +/// Whether every binding satisfies the predicate; stops at the first one that does not. +let forall (predicate : 'Key -> 'Value -> bool) (pairs : seq>) = + use enumerator = pairs.GetEnumerator () + let mutable holds = true + + while holds && enumerator.MoveNext () do + let pair = enumerator.Current + holds <- predicate pair.Key pair.Value + + holds + +/// The key of the first binding that satisfies the predicate. +let tryFindKey (predicate : 'Key -> 'Value -> bool) (pairs : seq>) = + use enumerator = pairs.GetEnumerator () + let mutable result = ValueNone + + while result.IsNone && enumerator.MoveNext () do + let pair = enumerator.Current + + if predicate pair.Key pair.Value then + result <- ValueSome pair.Key + + result + +/// The first value that the chooser returns for a binding. +let tryPick (chooser : 'Key -> 'Value -> 'U voption) (pairs : seq>) = + use enumerator = pairs.GetEnumerator () + let mutable result = ValueNone + + while result.IsNone && enumerator.MoveNext () do + let pair = enumerator.Current + result <- chooser pair.Key pair.Value + + result + +/// Applies the folder to the state and each binding, in the order of enumeration. +let fold (folder : 'State -> 'Key -> 'Value -> 'State) (state : 'State) (pairs : seq>) = + let mutable state = state + + for pair in pairs do + state <- folder state pair.Key pair.Value + + state + +/// Applies the folder to each binding and the state, in the reverse order of enumeration. +let foldBack (folder : 'Key -> 'Value -> 'State -> 'State) (pairs : seq>) (state : 'State) = + // An enumerator only moves forwards, so the bindings are copied first + let bindings = Seq.toArray pairs + let mutable state = state + + for index = bindings.Length - 1 downto 0 do + let pair = bindings[index] + state <- folder pair.Key pair.Value state + + state + +/// Applies the action to each binding, in the order of enumeration. +let iter (action : 'Key -> 'Value -> unit) (pairs : seq>) = + for pair in pairs do + action pair.Key pair.Value + +/// Sets the value that the chooser returns for a binding under its key in the target. +let choose (chooser : 'Key -> 'Value -> 'U voption) (target : IDictionary<'Key, 'U>) (pairs : seq>) = + for pair in pairs do + match chooser pair.Key pair.Value with + | ValueSome value -> target[pair.Key] <- value + | ValueNone -> () + +/// Sets the result of the mapping of each binding under its key in the target. +let map (mapping : 'Key -> 'Value -> 'U) (target : IDictionary<'Key, 'U>) (pairs : seq>) = + for pair in pairs do + target[pair.Key] <- mapping pair.Key pair.Value + +/// Removes the bindings that do not satisfy the predicate from a target that starts with every binding. +/// The target is a builder over the same map, so that it shares what stays and returns the map itself when nothing goes. +let filter (predicate : 'Key -> 'Value -> bool) (target : IDictionary<'Key, 'Value>) (pairs : seq>) = + for pair in pairs do + if not (predicate pair.Key pair.Value) then + target.Remove pair.Key |> ignore + +/// Moves the bindings that do not satisfy the predicate from a target that starts with every binding to an empty one. +let partition + (predicate : 'Key -> 'Value -> bool) + (accepted : IDictionary<'Key, 'Value>) + (rejected : IDictionary<'Key, 'Value>) + (pairs : seq>) + = + for pair in pairs do + if not (predicate pair.Key pair.Value) then + accepted.Remove pair.Key |> ignore + rejected[pair.Key] <- pair.Value + +/// The key and the value of a key/value pair, the shape of the bindings that every .NET dictionary enumerates. +let inline splitKeyValuePair (pair : KeyValuePair<'Key, 'Value>) = struct (pair.Key, pair.Value) + +/// The key and the value of a reference tuple, the shape of the bindings of most F# code. +let inline splitTuple (key : 'Key, value : 'Value) = struct (key, value) + +/// Sets each element, split into a key and a value, in the target; a later binding of a key replaces an earlier one, +/// as in `Map.ofSeq`. +let inline setAll + ([] split : 'Element -> struct ('Key * 'Value)) + (target : IDictionary<'Key, 'Value>) + (elements : seq<'Element>) + = + for element in elements do + let struct (key, value) = split element + target[key] <- value + +/// The elements, split into keys and values, as key/value pairs, computed on demand. +let inline toKeyValuePairs + ([] split : 'Element -> struct ('Key * 'Value)) + (elements : seq<'Element>) + : seq> = + elements + |> Seq.map (fun element -> + let struct (key, value) = split element + KeyValuePair (key, value) + ) + +/// The bindings as an array of tuples, in the order of enumeration. +let inline toArray + ([] tuple : 'Key -> 'Value -> 'R) + (pairs : IReadOnlyCollection>) + : 'R array = + let result = Array.zeroCreate pairs.Count + let mutable index = 0 + + for pair in pairs do + result[index] <- tuple pair.Key pair.Value + index <- index + 1 + + result + +/// The bindings as a list of tuples, in the order of enumeration. +let inline toList ([] tuple : 'Key -> 'Value -> 'R) (pairs : seq>) : 'R list = [ + for pair in pairs -> tuple pair.Key pair.Value +] + +/// The bindings as a sequence of tuples, in the order of enumeration, computed on demand. +let inline toSeq ([] tuple : 'Key -> 'Value -> 'R) (pairs : seq>) : seq<'R> = + pairs |> Seq.map (fun pair -> tuple pair.Key pair.Value) diff --git a/src/FSharp.Collections.Immutable/Helper/Raise.fs b/src/FSharp.Collections.Immutable/Helper/Raise.fs index b36a1141..d31cdbc0 100644 --- a/src/FSharp.Collections.Immutable/Helper/Raise.fs +++ b/src/FSharp.Collections.Immutable/Helper/Raise.fs @@ -65,5 +65,9 @@ let invalidOpListNotEnoughElements (index : int) : 'T = /// No element satisfies the predicate of a `find` or `pick` function. let indexNotFound () : 'T = raise (KeyNotFoundException ErrorStrings.KeyNotFoundAlt) +/// A map has no binding of a key, or no binding gives a value to `pick`: `Map.find` and `Map.pick` throw the exception +/// with its default message. +let keyNotFound () : 'T = raise (KeyNotFoundException ()) + /// A collection has fewer elements than a function needs. let notEnoughElements () : 'T = raise (InvalidOperationException ErrorStrings.NotEnoughElements) diff --git a/src/FSharp.Collections.Immutable/Maps.fs b/src/FSharp.Collections.Immutable/Maps.fs index 7a73565a..efbec178 100644 --- a/src/FSharp.Collections.Immutable/Maps.fs +++ b/src/FSharp.Collections.Immutable/Maps.fs @@ -2,167 +2,6 @@ namespace FSharp.Collections.Immutable open System.Collections.Generic -type IMap<'Key, 'Value> = System.Collections.Immutable.IImmutableDictionary<'Key, 'Value> - -type HashMap<'Key, 'Value when 'Key : not null> = System.Collections.Immutable.ImmutableDictionary<'Key, 'Value> - -type HashMapBuilder<'Key, 'Value when 'Key : not null> = HashMap<'Key, 'Value>.Builder - -[] -module HashMap = - - type internal HashMapFactory = System.Collections.Immutable.ImmutableDictionary - - let inline check (map : HashMap<_, _>) = checkNotNull (nameof map) map - - ////////// Creating ////////// - - let inline empty<'Key, 'Value when 'Key : not null> = HashMapFactory.Create<'Key, 'Value>() - let inline singleton item = empty.Add (item) - - let inline ofSeq source = HashMapFactory.CreateRange (source) - - let inline ofSeqWith getKey source = - source - |> Seq.map (fun i -> KeyValuePair (getKey i, i)) - |> HashMapFactory.CreateRange - - let inline ofSeqGroupBy getKey source = - source - |> Seq.groupBy getKey - |> Seq.map (fun (key, value) -> KeyValuePair (key, value)) - |> HashMapFactory.CreateRange - - let inline ofArray (source : _ array) = HashMapFactory.CreateRange (source) - - let inline toSeq (map : HashMap<_, _>) = map :> seq<_> - - let inline toArray (map : HashMap<_, _>) = - check map - Seq.toArray map - - ////////// Building ////////// - - let inline builder () = HashMapFactory.CreateBuilder () - let inline builderWithKeyComparer comparer = HashMapFactory.CreateBuilder (comparer) - let inline builderWithComparers keyComparer valueComparer = HashMapFactory.CreateBuilder (keyComparer, valueComparer) - - let inline ofBuilder (mapBuilder : HashMapBuilder<_, _>) = - checkNotNull (nameof mapBuilder) mapBuilder - mapBuilder.ToImmutable () - - let inline toBuilder map : HashMapBuilder<_, _> = - check map - map.ToBuilder () - - let inline ofKeyComparer<'Key, 'Value when 'Key : not null> comparer = HashMapFactory.Create<'Key, 'Value>(comparer) - let inline ofComparers<'Key, 'Value when 'Key : not null> keyComparer valueComparer = - HashMapFactory.Create<'Key, 'Value>(keyComparer, valueComparer) - - - let inline isEmpty map = - check map - map.IsEmpty - - let inline length map = - check map - map.Count - - let inline keyComparer map = - check map - map.KeyComparer - - let inline valueComparer map = - check map - map.ValueComparer - - let inline containsKey key map = - check map - map.ContainsKey key - - let inline find key map = - check map - map.[key] - - let inline tryFind key map = - check map - - match map.TryGetValue (key) with - | true, value -> Some value - | false, _ -> None - - let inline vTryFind key map = - check map - - match map.TryGetValue (key) with - | true, value -> ValueSome value - | false, _ -> ValueNone - - let inline pick chooser map = - check map - map |> Seq.pick (fun kvp -> chooser kvp.Key kvp.Value) - - let inline tryPick chooser map = - check map - map |> Seq.tryPick (fun kvp -> chooser kvp.Key kvp.Value) - - let inline vTryPick chooser map = - check map - - match map |> Seq.tryPick (fun kvp -> chooser kvp.Key kvp.Value) with - | Some value -> ValueSome value - | None -> ValueNone - - let inline iter action (map : HashMap<_, _>) = - check map - map |> Seq.iter (fun kvp -> action kvp.Key kvp.Value) - - let inline exists predicate map = - check map - map |> Seq.exists (fun kvp -> predicate kvp.Key kvp.Value) - - let inline add key value map : HashMap<_, _> = - check map - map.Add (key, value) - - let inline append map pairs : HashMap<_, _> = - check map - checkNotNull (nameof pairs) pairs - map.AddRange pairs - - let inline remove key map : HashMap<_, _> = - check map - map.Remove key - - let inline except keys map : HashMap<_, _> = - check map - map.RemoveRange keys - - let inline clear map : HashMap<_, _> = - check map - map.Clear () - - let inline filter predicate map = - map - |> Seq.filter (fun (kvp : KeyValuePair<_, _>) -> predicate kvp.Key kvp.Value) - - let inline forall predicate map = - map - |> Seq.forall (fun (kvp : KeyValuePair<_, _>) -> predicate kvp.Key kvp.Value) - - let inline map mapping map' = - map' - |> Seq.map (fun (kvp : KeyValuePair<_, _>) -> mapping kvp.Key kvp.Value) - |> ofSeq - - let inline where predicate map = - map - |> Seq.where (fun (kvp : KeyValuePair<_, _>) -> predicate kvp.Key kvp.Value) - |> empty.AddRange - - type SortedMap<'Key, 'Value when 'Key : not null> = System.Collections.Immutable.ImmutableSortedDictionary<'Key, 'Value> type SortedMapBuilder<'Key, 'Value when 'Key : not null> = SortedMap<'Key, 'Value>.Builder From 16cac3176ca20024d3eb823d412bf17a53b50d6a Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Sun, 18 Oct 2026 13:02:07 +0200 Subject: [PATCH 2/9] test: shared contracts of the map modules, run for `HashMap` and `IMap` - `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 --- .../Contracts/MapContract.fs | 425 +++++++++ .../Contracts/MapCreationContract.fs | 170 ++++ .../Contracts/MapInputs.fs | 99 +++ .../Contracts/MapMutationContract.fs | 295 +++++++ .../Contracts/MapOps.fs | 193 +++++ .../Contracts/MapTransformContract.fs | 103 +++ .../FSharp.Collections.Immutable.Tests.fsproj | 20 + .../HashMap/CustomComparersHashMapTests.fs | 560 ++++++++++++ .../HashMap/HashMapBuilderTests.fs | 199 +++++ .../HashMapComputationExpressionTests.fs | 403 +++++++++ .../HashMap/HashMapContractTests.fs | 29 + .../HashMap/HashMapExtensionsTests.fs | 141 +++ .../HashMap/HashMapOps.fs | 133 +++ .../HashMap/HashMapOptionTests.fs | 145 ++++ .../HashMap/HashMapTests.fs | 807 ++++++++++++++++++ .../HashMap/HashMapTupleTests.fs | 134 +++ .../HashMap/IMapContractTests.fs | 19 + .../HashMap/IMapOps.fs | 73 ++ .../HashMap/IMapOptionTests.fs | 133 +++ .../HashMap/IMapTests.fs | 542 ++++++++++++ .../HashMap/IMapTupleTests.fs | 113 +++ 21 files changed, 4736 insertions(+) create mode 100644 tests/FSharp.Collections.Immutable.Tests/Contracts/MapContract.fs create mode 100644 tests/FSharp.Collections.Immutable.Tests/Contracts/MapCreationContract.fs create mode 100644 tests/FSharp.Collections.Immutable.Tests/Contracts/MapInputs.fs create mode 100644 tests/FSharp.Collections.Immutable.Tests/Contracts/MapMutationContract.fs create mode 100644 tests/FSharp.Collections.Immutable.Tests/Contracts/MapOps.fs create mode 100644 tests/FSharp.Collections.Immutable.Tests/Contracts/MapTransformContract.fs create mode 100644 tests/FSharp.Collections.Immutable.Tests/HashMap/CustomComparersHashMapTests.fs create mode 100644 tests/FSharp.Collections.Immutable.Tests/HashMap/HashMapBuilderTests.fs create mode 100644 tests/FSharp.Collections.Immutable.Tests/HashMap/HashMapComputationExpressionTests.fs create mode 100644 tests/FSharp.Collections.Immutable.Tests/HashMap/HashMapContractTests.fs create mode 100644 tests/FSharp.Collections.Immutable.Tests/HashMap/HashMapExtensionsTests.fs create mode 100644 tests/FSharp.Collections.Immutable.Tests/HashMap/HashMapOps.fs create mode 100644 tests/FSharp.Collections.Immutable.Tests/HashMap/HashMapOptionTests.fs create mode 100644 tests/FSharp.Collections.Immutable.Tests/HashMap/HashMapTests.fs create mode 100644 tests/FSharp.Collections.Immutable.Tests/HashMap/HashMapTupleTests.fs create mode 100644 tests/FSharp.Collections.Immutable.Tests/HashMap/IMapContractTests.fs create mode 100644 tests/FSharp.Collections.Immutable.Tests/HashMap/IMapOps.fs create mode 100644 tests/FSharp.Collections.Immutable.Tests/HashMap/IMapOptionTests.fs create mode 100644 tests/FSharp.Collections.Immutable.Tests/HashMap/IMapTests.fs create mode 100644 tests/FSharp.Collections.Immutable.Tests/HashMap/IMapTupleTests.fs diff --git a/tests/FSharp.Collections.Immutable.Tests/Contracts/MapContract.fs b/tests/FSharp.Collections.Immutable.Tests/Contracts/MapContract.fs new file mode 100644 index 00000000..1caf6c37 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/Contracts/MapContract.fs @@ -0,0 +1,425 @@ +namespace FSharp.Collections.Immutable.Tests + +open System.Collections.Generic +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// +/// The predicates over the bindings of the map contracts, as the cases of their rows. +/// +/// +/// MSTest names a row after its values, and a function has no readable name, so a row holds a case and the test takes +/// the function of the case. +/// +[] +type BindingPredicate = + /// The bindings whose value is longer than four characters. + | HasLongValue + /// The bindings of negative keys. + | HasNegativeKey + /// The bindings of keys below a hundred, which are all the bindings of every input. + | HasKeyBelowHundred + /// Every binding. + | Always + /// No binding. + | Never + + /// Tells whether a binding satisfies the predicate. + member this.Matches (key : int) (value : string) = + match this with + | BindingPredicate.HasLongValue -> value.Length > 4 + | BindingPredicate.HasNegativeKey -> key < 0 + | BindingPredicate.HasKeyBelowHundred -> key < 100 + | BindingPredicate.Always -> true + | BindingPredicate.Never -> false + +/// +/// The helpers that the contract bases of the map modules share. +/// +/// +/// +/// Every map module mirrors the FSharp.Core Map module (ADR 0001, D6), so the map contracts take no +/// : they compare the exceptions with the Map oracle through +/// ExceptionAssert.Same, and Mirror stays about the sequence-like modules. +/// +/// +/// A hash map enumerates its bindings in an unspecified order. The contracts compare whole maps as bindings sorted by key, +/// and compute the expectations of the functions that visit the bindings one by one over the order in which the map under +/// test enumerates them, so that one test body serves hash, sorted and frozen maps. +/// +/// +/// The name of the module under test, for the messages of the assertions. +/// Maps the name of a parameter of the FSharp.Core oracle to the name of the parameter of the module. +/// Creates a map of the given bindings. +/// The bindings in the order in which the map enumerates them. +[] +type MapContractBase<'M> + ( + name : string, + paramName : string -> string, + ofSeq : seq -> 'M, + toArray : 'M -> struct (int * string) array + ) + = + + /// The map of the bindings of an input and its bindings in the order in which the map enumerates them. + member _.Create (input : struct (int * string) array) = + let table = ofSeq input + struct (table, toArray table) + + /// Asserts that bindings, in any order, are the bindings of an FSharp.Core map. + member _.AreBindings (expected : Map, actual : struct (int * 'T) array, message : string) = + CollectionAssert.AreEqual (MapInputs.OfMap expected, MapInputs.SortByKey actual, $"%s{name}: %s{message}") + + /// Asserts that a function of the module throws what the same function of the FSharp.Core Map module throws. + member _.ThrowsLikeMap (oracle : unit -> 'A, actual : unit -> 'R, message : string) = + ExceptionAssert.Same (paramName, oracle >> ignore, actual >> ignore, $"%s{name}: %s{message}") + +/// +/// The contract of the reading functions that every map module provides: HashMap, SortedMap, IMap +/// and FrozenMap (ADR 0001, Appendix B). +/// +[] +type MapContract<'M> (ops : MapOps<'M>) = + inherit MapContractBase<'M> (ops.Name, ops.ParamName, ops.OfSeq, ops.ToArray) + + /// A chooser that returns a value for the bindings of negative keys only. + static let negatedIfNegative (key : int) (value : string) = + if key < 0 then + ValueSome (value.Length - key) + else + ValueNone + + /// A folder whose state records the order in which it saw the bindings. + static let record (state : struct (int * string) array) (key : int) (value : string) = + Array.append state [| struct (key, value) |] + + /// A folder from the end whose state records the order in which it saw the bindings: prepending from the last binding + /// to the first one rebuilds the order of enumeration. + static let recordBack (key : int) (value : string) (state : struct (int * string) array) = + Array.append [| struct (key, value) |] state + + /// The bindings of a map of the module as struct tuples, in the order of its enumeration of key/value pairs: the + /// observation that toArray is compared with. + static let enumerated (table : 'M) = + match box table with + | :? seq> as pairs -> + pairs + |> Seq.map (fun pair -> struct (pair.Key, pair.Value)) + |> Seq.toArray + | _ -> failwith $"%s{typeof<'M>.Name} must enumerate key/value pairs" + + /// Every input that has bindings with each of its keys: the rows of the lookups that find a binding. + static member NonEmptyInputsAndKeys = seq { + for input in MapInputs.NonEmpty do + for key in MapInputs.KeysOf input do + struct (input, key) + } + + /// Every input with each key that no input has: the rows of the lookups that fail. + static member InputsAndAbsentKeys = seq { + for input in MapInputs.All do + for key in MapInputs.AbsentKeys do + struct (input, key) + } + + /// Every input with each of its keys and each key that no input has: the rows of the lookups that may find nothing. + static member InputsAndKeys = seq { + for input in MapInputs.All do + for key in Array.append (MapInputs.KeysOf input) MapInputs.AbsentKeys do + struct (input, key) + } + + /// Every input with the probes of containsBinding: each of its bindings, each of its keys with another value, and each + /// key that no input has with a value that an input has. + static member InputsAndBindingProbes = seq { + for input in MapInputs.All do + for struct (key, value) in input do + struct (input, key, value) + struct (input, key, value + "!") + + for key in MapInputs.AbsentKeys do + struct (input, key, "five") + } + + /// Every input with each predicate of exists and tryFindKey: one that some bindings satisfy, one that other bindings + /// satisfy and one that no binding satisfies. + static member InputsAndPredicates = seq { + for input in MapInputs.All do + for predicate in [| BindingPredicate.HasLongValue; BindingPredicate.HasNegativeKey; BindingPredicate.Never |] do + struct (input, predicate) + } + + /// Every input with each predicate of forall: one that some bindings satisfy, one that every binding satisfies and one + /// that no binding satisfies. + static member InputsAndForallPredicates = seq { + for input in MapInputs.All do + for predicate in [| BindingPredicate.HasLongValue; BindingPredicate.HasKeyBelowHundred; BindingPredicate.Never |] do + struct (input, predicate) + } + + /// The predicates of findKey, which some bindings of the input with several bindings satisfy. + static member FindKeyPredicates = seq { + BindingPredicate.HasLongValue + BindingPredicate.HasNegativeKey + } + + // ---- Properties ---- + + [)>] + member this.``count counts the bindings`` (input : struct (int * string) array) = + let struct (table, _) = this.Create input + Assert.AreEqual(Map.count (MapInputs.ToMap input), ops.Count table, $"%s{ops.Name}.count must count the bindings") + + [)>] + member this.``isEmpty is true only for a map without bindings`` (input : struct (int * string) array) = + let struct (table, _) = this.Create input + + Assert.AreEqual( + Map.isEmpty (MapInputs.ToMap input), + ops.IsEmpty table, + $"%s{ops.Name}.isEmpty must be true only for a map without bindings" + ) + + [)>] + member this.``keys returns the keys in the order of enumeration`` (input : struct (int * string) array) = + let struct (table, bindings) = this.Create input + let keys = Seq.toArray (ops.Keys table) + CollectionAssert.AreEqual (MapInputs.KeysOf bindings, keys, $"%s{ops.Name}.keys must follow the enumeration") + + CollectionAssert.AreEqual ( + Seq.toArray (Map.keys (MapInputs.ToMap input)), + Array.sort keys, + $"%s{ops.Name}.keys must return the keys of the map" + ) + + [)>] + member this.``values returns the values with duplicates in the order of enumeration`` (input : struct (int * string) array) = + let struct (table, bindings) = this.Create input + let values = Seq.toArray (ops.Values table) + + CollectionAssert.AreEqual ( + bindings |> Array.map (fun struct (_, value) -> value), + values, + $"%s{ops.Name}.values must follow the enumeration" + ) + + CollectionAssert.AreEqual ( + Array.sort (Seq.toArray (Map.values (MapInputs.ToMap input))), + Array.sort values, + $"%s{ops.Name}.values must return the values of the map with duplicates" + ) + + // ---- Access ---- + + [.NonEmptyInputsAndKeys)>] + member this.``find returns the value of each key`` (input : struct (int * string) array, key : int) = + let struct (table, _) = this.Create input + + Assert.AreEqual( + Map.find key (MapInputs.ToMap input), + ops.Find key table, + $"%s{ops.Name}.find must return the value of the key" + ) + + [.InputsAndAbsentKeys)>] + member this.``find throws for a key without a binding as Map does`` (input : struct (int * string) array, key : int) = + let struct (table, _) = this.Create input + + this.ThrowsLikeMap ( + (fun () -> Map.find key (MapInputs.ToMap input)), + (fun () -> ops.Find key table), + "find of a key without a binding must throw" + ) + + [.InputsAndKeys)>] + member this.``tryFind returns the value of a key or nothing`` (input : struct (int * string) array, key : int) = + let struct (table, _) = this.Create input + + Assert.AreEqual( + Map.tryFind key (MapInputs.ToMap input) + |> ValueOption.ofOption, + ops.TryFind key table, + $"%s{ops.Name}.tryFind must return the value of the key or nothing" + ) + + // ---- Search ---- + + [.InputsAndBindingProbes)>] + member this.``containsBinding is true only for a key that is bound to the value`` + (input : struct (int * string) array, key : int, value : string) + = + let struct (table, _) = this.Create input + + Assert.AreEqual( + Map.tryFind key (MapInputs.ToMap input) + |> Option.exists (fun stored -> stored = value), + ops.ContainsBinding key value table, + $"%s{ops.Name}.containsBinding must be true only for a key that is bound to the value" + ) + + [.InputsAndKeys)>] + member this.``containsKey tells the keys with a binding`` (input : struct (int * string) array, key : int) = + let struct (table, _) = this.Create input + + Assert.AreEqual( + Map.containsKey key (MapInputs.ToMap input), + ops.ContainsKey key table, + $"%s{ops.Name}.containsKey must tell the keys with a binding" + ) + + [.InputsAndPredicates)>] + member this.``exists finds a binding that satisfies the predicate`` + (input : struct (int * string) array, predicate : BindingPredicate) + = + let struct (table, _) = this.Create input + + Assert.AreEqual( + Map.exists predicate.Matches (MapInputs.ToMap input), + ops.Exists predicate.Matches table, + $"%s{ops.Name}.exists must find a binding that satisfies the predicate" + ) + + [.FindKeyPredicates)>] + member this.``findKey returns the key of the first binding that satisfies the predicate`` (predicate : BindingPredicate) = + let struct (table, bindings) = this.Create MapInputs.Several + + let expected = + bindings + |> Array.pick (fun struct (key, value) -> if predicate.Matches key value then Some key else None) + + let actual = ops.FindKey predicate.Matches table + Assert.AreEqual(expected, actual, $"%s{ops.Name}.findKey must return the first key in the enumeration") + + Assert.IsTrue ( + predicate.Matches actual (Map.find actual (MapInputs.ToMap MapInputs.Several)), + $"%s{ops.Name}.findKey must return a matching key" + ) + + [)>] + member this.``findKey throws when no binding satisfies the predicate as Map does`` (input : struct (int * string) array) = + let struct (table, _) = this.Create input + let never = BindingPredicate.Never + + this.ThrowsLikeMap ( + (fun () -> Map.findKey never.Matches (MapInputs.ToMap input)), + (fun () -> ops.FindKey never.Matches table), + "findKey without a match must throw" + ) + + [.InputsAndForallPredicates)>] + member this.``forall checks that every binding satisfies the predicate`` + (input : struct (int * string) array, predicate : BindingPredicate) + = + let struct (table, _) = this.Create input + + Assert.AreEqual( + Map.forall predicate.Matches (MapInputs.ToMap input), + ops.Forall predicate.Matches table, + $"%s{ops.Name}.forall must check that every binding satisfies the predicate" + ) + + [] + member this.``pick returns the first chosen value`` () = + let struct (table, bindings) = this.Create MapInputs.Several + + Assert.AreEqual( + bindings + |> Array.pick (fun struct (key, value) -> negatedIfNegative key value |> ValueOption.toOption), + ops.Pick negatedIfNegative table, + $"%s{ops.Name}.pick must return the first value in the enumeration" + ) + + [)>] + member this.``pick throws when the chooser returns nothing as Map does`` (input : struct (int * string) array) = + let struct (table, _) = this.Create input + + this.ThrowsLikeMap ( + (fun () -> Map.pick (fun _ _ -> None) (MapInputs.ToMap input) : int), + (fun () -> ops.Pick (fun _ _ -> ValueNone) table), + "pick without a choice must throw" + ) + + [.InputsAndPredicates)>] + member this.``tryFindKey returns the key of the first binding that satisfies the predicate or nothing`` + (input : struct (int * string) array, predicate : BindingPredicate) + = + let struct (table, bindings) = this.Create input + + let expected = + bindings + |> Array.tryPick (fun struct (key, value) -> if predicate.Matches key value then Some key else None) + + Assert.AreEqual( + ValueOption.ofOption expected, + ops.TryFindKey predicate.Matches table, + $"%s{ops.Name}.tryFindKey must return the first key in the enumeration or nothing" + ) + + Assert.AreEqual( + (Map.tryFindKey predicate.Matches (MapInputs.ToMap input)).IsSome, + (ops.TryFindKey predicate.Matches table).IsSome, + $"%s{ops.Name}.tryFindKey must find a key when Map.tryFindKey does" + ) + + [)>] + member this.``tryPick returns the first chosen value or nothing`` (input : struct (int * string) array) = + let struct (table, bindings) = this.Create input + + Assert.AreEqual( + bindings + |> Array.tryPick (fun struct (key, value) -> negatedIfNegative key value |> ValueOption.toOption) + |> ValueOption.ofOption, + ops.TryPick negatedIfNegative table, + $"%s{ops.Name}.tryPick must return the first value in the enumeration or nothing" + ) + + // ---- Folding ---- + + [)>] + member this.``fold visits every binding once in the order of enumeration`` (input : struct (int * string) array) = + let struct (table, bindings) = this.Create input + let visited = ops.Fold record [||] table + CollectionAssert.AreEqual (bindings, visited, $"%s{ops.Name}.fold must follow the enumeration") + this.AreBindings (MapInputs.ToMap input, visited, "fold must visit every binding") + + [)>] + member this.``foldBack visits every binding once in the reverse order of enumeration`` (input : struct (int * string) array) = + let struct (table, bindings) = this.Create input + let visited = ops.FoldBack recordBack table [||] + CollectionAssert.AreEqual (bindings, visited, $"%s{ops.Name}.foldBack must follow the reverse enumeration") + this.AreBindings (MapInputs.ToMap input, visited, "foldBack must visit every binding") + + [)>] + member this.``iter visits every binding once in the order of enumeration`` (input : struct (int * string) array) = + let struct (table, bindings) = this.Create input + let visited = ResizeArray() + ops.Iter (fun key value -> visited.Add (struct (key, value))) table + CollectionAssert.AreEqual (bindings, visited.ToArray (), $"%s{ops.Name}.iter must follow the enumeration") + this.AreBindings (MapInputs.ToMap input, visited.ToArray (), "iter must visit every binding") + + // ---- Conversion ---- + + [)>] + member this.``toArray returns the bindings in the order of enumeration`` (input : struct (int * string) array) = + let table = ops.OfSeq input + let array = ops.ToArray table + CollectionAssert.AreEqual (enumerated table, array, $"%s{ops.Name}.toArray must follow the enumeration of the map") + this.AreBindings (MapInputs.ToMap input, array, "toArray must return every binding") + + [] + member _.``toArray returns a new array`` () = + let table = ops.OfSeq MapInputs.Several + let array = ops.ToArray table + array[0] <- struct (1000, "thousand") + Assert.AreEqual(MapInputs.Several.Length, ops.Count table, $"%s{ops.Name}.toArray must return a copy") + Assert.IsFalse (ops.ContainsKey 1000 table, $"%s{ops.Name}.toArray must return a copy that does not change the map") + + [)>] + member this.``toList returns the bindings in the order of enumeration`` (input : struct (int * string) array) = + let struct (table, bindings) = this.Create input + CollectionAssert.AreEqual (bindings, List.toArray (ops.ToList table), $"%s{ops.Name}.toList must follow the enumeration") + + [)>] + member this.``toSeq returns the bindings in the order of enumeration`` (input : struct (int * string) array) = + let struct (table, bindings) = this.Create input + CollectionAssert.AreEqual (bindings, Seq.toArray (ops.ToSeq table), $"%s{ops.Name}.toSeq must follow the enumeration") diff --git a/tests/FSharp.Collections.Immutable.Tests/Contracts/MapCreationContract.fs b/tests/FSharp.Collections.Immutable.Tests/Contracts/MapCreationContract.fs new file mode 100644 index 00000000..9d681b6f --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/Contracts/MapCreationContract.fs @@ -0,0 +1,170 @@ +namespace FSharp.Collections.Immutable.Tests + +open System.Collections.Generic +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// +/// The contract of the creation functions of the concrete map modules and of their comparer structs: HashMap, +/// SortedMap and FrozenMap (ADR 0001, Appendix B, D12 and D13). +/// +/// +/// +/// ofArray, ofList and ofSeq take key/value pairs, reference tuples and struct tuples, and have the +/// semantics of the FSharp.Core Map module in every shape: a later binding of a key wins. Their tests run once per +/// shape and input. ofSeqBy wraps the BCL, which handles a repeated key differently in the immutable and the frozen +/// dictionaries, so the contract runs it on distinct keys and the collections test the repeated keys. +/// +/// +/// Every created map must look its keys up with the key comparer of the functions under test, which +/// describes. +/// +/// +[] +type MapCreationContract<'M> (ops : MapCreationOps<'M>) = + inherit MapContractBase<'M> (ops.Name, ops.ParamName, ops.OfSeq, ops.ToArray) + + /// The bindings of a map of the module as key/value pairs, the shape that every map enumerates. + static let asKeyValuePairs (table : 'M) = + match box table with + | :? seq> as pairs -> pairs + | _ -> failwith $"%s{typeof<'M>.Name} must enumerate key/value pairs" + + /// Every shape with every input, the bindings that repeat keys included: the rows of ofArray, ofList and ofSeq. The + /// rows of every property are built on each read. + static member ShapesAndBindings = seq { + for shape in BindingShape.All do + for input in + seq { + yield! MapInputs.All + MapInputs.RepeatedKeys + } do + struct (shape, input) + } + + /// The values of ofSeqBy, whose lengths are their keys. + static member Values : TestDataRow seq = seq { + TestDataRow [||] + TestDataRow [| "one" |] + TestDataRow [| "a"; "three"; "be"; "seven!" |] + } + + /// Creates a map of the bindings with ofArray of a shape and changes the array afterwards. + static member private CreateThenChange (bind : struct (int * string) -> 'Binding, ofArray : 'Binding array -> 'M) = + let elements = MapInputs.Several |> Array.map bind + let table = ofArray elements + elements[0] <- bind (struct (1000, "thousand")) + table + + /// Asserts that a created map has the bindings of an FSharp.Core map and finds every key with its key comparer. + member private this.AreCreated (expected : Map, table : 'M, rule : string) = + this.AreBindings (expected, ops.ToArray table, rule) + + for key in Map.keys expected do + Assert.IsTrue ( + ops.ContainsKey (ops.EquivalentKey key) table, + $"%s{ops.Name}: the map must find %d{ops.EquivalentKey key} with its key comparer" + ) + + /// Creates a map with ofArray of a shape, from bindings given as struct tuples. + member private _.OfArray (shape : BindingShape, bindings : struct (int * string) array) = + match shape with + | BindingShape.KeyValuePairs -> ops.KeyValuePairs.OfArray (Array.map MapInputs.ToKeyValuePair bindings) + | BindingShape.Tuples -> ops.Tuples.OfArray (Array.map MapInputs.ToTuple bindings) + | BindingShape.StructTuples -> ops.StructTuples.OfArray bindings + + /// Creates a map with ofList of a shape, from bindings given as struct tuples; the list is the input of ofList. + member private _.OfList (shape : BindingShape, bindings : struct (int * string) array) = + match shape with + | BindingShape.KeyValuePairs -> + ops.KeyValuePairs.OfList ( + bindings + |> Array.map MapInputs.ToKeyValuePair + |> List.ofArray + ) + | BindingShape.Tuples -> ops.Tuples.OfList (bindings |> Array.map MapInputs.ToTuple |> List.ofArray) + | BindingShape.StructTuples -> ops.StructTuples.OfList (List.ofArray bindings) + + /// Creates a map with ofSeq of a shape, from a lazy sequence of bindings given as struct tuples. + member private _.OfSeq (shape : BindingShape, bindings : struct (int * string) array) = + match shape with + | BindingShape.KeyValuePairs -> ops.KeyValuePairs.OfSeq (Seq.map MapInputs.ToKeyValuePair bindings) + | BindingShape.Tuples -> ops.Tuples.OfSeq (Seq.map MapInputs.ToTuple bindings) + | BindingShape.StructTuples -> ops.StructTuples.OfSeq (Seq.map id bindings) + + [] + member _.``empty has no bindings`` () = + Assert.IsEmpty (ops.ToArray (ops.Empty ()), $"%s{ops.Name}.empty must have no bindings") + + [.ShapesAndBindings)>] + member this.``ofArray keeps the last binding of each key as Map does`` + (shape : BindingShape, input : struct (int * string) array) + = + this.AreCreated ( + Map.ofArray (Array.map MapInputs.ToTuple input), + this.OfArray (shape, input), + "ofArray must keep the last binding of each key, as Map.ofArray does" + ) + + [)>] + member _.``ofArray copies the bindings`` (shape : BindingShape) = + let table = + match shape with + | BindingShape.KeyValuePairs -> + MapCreationContract<'M>.CreateThenChange(MapInputs.ToKeyValuePair, ops.KeyValuePairs.OfArray) + | BindingShape.Tuples -> MapCreationContract<'M>.CreateThenChange(MapInputs.ToTuple, ops.Tuples.OfArray) + | BindingShape.StructTuples -> MapCreationContract<'M>.CreateThenChange(id, ops.StructTuples.OfArray) + + Assert.IsFalse (ops.ContainsKey 1000 table, $"%s{ops.Name}.ofArray must copy the bindings") + + [.ShapesAndBindings)>] + member this.``ofList keeps the last binding of each key as Map does`` + (shape : BindingShape, input : struct (int * string) array) + = + this.AreCreated ( + Map.ofList (input |> Array.map MapInputs.ToTuple |> List.ofArray), + this.OfList (shape, input), + "ofList must keep the last binding of each key, as Map.ofList does" + ) + + [.ShapesAndBindings)>] + member this.``ofSeq keeps the last binding of each key as Map does`` + (shape : BindingShape, input : struct (int * string) array) + = + this.AreCreated ( + Map.ofSeq (Seq.map MapInputs.ToTuple input), + this.OfSeq (shape, input), + "ofSeq must keep the last binding of each key, as Map.ofSeq does" + ) + + [)>] + member this.``ofSeq takes the key value pairs of a dictionary`` (input : struct (int * string) array) = + this.AreCreated ( + MapInputs.ToMap input, + ops.KeyValuePairs.OfSeq (dict (Array.map MapInputs.ToTuple input)), + "ofSeq must take the key/value pairs of a dictionary" + ) + + [)>] + member this.``ofSeq takes the key value pairs of an F# map`` (input : struct (int * string) array) = + let expected = MapInputs.ToMap input + this.AreCreated (expected, ops.KeyValuePairs.OfSeq expected, "ofSeq must take the key/value pairs of an F# map") + + [)>] + member this.``ofSeq takes the key value pairs of a map of the module`` (input : struct (int * string) array) = + this.AreCreated ( + MapInputs.ToMap input, + ops.KeyValuePairs.OfSeq (asKeyValuePairs (ops.OfSeq input)), + "ofSeq must take the key/value pairs of a map of the module" + ) + + [.Values)>] + member this.``ofSeqBy binds the values to the keys of the projection`` (values : string array) = + this.AreCreated ( + Map.ofArray (Array.map (fun (value : string) -> value.Length, value) values), + ops.OfSeqBy String.length values, + "ofSeqBy must bind every value to the key that the projection returns for it" + ) + + [] + member this.``singleton has one binding`` () = + this.AreCreated (Map.add 42 "answer" Map.empty, ops.Singleton 42 "answer", "singleton must have one binding") diff --git a/tests/FSharp.Collections.Immutable.Tests/Contracts/MapInputs.fs b/tests/FSharp.Collections.Immutable.Tests/Contracts/MapInputs.fs new file mode 100644 index 00000000..568cef6b --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/Contracts/MapInputs.fs @@ -0,0 +1,99 @@ +namespace FSharp.Collections.Immutable.Tests + +open System +open System.Collections.Generic + +/// +/// The inputs that the contract tests of the map modules run the functions on. +/// +/// +/// +/// The bindings map keys to values. The FSharp.Core Map +/// module computes the expectations. A hash map enumerates its bindings in an unspecified order, so the contracts compare +/// the bindings sorted by key, and compute the expectations of the functions that visit the bindings one by one over the +/// order in which the map under test enumerates them (ADR 0001, D8). +/// +/// +/// Every property builds new arrays on each read: MSTest hands the arrays of a row source to the tests as they are, so an +/// array that the tests shared would carry the change that one test made into the others. +/// +/// +[] +type MapInputs = + + /// No bindings. + static member Empty : struct (int * string) array = [||] + + /// One binding. + static member Singleton = [| struct (42, "answer") |] + + /// Unsorted keys, negative keys and zero, and a value that two keys share. The absolute values of the keys are distinct, so + /// that a key comparer of absolute values keeps every binding. + static member Several = [| + struct (5, "five") + struct (-3, "minus three") + struct (8, "eight") + struct (0, "zero") + struct (12, "twelve") + struct (-7, "five") + struct (21, "twenty-one") + struct (2, "two") + |] + + /// Every input, as the rows of the tests that run on each one. + static member All = seq { + MapInputs.Empty + MapInputs.Singleton + MapInputs.Several + } + + /// Every input with at least one binding, as the rows of the tests that need one. + static member NonEmpty = seq { + MapInputs.Singleton + MapInputs.Several + } + + /// Bindings that repeat keys with other values; in the FSharp.Core Map module the later binding of a key wins. + static member RepeatedKeys = [| + struct (1, "one") + struct (2, "two") + struct (1, "uno") + struct (3, "three") + struct (2, "dos") + |] + + /// Keys that no input has. + static member AbsentKeys = [| 1; -5; 100 |] + + /// The FSharp.Core map of bindings, the oracle of the contracts. + static member ToMap (bindings : seq) : Map = + bindings + |> Seq.map (fun struct (key, value) -> key, value) + |> Map.ofSeq + + /// The bindings of an FSharp.Core map, sorted by key. + static member OfMap (map : Map) : struct (int * 'T) array = + map + |> Map.toArray + |> Array.map (fun (key, value) -> struct (key, value)) + + /// Bindings sorted by key, to compare the bindings of a map under test with the oracle. + static member SortByKey (bindings : struct (int * 'T) array) = bindings |> Array.sortBy (fun struct (key, _) -> key) + + /// The keys of bindings, in their order. + static member KeysOf (bindings : struct (int * 'T) array) = bindings |> Array.map (fun struct (key, _) -> key) + + /// A binding as a key/value pair, the shape that every .NET dictionary enumerates (ADR 0001, D13). + static member ToKeyValuePair (struct (key : 'Key, value : 'T)) = KeyValuePair (key, value) + + /// A binding as a reference tuple, the shape of most F# code (ADR 0001, D13). + static member ToTuple (struct (key : 'Key, value : 'T)) = key, value + + /// + /// A new string equal to the given one. + /// + /// + /// The default comparer finds the copy equal to the original, and a reference comparison tells them apart: the tests + /// of the values that a value comparer finds equal see which one a map keeps (ADR 0001, D14). + /// + static member Copy (text : string) = String (text.ToCharArray ()) diff --git a/tests/FSharp.Collections.Immutable.Tests/Contracts/MapMutationContract.fs b/tests/FSharp.Collections.Immutable.Tests/Contracts/MapMutationContract.fs new file mode 100644 index 00000000..ced0f199 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/Contracts/MapMutationContract.fs @@ -0,0 +1,295 @@ +namespace FSharp.Collections.Immutable.Tests + +open System +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// +/// The functions that change one binding in the tests of change, as the cases of their rows. +/// +/// +/// MSTest names a row after its values, and a function has no readable name, so a row holds a case and the test takes +/// the function of the case. +/// +[] +type BindingChange = + /// Removes the binding. + | KeepsNothing + /// Sets the value "changed". + | SetsAValue + /// Appends "!" to the current value and keeps a missing value missing. + | ExtendsTheValue + /// Keeps the current value and adds the value "new" to a missing key. + | AddsAMissingValue + + /// Every change, as the rows of the tests that run once per change. + static member All = seq { + BindingChange.KeepsNothing + BindingChange.SetsAValue + BindingChange.ExtendsTheValue + BindingChange.AddsAMissingValue + } + + /// The new value of a binding, or nothing to remove it, for its current value or nothing. + member this.Apply (current : string voption) = + match this with + | BindingChange.KeepsNothing -> ValueNone + | BindingChange.SetsAValue -> ValueSome "changed" + | BindingChange.ExtendsTheValue -> current |> ValueOption.map (fun value -> value + "!") + | BindingChange.AddsAMissingValue -> current |> ValueOption.orElse (ValueSome "new") + +/// +/// The contract of the incremental changes of the map modules that have them: HashMap, SortedMap and +/// IMap (ADR 0001, Appendix B). +/// +/// +/// +/// change, remove, setItem and setItems have the semantics of the FSharp.Core Map module +/// where Map has them, and add, addRange and removeRange wrap the immutable dictionaries of the +/// BCL, which share their semantics: add and addRange reject another value of a key and accept an equal +/// binding, setItem and setItems replace the value (ADR 0001, D14). Map.add replaces the value, so the +/// tests compare add with it only for keys without a binding. +/// +/// +/// addRange and setItems take key/value pairs, and the nested modules Tuple and ValueTuple take +/// reference and struct tuples (ADR 0001, D13). Their tests run once per shape and input. +/// +/// +[] +type MapMutationContract<'M> (ops : MapMutationOps<'M>) = + inherit MapContractBase<'M> (ops.Name, ops.ParamName, ops.OfSeq, ops.ToArray) + + /// The keys of a row of an input: each key of the input and each key that no input has. + static let keysAndAbsentKeys (input : struct (int * string) array) = + Array.append (MapInputs.KeysOf input) MapInputs.AbsentKeys + + /// Every input with each of its keys and each key that no input has: the rows of add, remove and setItem. The rows of + /// every property are built on each read. + static member InputsAndKeys = seq { + for input in MapInputs.All do + for key in keysAndAbsentKeys input do + struct (input, key) + } + + /// Every input with each key that no input has: the rows of add of a key without a binding. + static member InputsAndNewKeys = seq { + for input in MapInputs.All do + for key in MapInputs.AbsentKeys do + struct (input, key) + } + + /// Every input that has bindings with each of its bindings: the rows of add of a key with a binding. + static member NonEmptyInputsAndBindings = seq { + for input in MapInputs.NonEmpty do + for struct (key, value) in input do + struct (input, key, value) + } + + /// Every input with each of its keys, each key that no input has and each change: the rows of change. + static member InputsKeysAndChanges = seq { + for input in MapInputs.All do + for key in keysAndAbsentKeys input do + for change in BindingChange.All do + struct (input, key, change) + } + + /// Every shape with every input: the rows of addRange and setItems. + static member ShapesAndBindings = seq { + for shape in BindingShape.All do + for input in MapInputs.All do + struct (shape, input) + } + + /// Every shape with every input that has bindings. + static member ShapesAndNonEmptyBindings = seq { + for shape in BindingShape.All do + for input in MapInputs.NonEmpty do + struct (shape, input) + } + + /// Adds bindings given as struct tuples with addRange of a shape. + member private _.AddRange (shape : BindingShape, bindings : struct (int * string) array, table : 'M) = + match shape with + | BindingShape.KeyValuePairs -> ops.KeyValuePairs.AddRange (Array.map MapInputs.ToKeyValuePair bindings) table + | BindingShape.Tuples -> ops.Tuples.AddRange (Array.map MapInputs.ToTuple bindings) table + | BindingShape.StructTuples -> ops.StructTuples.AddRange bindings table + + /// Sets bindings given as struct tuples with setItems of a shape. + member private _.SetItems (shape : BindingShape, bindings : struct (int * string) array, table : 'M) = + match shape with + | BindingShape.KeyValuePairs -> ops.KeyValuePairs.SetItems (Array.map MapInputs.ToKeyValuePair bindings) table + | BindingShape.Tuples -> ops.Tuples.SetItems (Array.map MapInputs.ToTuple bindings) table + | BindingShape.StructTuples -> ops.StructTuples.SetItems bindings table + + [.InputsAndNewKeys)>] + member this.``add adds a key without a binding as Map does`` (input : struct (int * string) array, key : int) = + let struct (table, _) = this.Create input + + this.AreBindings ( + Map.add key "added" (MapInputs.ToMap input), + ops.ToArray (ops.Add key "added" table), + "add must add a key without a binding" + ) + + [.NonEmptyInputsAndBindings)>] + member _.``add returns the input itself for a binding that the map has as the BCL Add does`` + (input : struct (int * string) array, key : int, value : string) + = + let table = ops.OfSeq input + + Assert.AreSame( + box table, + box (ops.Add key value table), + $"%s{ops.Name}.add must return the input itself for a binding that the map has" + ) + + [.NonEmptyInputsAndBindings)>] + member _.``add throws for a key that is bound to another value as the BCL Add does`` + (input : struct (int * string) array, key : int, value : string) + = + let table = ops.OfSeq input + + Assert.ThrowsExactly( + Action (fun () -> ops.Add key (value + "!") table |> ignore), + $"%s{ops.Name}.add must reject another value of a key of the map" + ) + |> ignore + + [.ShapesAndBindings)>] + member this.``addRange adds the bindings of new keys`` (shape : BindingShape, input : struct (int * string) array) = + let pairs = [| struct (1, "one"); struct (-5, "minus five"); struct (100, "hundred") |] + + let expected = + (MapInputs.ToMap input, pairs) + ||> Array.fold (fun map struct (key, value) -> Map.add key value map) + + this.AreBindings ( + expected, + ops.ToArray (this.AddRange (shape, pairs, ops.OfSeq input)), + "addRange must add the bindings of new keys" + ) + + [.ShapesAndNonEmptyBindings)>] + member this.``addRange accepts a binding that the map already has`` + (shape : BindingShape, input : struct (int * string) array) + = + this.AreBindings ( + MapInputs.ToMap input, + ops.ToArray (this.AddRange (shape, input, ops.OfSeq input)), + "addRange must accept the bindings that the map already has" + ) + + [)>] + member this.``addRange throws for another value of a key as the BCL AddRange does`` (shape : BindingShape) = + let table = ops.OfSeq MapInputs.Several + + Assert.ThrowsExactly( + Action (fun () -> + this.AddRange (shape, [| struct (5, "other") |], table) + |> ignore + ), + $"%s{ops.Name}.addRange must reject another value of a key of the map" + ) + |> ignore + + Assert.ThrowsExactly( + Action (fun () -> + this.AddRange (shape, [| struct (1, "one"); struct (1, "uno") |], table) + |> ignore + ), + $"%s{ops.Name}.addRange must reject two values of a new key" + ) + |> ignore + + [.InputsKeysAndChanges)>] + member this.``change adds, replaces and removes a binding as Map does`` + (input : struct (int * string) array, key : int, change : BindingChange) + = + let struct (table, _) = this.Create input + + let expected = + Map.change + key + (fun current -> + change.Apply (ValueOption.ofOption current) + |> ValueOption.toOption + ) + (MapInputs.ToMap input) + + this.AreBindings ( + expected, + ops.ToArray (ops.Change key change.Apply table), + "change must change the binding as Map.change does" + ) + + [] + member _.``change calls the function once with the current value`` (key : int) = + let table = ops.OfSeq MapInputs.Several + let expected = + Map.tryFind key (MapInputs.ToMap MapInputs.Several) + |> ValueOption.ofOption + let received = ResizeArray() + + ops.Change + key + (fun current -> + received.Add current + current + ) + table + |> ignore + + CollectionAssert.AreEqual ([| expected |], received.ToArray (), $"%s{ops.Name}.change must pass the current value once") + + [)>] + member this.``clear removes every binding`` (input : struct (int * string) array) = + let struct (table, _) = this.Create input + Assert.IsEmpty (ops.ToArray (ops.Clear table), $"%s{ops.Name}.clear must remove every binding") + + [.InputsAndKeys)>] + member this.``remove removes the binding of a key as Map does`` (input : struct (int * string) array, key : int) = + let struct (table, _) = this.Create input + + this.AreBindings ( + Map.remove key (MapInputs.ToMap input), + ops.ToArray (ops.Remove key table), + "remove must remove the binding of the key" + ) + + [)>] + member this.``removeRange removes the bindings of keys and skips the other keys`` (input : struct (int * string) array) = + let struct (table, _) = this.Create input + let keys = [| 5; 1; -7; 100; 5 |] + + let expected = + (MapInputs.ToMap input, keys) + ||> Array.fold (fun map key -> Map.remove key map) + + this.AreBindings (expected, ops.ToArray (ops.RemoveRange keys table), "removeRange must remove the bindings of the keys") + + [.InputsAndKeys)>] + member this.``setItem adds a new key and replaces the value of an existing key`` + (input : struct (int * string) array, key : int) + = + let struct (table, _) = this.Create input + + this.AreBindings ( + Map.add key "set" (MapInputs.ToMap input), + ops.ToArray (ops.SetItem key "set" table), + "setItem must add a new key and replace the value of an existing key" + ) + + [.ShapesAndBindings)>] + member this.``setItems sets the pairs in order, so that a later pair of a key wins`` + (shape : BindingShape, input : struct (int * string) array) + = + let pairs = [| struct (5, "five again"); struct (1, "one"); struct (1, "uno"); struct (-3, "minus three") |] + + let expected = + (MapInputs.ToMap input, pairs) + ||> Array.fold (fun map struct (key, value) -> Map.add key value map) + + this.AreBindings ( + expected, + ops.ToArray (this.SetItems (shape, pairs, ops.OfSeq input)), + "setItems must set the bindings in order, so that a later binding of a key wins" + ) diff --git a/tests/FSharp.Collections.Immutable.Tests/Contracts/MapOps.fs b/tests/FSharp.Collections.Immutable.Tests/Contracts/MapOps.fs new file mode 100644 index 00000000..7e37a48e --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/Contracts/MapOps.fs @@ -0,0 +1,193 @@ +namespace FSharp.Collections.Immutable.Tests + +open System.Collections.Generic + +/// +/// The reading functions of a map module for the contract tests, over keys and +/// values. +/// +/// +/// +/// A record of functions checks at compile time that a module provides every function of a facet (ADR 0001, D8). The +/// fields follow the order of the signature files. +/// +/// +/// Results of another type of the values are arrays: the instance of a module converts them with its own toArray, +/// so that one record type serves every map module. +/// +/// +type MapOps<'M> = { + /// The name of the module, for the messages of the assertions. + Name : string + /// Maps the name of a parameter of the FSharp.Core oracle to the name of the parameter of the module. + ParamName : string -> string + /// Creates a map of the given bindings; the bindings of the inputs have distinct keys. + OfSeq : seq -> 'M + /// The bindings in the order in which the map enumerates them: the `toArray` of the module, which the conversion tests + /// check as well. + ToArray : 'M -> struct (int * string) array + + Count : 'M -> int + IsEmpty : 'M -> bool + Keys : 'M -> seq + Values : 'M -> seq + + Find : int -> 'M -> string + TryFind : int -> 'M -> string voption + + ContainsBinding : int -> string -> 'M -> bool + ContainsKey : int -> 'M -> bool + Exists : (int -> string -> bool) -> 'M -> bool + FindKey : (int -> string -> bool) -> 'M -> int + Forall : (int -> string -> bool) -> 'M -> bool + Pick : (int -> string -> int voption) -> 'M -> int + TryFindKey : (int -> string -> bool) -> 'M -> int voption + TryPick : (int -> string -> int voption) -> 'M -> int voption + + Fold : + (struct (int * string) array -> int -> string -> struct (int * string) array) + -> struct (int * string) array + -> 'M + -> struct (int * string) array + FoldBack : + (int -> string -> struct (int * string) array -> struct (int * string) array) + -> 'M + -> struct (int * string) array + -> struct (int * string) array + Iter : (int -> string -> unit) -> 'M -> unit + + ToList : 'M -> struct (int * string) list + ToSeq : 'M -> seq +} + +/// +/// The whole-map transformations of a map module for the contract tests. +/// +/// +/// The functions that change the type of the values return the bindings of their result as an array. +/// +type MapTransformOps<'M> = { + /// The name of the module, for the messages of the assertions. + Name : string + /// Maps the name of a parameter of the FSharp.Core oracle to the name of the parameter of the module. + ParamName : string -> string + /// Creates a map of the given bindings; the bindings of the inputs have distinct keys. + OfSeq : seq -> 'M + /// The bindings in the order in which the map enumerates them. + ToArray : 'M -> struct (int * string) array + + /// Chooses new values and returns the bindings of the result. + Choose : (int -> string -> int voption) -> 'M -> struct (int * int) array + Filter : (int -> string -> bool) -> 'M -> 'M + /// Maps the values and returns the bindings of the result. + Map : (int -> string -> int) -> 'M -> struct (int * int) array + Partition : (int -> string -> bool) -> 'M -> struct ('M * 'M) +} + +/// +/// The shapes of the bindings of a map (ADR 0001, D13): the rows of the tests of the functions that take bindings, which +/// convert their inputs, struct tuples, to the shape of the row. +/// +[] +type BindingShape = + /// `KeyValuePair<'Key, 'Value>`, which every .NET dictionary enumerates. + | KeyValuePairs + /// `'Key * 'Value`, the tuples of most F# code. + | Tuples + /// `struct ('Key * 'Value)`, the tuples of ADR 0001, D3. + | StructTuples + + /// Every shape: the rows of the tests that run once per shape. The property builds its rows on each read. + static member All : BindingShape seq = seq { + BindingShape.KeyValuePairs + BindingShape.Tuples + BindingShape.StructTuples + } + +/// +/// The functions of a map module that change a map by bindings of one shape (ADR 0001, D13): the module takes key/value +/// pairs, its nested modules Tuple and ValueTuple take reference and struct tuples. +/// +type MapShapeMutationOps<'Binding, 'M> = { AddRange : seq<'Binding> -> 'M -> 'M; SetItems : seq<'Binding> -> 'M -> 'M } + +/// +/// The incremental changes of a map module for the contract tests. +/// +/// +/// A frozen map has no incremental changes, so its module does not derive this facet (ADR 0001, D1). +/// +type MapMutationOps<'M> = { + /// The name of the module, for the messages of the assertions. + Name : string + /// Maps the name of a parameter of the FSharp.Core oracle to the name of the parameter of the module. + ParamName : string -> string + /// Creates a map of the given bindings; the bindings of the inputs have distinct keys. + OfSeq : seq -> 'M + /// The bindings in the order in which the map enumerates them. + ToArray : 'M -> struct (int * string) array + + Add : int -> string -> 'M -> 'M + Change : int -> (string voption -> string voption) -> 'M -> 'M + Clear : 'M -> 'M + Remove : int -> 'M -> 'M + RemoveRange : seq -> 'M -> 'M + SetItem : int -> string -> 'M -> 'M + + /// `addRange` and `setItems` of the module, over key/value pairs. + KeyValuePairs : MapShapeMutationOps, 'M> + /// `addRange` and `setItems` of the nested module `Tuple`, over reference tuples. + Tuples : MapShapeMutationOps + /// `addRange` and `setItems` of the nested module `ValueTuple`, over struct tuples. + StructTuples : MapShapeMutationOps +} + +/// +/// The functions of a map module that create a map from bindings of one shape (ADR 0001, D13): the overloads of the +/// extension members, or of the members of a comparer struct (D12), for one shape. +/// +type MapShapeCreationOps<'Binding, 'M> = { + OfArray : 'Binding array -> 'M + OfList : 'Binding list -> 'M + OfSeq : seq<'Binding> -> 'M +} + +/// +/// The creation functions of a concrete map module, or of its comparer struct, for the contract tests. +/// +/// +/// +/// A collection derives the contract once for the functions with the default comparers and once for the members of its +/// comparer struct with a key comparer that compares the absolute values of the keys (ADR 0001, D8 and D12). +/// tells the contract how the created maps +/// look the keys up. +/// +/// +/// A frozen map has no value comparer and no builder; the record holds only what every concrete map module creates. +/// +/// +type MapCreationOps<'M> = { + /// The name of the module or of the struct, for the messages of the assertions. + Name : string + /// Maps the name of a parameter of the FSharp.Core oracle to the name of the parameter of the module. + ParamName : string -> string + /// Creates a map of the given bindings with the default comparers, as an existing map of the module. + OfSeq : seq -> 'M + /// The bindings in the order in which the map enumerates them. + ToArray : 'M -> struct (int * string) array + /// Whether the map has a binding of a key, in its key comparer. + ContainsKey : int -> 'M -> bool + /// A key that the key comparer of the created maps considers equal to the given key: the key itself for the default + /// comparer, its negation for the comparer of absolute values. + EquivalentKey : int -> int + + Empty : unit -> 'M + /// `ofArray`, `ofList` and `ofSeq` over key/value pairs. + KeyValuePairs : MapShapeCreationOps, 'M> + /// `ofArray`, `ofList` and `ofSeq` over reference tuples. + Tuples : MapShapeCreationOps + /// `ofArray`, `ofList` and `ofSeq` over struct tuples. + StructTuples : MapShapeCreationOps + /// Creates a map of the given values under the keys that the projection returns for them. + OfSeqBy : (string -> int) -> seq -> 'M + Singleton : int -> string -> 'M +} diff --git a/tests/FSharp.Collections.Immutable.Tests/Contracts/MapTransformContract.fs b/tests/FSharp.Collections.Immutable.Tests/Contracts/MapTransformContract.fs new file mode 100644 index 00000000..984d7b99 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/Contracts/MapTransformContract.fs @@ -0,0 +1,103 @@ +namespace FSharp.Collections.Immutable.Tests + +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// +/// The contract of the whole-map transformations that every map module provides: HashMap, SortedMap, +/// IMap and FrozenMap (ADR 0001, Appendix B). +/// +/// +/// The comparers of the results are collection-specific and have their own tests. +/// +[] +type MapTransformContract<'M> (ops : MapTransformOps<'M>) = + inherit MapContractBase<'M> (ops.Name, ops.ParamName, ops.OfSeq, ops.ToArray) + + /// A chooser that returns a new value for the bindings of long values only. + static let chooser (key : int) (value : string) = + if value.Length > 4 then + ValueSome (key * 10 + value.Length) + else + ValueNone + + /// Every input with each predicate of filter and partition: two that some bindings satisfy, one that no binding + /// satisfies and one that every binding satisfies. + static member InputsAndPredicates = seq { + for input in MapInputs.All do + for predicate in + [| + BindingPredicate.HasLongValue + BindingPredicate.HasNegativeKey + BindingPredicate.Never + BindingPredicate.Always + |] do + struct (input, predicate) + } + + [)>] + member this.``choose keeps the chosen values under their keys`` (input : struct (int * string) array) = + let struct (table, _) = this.Create input + + let expected = + MapInputs.ToMap input + |> Map.toSeq + |> Seq.choose (fun (key, value) -> + chooser key value + |> ValueOption.map (fun chosen -> key, chosen) + |> ValueOption.toOption + ) + |> Map.ofSeq + + this.AreBindings (expected, ops.Choose chooser table, "choose must keep the chosen values under their keys") + + [.InputsAndPredicates)>] + member this.``filter keeps the bindings that satisfy the predicate as Map does`` + (input : struct (int * string) array, predicate : BindingPredicate) + = + let struct (table, _) = this.Create input + + this.AreBindings ( + Map.filter predicate.Matches (MapInputs.ToMap input), + ops.ToArray (ops.Filter predicate.Matches table), + "filter must keep the bindings that satisfy the predicate" + ) + + [)>] + member this.``map transforms the values under their keys as Map does`` (input : struct (int * string) array) = + let struct (table, _) = this.Create input + let mapping (key : int) (value : string) = key * 10 + value.Length + + this.AreBindings ( + Map.map mapping (MapInputs.ToMap input), + ops.Map mapping table, + "map must transform the values under their keys" + ) + + [.InputsAndPredicates)>] + member this.``partition splits the bindings by the predicate as Map does`` + (input : struct (int * string) array, predicate : BindingPredicate) + = + let struct (table, _) = this.Create input + let expectedAccepted, expectedRejected = Map.partition predicate.Matches (MapInputs.ToMap input) + let struct (accepted, rejected) = ops.Partition predicate.Matches table + this.AreBindings (expectedAccepted, ops.ToArray accepted, "partition must accept the bindings that satisfy the predicate") + this.AreBindings (expectedRejected, ops.ToArray rejected, "partition must reject the other bindings") + + [] + member _.``partition calls the predicate once per binding`` () = + let table = ops.OfSeq MapInputs.Several + let visited = ResizeArray() + + ops.Partition + (fun key _ -> + visited.Add key + key > 0 + ) + table + |> ignore + + CollectionAssert.AreEqual ( + MapInputs.KeysOf (ops.ToArray table), + visited.ToArray (), + $"%s{ops.Name}.partition must test each binding once, in the order of enumeration" + ) diff --git a/tests/FSharp.Collections.Immutable.Tests/FSharp.Collections.Immutable.Tests.fsproj b/tests/FSharp.Collections.Immutable.Tests/FSharp.Collections.Immutable.Tests.fsproj index ff9cc792..4669bd86 100644 --- a/tests/FSharp.Collections.Immutable.Tests/FSharp.Collections.Immutable.Tests.fsproj +++ b/tests/FSharp.Collections.Immutable.Tests/FSharp.Collections.Immutable.Tests.fsproj @@ -49,6 +49,12 @@ + + + + + + @@ -63,6 +69,20 @@ + + + + + + + + + + + + + + diff --git a/tests/FSharp.Collections.Immutable.Tests/HashMap/CustomComparersHashMapTests.fs b/tests/FSharp.Collections.Immutable.Tests/HashMap/CustomComparersHashMapTests.fs new file mode 100644 index 00000000..6bdbb030 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/HashMap/CustomComparersHashMapTests.fs @@ -0,0 +1,560 @@ +namespace FSharp.Collections.Immutable.Tests + +open System +open System.Collections.Generic +open FSharp.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// The comparer struct of `HashMap`, `CustomComparersHashMap`, and the functions of the module that create it (ADR 0001, +/// D12). The creation contract runs its members with a key comparer; these tests cover the comparers themselves. +[] +type CustomComparersHashMapTests () = + + /// A value comparer that differs from the default one: it compares strings by their length. + static let byLength = + HashIdentity.FromFunctions (fun (value : string) -> value.Length) (fun left right -> left.Length = right.Length) + + static let nullComparer = Unchecked.defaultof> + + /// The key comparer and the value comparer of a row of the tests of null comparers: the comparer whose parameter the + /// row names is null. + static let comparersWithNull (nullParameter : string) : struct (IEqualityComparer * IEqualityComparer) = + match nullParameter with + | "keyComparer" -> struct (nullComparer, byLength) + | _ -> struct (StringComparer.Ordinal, nullComparer) + + /// The members of the struct that create a hash map, by name. + static let creations : Map -> HashMap> = + Map.ofArray [| + "empty", (fun comparers -> comparers.empty) + "ofArray of key/value pairs", (fun comparers -> comparers.ofArray [| KeyValuePair ("a", "b") |]) + "ofArray of reference tuples", (fun comparers -> comparers.ofArray [| "a", "b" |]) + "ofArray of struct tuples", (fun comparers -> comparers.ofArray [| struct ("a", "b") |]) + "ofList of key/value pairs", (fun comparers -> comparers.ofList [ KeyValuePair ("a", "b") ]) + "ofList of reference tuples", (fun comparers -> comparers.ofList [ ("a", "b") ]) + "ofList of struct tuples", (fun comparers -> comparers.ofList [ struct ("a", "b") ]) + "ofReadOnlySpan", (fun comparers -> comparers.ofReadOnlySpan (ReadOnlySpan [| KeyValuePair ("a", "b") |])) + "ofSeq of key/value pairs", (fun comparers -> comparers.ofSeq (dict [| "a", "b" |])) + "ofSeq of reference tuples", (fun comparers -> comparers.ofSeq (Seq.singleton ("a", "b"))) + "ofSeq of struct tuples", (fun comparers -> comparers.ofSeq (Seq.singleton (struct ("a", "b")))) + "ofSeqBy", (fun comparers -> comparers.ofSeqBy id [| "a" |]) + "ofSpan", (fun comparers -> comparers.ofSpan (Span [| KeyValuePair ("a", "b") |])) + "singleton", (fun comparers -> comparers.singleton "a" "b") + "build", (fun comparers -> comparers.build (fun builder -> builder["a"] <- "b")) + "builder", (fun comparers -> comparers.builder().ToImmutable()) + |] + + /// The members of the struct that create a hash map from the bindings of "one" to "uno" and of "ONE" to "abc", by name: + /// keys that ignore the case and values that compare by their length make the two bindings equal. + static let equalValueCreations : Map -> HashMap> = + Map.ofArray [| + "ofArray of key/value pairs", + (fun comparers -> comparers.ofArray [| KeyValuePair ("one", "uno"); KeyValuePair ("ONE", "abc") |]) + "ofArray of reference tuples", (fun comparers -> comparers.ofArray [| "one", "uno"; "ONE", "abc" |]) + "ofArray of struct tuples", (fun comparers -> comparers.ofArray [| struct ("one", "uno"); struct ("ONE", "abc") |]) + "ofList of key/value pairs", + (fun comparers -> comparers.ofList [ KeyValuePair ("one", "uno"); KeyValuePair ("ONE", "abc") ]) + "ofList of reference tuples", (fun comparers -> comparers.ofList [ "one", "uno"; "ONE", "abc" ]) + "ofList of struct tuples", (fun comparers -> comparers.ofList [ struct ("one", "uno"); struct ("ONE", "abc") ]) + "ofReadOnlySpan", + (fun comparers -> + comparers.ofReadOnlySpan (ReadOnlySpan [| KeyValuePair ("one", "uno"); KeyValuePair ("ONE", "abc") |]) + ) + "ofSeq of key/value pairs", + (fun comparers -> comparers.ofSeq (Seq.ofArray [| KeyValuePair ("one", "uno"); KeyValuePair ("ONE", "abc") |])) + "ofSeq of reference tuples", (fun comparers -> comparers.ofSeq (Seq.ofArray [| "one", "uno"; "ONE", "abc" |])) + "ofSeq of struct tuples", + (fun comparers -> comparers.ofSeq (Seq.ofArray [| struct ("one", "uno"); struct ("ONE", "abc") |])) + "ofSeqBy", + (fun comparers -> + comparers.ofSeqBy + (fun value -> + if String.Equals (value, "uno", StringComparison.Ordinal) then + "one" + else + "ONE" + ) + [| "uno"; "abc" |] + ) + "ofSpan", (fun comparers -> comparers.ofSpan (Span [| KeyValuePair ("one", "uno"); KeyValuePair ("ONE", "abc") |])) + |] + + /// The members of the struct that take a sequence, by name, with the name of the sequence parameter and the member on a + /// null sequence. The struct has comparers that differ from the default ones. + static let nullSequenceMembers : Map unit))> = + let comparers = HashMap.forComparers StringComparer.OrdinalIgnoreCase byLength + + Map.ofArray [| + "ofSeq of key/value pairs", + struct ("elements", + fun () -> + comparers.ofSeq (Unchecked.defaultof>>) + |> ignore) + "ofSeq of reference tuples", + struct ("elements", + fun () -> + comparers.ofSeq (Unchecked.defaultof>) + |> ignore) + "ofSeq of struct tuples", + struct ("elements", + fun () -> + comparers.ofSeq (Unchecked.defaultof>) + |> ignore) + "ofSeqBy", + struct ("source", + fun () -> + comparers.ofSeqBy id (Unchecked.defaultof>) + |> ignore) + |] + + /// The members of the struct that take a sequence, by name, with the name of the sequence parameter and the member on a + /// default flat list that is given as the sequence. A default flat list converts to a sequence without a check and fails + /// with InvalidOperationException when the sequence is enumerated. The last row gives a sequence of objects, which a + /// default flat list of strings is by the covariance of seq: the element type of the sequence is not the one of the + /// flat list. + static let defaultSequenceMembers : Map unit))> = + let comparers = HashMap.forComparers StringComparer.OrdinalIgnoreCase byLength + let objectComparers : CustomComparersHashMap = + HashMap.forKeyComparer EqualityComparer.Default + let defaultPairs = Unchecked.defaultof>> :> seq<_> + let defaultTuples = Unchecked.defaultof> :> seq<_> + let defaultStructTuples = Unchecked.defaultof> :> seq<_> + let defaultValues = Unchecked.defaultof> :> seq<_> + + Map.ofArray [| + "ofSeq of key/value pairs", struct ("elements", fun () -> comparers.ofSeq defaultPairs |> ignore) + "ofSeq of reference tuples", struct ("elements", fun () -> comparers.ofSeq defaultTuples |> ignore) + "ofSeq of struct tuples", struct ("elements", fun () -> comparers.ofSeq defaultStructTuples |> ignore) + "ofSeqBy", struct ("source", fun () -> comparers.ofSeqBy id defaultValues |> ignore) + "ofSeqBy (default flat list of strings as objects)", + struct ("source", + fun () -> + objectComparers.ofSeqBy id FlatListOracles.defaultStringsAsObjects + |> ignore) + |] + + /// The comparer structs of the two paths of the span members, by name: the default comparer of the values wraps + /// CreateRangeWithOverwrite, and any other comparer, even one that behaves like the default one, sets the bindings in a + /// builder. + static let spanPaths : Map> = + Map.ofArray [| + "the default value comparer", HashMap.forKeyComparer StringComparer.OrdinalIgnoreCase + "another value comparer", HashMap.forComparers StringComparer.OrdinalIgnoreCase StringComparer.Ordinal + |] + + /// The bindings that a hash map with keys that ignore the case and values that compare ordinally holds after the given + /// bindings are set in order, sorted by key (ADR 0001, D14): a binding replaces the binding of an equal key only when its + /// value is different. + static let expectedBindings (bindings : struct (string * string) array) = + let expected = ResizeArray() + + for struct (key, value) in bindings do + let index = + expected.FindIndex (fun struct (stored, _) -> String.Equals (stored, key, StringComparison.OrdinalIgnoreCase)) + + if index < 0 then + expected.Add (struct (key, value)) + else + let struct (_, storedValue) = expected[index] + + if not (String.Equals (storedValue, value, StringComparison.Ordinal)) then + expected[index] <- struct (key, value) + + expected + |> Seq.sortBy (fun struct (key, _) -> key) + |> Seq.toArray + + /// The bindings of a hash map sorted by key. + static let sortedBindings (table : HashMap) = + HashMap.toArray table + |> Array.sortBy (fun struct (key, _) -> key) + + /// Every span member and every path with bindings whose keys differ in their case, with values that are equal or + /// different: the rows of the test of the bindings. + static member SpanCreationsPathsAndBindings = seq { + for creation in SpanCreation.All do + for path in spanPaths.Keys do + for bindings in + [| + [||] + [| struct ("a", "x"); struct ("A", "x") |] + [| struct ("a", "x"); struct ("A", "y") |] + [| struct ("a", "x"); struct ("A", "y"); struct ("a", "y") |] + [| struct ("a", "x"); struct ("b", "y"); struct ("A", "z"); struct ("B", "y") |] + |] do + struct (creation, path, bindings) + } + + /// Every span member with every path, as the rows of the tests that run once per member and path. + static member SpanCreationsAndPaths = seq { + for creation in SpanCreation.All do + for path in spanPaths.Keys do + struct (creation, path) + } + + /// The names of the members that create a hash map, as the rows of the test of their comparers. + static member Creations = creations.Keys + + /// The names of the members that create a hash map from bindings that the comparers find equal, as the rows of their + /// test. + static member EqualValueCreations = equalValueCreations.Keys + + /// The names of the members that take a sequence and are given a null one, as the rows of their test. + static member NullSequenceMembers = nullSequenceMembers.Keys + + /// The names of the members that take a sequence and are given a default flat list, as the rows of their test. + static member DefaultSequenceMembers = defaultSequenceMembers.Keys + + [] + member _.``the constructor with both comparers rejects a null comparer`` (nullParameter : string) = + let struct (keyComparer, valueComparer) = comparersWithNull nullParameter + + let ex = + Assert.ThrowsExactly( + Action (fun () -> + CustomComparersHashMap(keyComparer, valueComparer) + |> ignore + ), + "The constructor must reject a null comparer" + ) + + Assert.AreEqual(nullParameter, ex.ParamName, "The constructor must name the null comparer") + + [] + member _.``the constructor with the key comparer rejects a null comparer`` () = + let ex = + Assert.ThrowsExactly( + Action (fun () -> + CustomComparersHashMap nullComparer + |> ignore + ), + "The constructor must reject a null key comparer" + ) + + Assert.AreEqual("keyComparer", ex.ParamName, "The constructor must name the key comparer") + + [] + member _.``forComparers rejects a null comparer`` (nullParameter : string) = + let struct (keyComparer, valueComparer) = comparersWithNull nullParameter + + let ex = + Assert.ThrowsExactly( + Action (fun () -> HashMap.forComparers keyComparer valueComparer |> ignore), + "HashMap.forComparers must reject a null comparer" + ) + + Assert.AreEqual(nullParameter, ex.ParamName, "HashMap.forComparers must name the null comparer") + + [] + member _.``forKeyComparer rejects a null comparer`` () = + let ex = + Assert.ThrowsExactly( + Action (fun () -> + (HashMap.forKeyComparer nullComparer : CustomComparersHashMap) + |> ignore + ), + "HashMap.forKeyComparer must reject a null key comparer" + ) + + Assert.AreEqual("keyComparer", ex.ParamName, "HashMap.forKeyComparer must name the key comparer") + + [] + member _.``members reject a null sequence`` (name : string) = + let struct (paramName, call) = nullSequenceMembers[name] + FlatListAssert.RejectsNull ($"CustomComparersHashMap.%s{name}", paramName, call) + + [] + member _.``members reject a default flat list given as a sequence`` (name : string) = + // The exception names the parameter of the member, not the one of the BCL member that it would have called + let struct (paramName, call) = defaultSequenceMembers[name] + FlatListAssert.RejectsNull ($"CustomComparersHashMap.%s{name}", paramName, call) + + [] + member _.``members take a flat list that is not default as a sequence, also one of strings as a sequence of objects`` () = + let flatList (elements : 'T array) = FlatList.ofArray elements :> seq<'T> + let comparers = HashMap.forKeyComparer StringComparer.OrdinalIgnoreCase + + let sortedBindings (table : HashMap) = + HashMap.toArray table + |> Array.sortBy (fun struct (key, _) -> key) + + CollectionAssert.AreEqual ( + [| struct ("a", "b") |], + sortedBindings (comparers.ofSeq (flatList [| KeyValuePair ("a", "b") |])), + "ofSeq must take the key/value pairs of a flat list" + ) + + CollectionAssert.AreEqual ( + [| struct ("a", "b") |], + sortedBindings (comparers.ofSeq (flatList [| ("a", "b") |])), + "ofSeq must take the reference tuples of a flat list" + ) + + CollectionAssert.AreEqual ( + [| struct ("a", "b") |], + sortedBindings (comparers.ofSeq (flatList [| struct ("a", "b") |])), + "ofSeq must take the struct tuples of a flat list" + ) + + CollectionAssert.AreEqual ( + [| struct ("ab", "ab") |], + sortedBindings (comparers.ofSeqBy id (flatList [| "ab" |])), + "ofSeqBy must take the elements of a flat list" + ) + + // A flat list of strings is a sequence of objects, which a type test for the flat list of objects does not see + let objectComparers : CustomComparersHashMap = + HashMap.forKeyComparer EqualityComparer.Default + let widened = FlatListOracles.stringsAsObjects (FlatList.ofArray [| "ab" |]) + + CollectionAssert.AreEqual ( + [| "ab" :> obj |], + HashMap.keys (objectComparers.ofSeqBy id widened) + |> Seq.toArray, + "ofSeqBy must take the elements of a flat list of strings as objects" + ) + + [] + member _.``forComparers holds both comparers`` () = + let comparers = HashMap.forComparers StringComparer.OrdinalIgnoreCase byLength + + Assert.AreSame>( + StringComparer.OrdinalIgnoreCase, + comparers.KeyComparer, + "HashMap.forComparers must hold the key comparer" + ) + + Assert.AreSame (byLength, comparers.ValueComparer, "HashMap.forComparers must hold the value comparer") + + [] + member _.``forKeyComparer holds the key comparer and the default comparer of the values`` () = + let comparers : CustomComparersHashMap = + HashMap.forKeyComparer StringComparer.OrdinalIgnoreCase + + Assert.AreSame>( + StringComparer.OrdinalIgnoreCase, + comparers.KeyComparer, + "HashMap.forKeyComparer must hold the key comparer" + ) + + Assert.AreSame>( + EqualityComparer.Default, + comparers.ValueComparer, + "HashMap.forKeyComparer must hold the default comparer of the values" + ) + + [] + member _.``forKeyComparer StringComparer.OrdinalIgnoreCase applies the comparer`` () = + let table = + (HashMap.forKeyComparer StringComparer.OrdinalIgnoreCase).ofSeq [| "one", 1; "ONE", 2; "two", 3 |] + + Assert.AreSame>( + StringComparer.OrdinalIgnoreCase, + HashMap.keyComparer table, + "The hash map of HashMap.forKeyComparer must have the key comparer" + ) + + Assert.AreEqual(2, HashMap.count table, "The comparer must merge the keys that differ in their case") + Assert.AreEqual(2, HashMap.find "One" table, "The hash map must keep the last binding of keys that are equal") + Assert.AreEqual(3, HashMap.find "TWO" table, "The hash map must look the keys up with the comparer") + + [] + member _.``every member creates hash maps with the comparers of the struct`` (creation : string) = + let table = creations[creation](HashMap.forComparers StringComparer.OrdinalIgnoreCase byLength) + + Assert.AreSame>( + StringComparer.OrdinalIgnoreCase, + HashMap.keyComparer table, + "The member must create a hash map with the key comparer of the struct" + ) + + Assert.AreSame ( + byLength, + HashMap.valueComparer table, + "The member must create a hash map with the value comparer of the struct" + ) + + [] + member _.``every member keeps the earlier binding of a key for a value that the value comparer finds equal`` + (creation : string) + = + // The indexer of the builder and ToImmutableDictionary keep the earlier binding, its key included (ADR 0001, D14) + let table = + equalValueCreations[creation](HashMap.forComparers StringComparer.OrdinalIgnoreCase byLength) + + let struct (key, value) = + Assert.ContainsSingle (HashMap.toArray table, "The member must merge the bindings of the equal keys") + + Assert.AreEqual("one", key, "The member must keep the earlier key") + Assert.AreEqual("uno", value, "The member must keep the earlier value") + + [] + member _.``the span members replace the binding of a key only for a value that the value comparer finds different`` + (creation : SpanCreation, path : string, bindings : struct (string * string) array) + = + let table = creation.CreateWith (spanPaths[path], Array.map MapInputs.ToKeyValuePair bindings) + + CollectionAssert.AreEqual ( + expectedBindings bindings, + sortedBindings table, + $"%s{creation.Name} with %s{path} must replace the binding of a key only for a different value" + ) + + Assert.AreSame>( + StringComparer.OrdinalIgnoreCase, + HashMap.keyComparer table, + $"%s{creation.Name} with %s{path} must create a hash map with the key comparer of the struct" + ) + + [] + member _.``the span members keep the first key and value of bindings that the comparers find equal`` + (creation : SpanCreation, path : string) + = + // The copies are equal in the comparers and tell the two bindings apart (ADR 0001, D14) + let firstKey = MapInputs.Copy "one" + let firstValue = MapInputs.Copy "uno" + + let table = + creation.CreateWith ( + spanPaths[path], + [| KeyValuePair (firstKey, firstValue); KeyValuePair (MapInputs.Copy "ONE", MapInputs.Copy "uno") |] + ) + + let struct (key, value) = + Assert.ContainsSingle ( + HashMap.toArray table, + $"%s{creation.Name} with %s{path} must merge the bindings of equal keys" + ) + + Assert.AreSame (firstKey, key, $"%s{creation.Name} with %s{path} must keep the first key") + Assert.AreSame (firstValue, value, $"%s{creation.Name} with %s{path} must keep the first value") + + [] + member _.``the span members copy the span`` (creation : SpanCreation, path : string) = + let pairs = [| KeyValuePair ("one", "uno") |] + let table = creation.CreateWith (spanPaths[path], pairs) + pairs[0] <- KeyValuePair ("one", "changed") + Assert.AreEqual("uno", HashMap.find "one" table, $"%s{creation.Name} with %s{path} must copy the span") + + [] + member _.``the span members take an array when the types of the struct are known`` (creation : SpanCreation, path : string) = + let table = creation.CreateFromArrayWith (spanPaths[path], [| KeyValuePair ("one", "uno") |]) + + CollectionAssert.AreEqual ( + [| struct ("one", "uno") |], + HashMap.toArray table, + $"%s{creation.Name} with %s{path} must take an array" + ) + + [] + member _.``the default value of the struct stands for the default comparers`` () = + let comparers = CustomComparersHashMap() + + Assert.AreSame>( + EqualityComparer.Default, + comparers.KeyComparer, + "The default value of the struct must report the default key comparer" + ) + + Assert.AreSame>( + EqualityComparer.Default, + comparers.ValueComparer, + "The default value of the struct must report the default value comparer" + ) + + Assert.AreSame>( + EqualityComparer.Default, + HashMap.keyComparer (comparers.ofArray [| "a", "b" |]), + "The default value of the struct must create hash maps with the default key comparer" + ) + + [] + member _.``ofSeq returns a hash map with the comparers of the struct itself and shares one with its key comparer`` () = + let table = + (HashMap.forKeyComparer StringComparer.OrdinalIgnoreCase).ofArray [| "one", "uno"; "two", "dos" |] + + Assert.AreSame ( + table, + (HashMap.forKeyComparer StringComparer.OrdinalIgnoreCase).ofSeq table, + "ofSeq must return a hash map with the comparers of the struct itself" + ) + + let byLengthTable = (HashMap.forComparers StringComparer.OrdinalIgnoreCase byLength).ofSeq table + Assert.AreSame (byLength, HashMap.valueComparer byLengthTable, "ofSeq must give the hash map the value comparer") + + CollectionAssert.AreEqual ( + HashMap.toArray table, + HashMap.toArray byLengthTable, + "ofSeq must keep the bindings of a hash map with its key comparer, in their order" + ) + + [] + member _.``ofSeq gives a hash map the key comparer of the struct and keeps the last of the keys that become equal`` () = + let table = HashMap.ofArray [| "a", 1; "A", 2; "b", 3 |] + + let struct (_, last) = + HashMap.toArray table + |> Array.filter (fun struct (key, _) -> String.Equals (key, "a", StringComparison.OrdinalIgnoreCase)) + |> Array.last + + let ignoreCase = (HashMap.forKeyComparer StringComparer.OrdinalIgnoreCase).ofSeq table + + Assert.AreSame>( + StringComparer.OrdinalIgnoreCase, + HashMap.keyComparer ignoreCase, + "ofSeq must give the hash map the key comparer of the struct" + ) + + Assert.AreEqual(2, HashMap.count ignoreCase, "ofSeq must merge the keys that the comparer of the struct makes equal") + + Assert.AreEqual( + last, + HashMap.find "a" ignoreCase, + "ofSeq must keep the value of the last of the equal keys in the order of enumeration" + ) + + [] + member _.``ofSeq keeps the first of the keys that become equal when the value comparer finds their values equal`` () = + // "uno" and "abc" have the same length, so the earlier binding stays, its key included (ADR 0001, D14) + let table = HashMap.ofArray [| "a", "uno"; "A", "abc"; "b", "dos" |] + + let struct (firstKey, firstValue) = + HashMap.toArray table + |> Array.find (fun struct (key, _) -> String.Equals (key, "a", StringComparison.OrdinalIgnoreCase)) + + let ignoreCase = (HashMap.forComparers StringComparer.OrdinalIgnoreCase byLength).ofSeq table + Assert.AreEqual(2, HashMap.count ignoreCase, "ofSeq must merge the keys that the comparer of the struct makes equal") + + Assert.AreEqual( + ValueSome firstKey, + HashMap.tryGetKey "a" ignoreCase, + "ofSeq must keep the first of the equal keys in the order of enumeration" + ) + + Assert.AreEqual( + firstValue, + HashMap.find "a" ignoreCase, + "ofSeq must keep the value of the first of the equal keys" + ) + + [] + member _.``ofSeqBy compares the keys and the values with the comparers of the struct`` () = + let comparers = HashMap.forComparers StringComparer.OrdinalIgnoreCase byLength + // The two keys differ only in their case, so that the key comparer of the struct makes them equal + let projection (value : string) = + if String.Equals (value, "one", StringComparison.Ordinal) then + "key" + else + "KEY" + + Assert.AreEqual( + 1, + HashMap.count (comparers.ofSeqBy projection [| "one"; "two" |]), + "ofSeqBy must accept equal keys whose values are equal in the value comparer, as the BCL does" + ) + + Assert.ThrowsExactly( + Action (fun () -> comparers.ofSeqBy projection [| "one"; "three" |] |> ignore), + "ofSeqBy must reject equal keys whose values differ in the value comparer, as the BCL does" + ) + |> ignore diff --git a/tests/FSharp.Collections.Immutable.Tests/HashMap/HashMapBuilderTests.fs b/tests/FSharp.Collections.Immutable.Tests/HashMap/HashMapBuilderTests.fs new file mode 100644 index 00000000..143f1d19 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/HashMap/HashMapBuilderTests.fs @@ -0,0 +1,199 @@ +namespace FSharp.Collections.Immutable.Tests + +open System +open System.Collections.Generic +open FSharp.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// The builder functions of `HashMap`; maps have no builder module (ADR 0001, D2). +[] +type HashMapBuilderTests () = + + /// A value comparer that differs from the default one: it finds numbers of the same absolute value equal. + static let absoluteValues = + HashIdentity.FromFunctions (fun (value : int) -> abs value) (fun left right -> abs left = abs right) + + [] + member _.``build returns the bindings that the action sets in a builder with the default comparers`` () = + let table = + HashMap.build (fun builder -> + for i in 1..3 do + builder[i] <- i * i + ) + + CollectionAssert.AreEqual ( + [| struct (1, 1); struct (2, 4); struct (3, 9) |], + MapInputs.SortByKey (HashMap.toArray table), + "HashMap.build must return the bindings that the action set" + ) + + Assert.AreSame>( + EqualityComparer.Default, + HashMap.keyComparer table, + "HashMap.build must compare the keys with the default comparer" + ) + + [] + member _.``build returns an empty map for an action that sets nothing`` () = + let table : HashMap = HashMap.build ignore + Assert.IsTrue (HashMap.isEmpty table, "HashMap.build must return an empty map when the action sets nothing") + + [] + member _.``build keeps the earlier binding of a key for a value that the default comparer finds equal`` () = + // The indexer of the builder leaves the binding as it is, its key included (ADR 0001, D14) + let first = MapInputs.Copy "uno" + + let table = + HashMap.build (fun builder -> + builder["one"] <- first + builder["one"] <- MapInputs.Copy "uno" + ) + + Assert.AreSame (first, HashMap.find "one" table, "HashMap.build must keep the earlier value of the key") + + [] + member _.``the build of the comparer struct has both comparers and merges the equal bindings`` () = + let comparers = HashMap.forComparers StringComparer.OrdinalIgnoreCase absoluteValues + + let table = + comparers.build (fun builder -> + builder["one"] <- 1 + builder["ONE"] <- -1 + ) + + Assert.AreSame>( + StringComparer.OrdinalIgnoreCase, + HashMap.keyComparer table, + "The build of the struct must create a hash map with the key comparer" + ) + + Assert.AreSame ( + absoluteValues, + HashMap.valueComparer table, + "The build of the struct must create a hash map with the value comparer" + ) + + CollectionAssert.AreEqual ( + [| struct ("one", 1) |], + HashMap.toArray table, + "The build of the struct must keep the earlier binding for a value that the value comparer finds equal" + ) + + [] + member _.``update applies the action to a builder over the bindings and keeps the comparers`` () = + let table = (HashMap.forKeyComparer StringComparer.OrdinalIgnoreCase).ofArray [| "one", 1; "two", 2 |] + + let updated = + HashMap.update + (fun builder -> + builder["THREE"] <- 3 + builder.Remove "ONE" |> ignore + ) + table + + CollectionAssert.AreEqual ( + [| struct ("THREE", 3); struct ("two", 2) |], + Array.sortBy (fun struct (key, _) -> key) (HashMap.toArray updated), + "HashMap.update must apply the action to the bindings of the map" + ) + + Assert.AreSame>( + StringComparer.OrdinalIgnoreCase, + HashMap.keyComparer updated, + "HashMap.update must keep the key comparer" + ) + + Assert.AreEqual(2, HashMap.count table, "HashMap.update must not change the input") + Assert.IsTrue (HashMap.containsKey "one" table, "HashMap.update must not change the input") + + [] + member _.``builder starts empty with the default comparers`` () = + let builder = HashMap.builder() + Assert.AreEqual(0, builder.Count, "HashMap.builder must start without bindings") + Assert.AreSame>( + EqualityComparer.Default, + builder.KeyComparer, + "HashMap.builder must compare the keys with the default comparer" + ) + Assert.AreSame>( + EqualityComparer.Default, + builder.ValueComparer, + "HashMap.builder must compare the values with the default comparer" + ) + + [] + member _.``the builder of the comparer struct has both comparers`` () = + let valueComparer = + HashIdentity.FromFunctions (fun (value : int) -> abs value) (fun left right -> abs left = abs right) + let builder = (HashMap.forComparers StringComparer.OrdinalIgnoreCase valueComparer).builder() + Assert.AreSame>( + StringComparer.OrdinalIgnoreCase, + builder.KeyComparer, + "The builder of HashMap.forComparers must have the key comparer" + ) + Assert.AreSame (valueComparer, builder.ValueComparer, "The builder of HashMap.forComparers must have the value comparer") + + [] + member _.``the builder of a comparer struct with a key comparer has the default comparer of the values`` () = + let builder : HashMap.Builder = + (HashMap.forKeyComparer StringComparer.OrdinalIgnoreCase).builder() + builder["one"] <- 1 + builder["ONE"] <- 2 + Assert.AreEqual(1, builder.Count, "The builder of HashMap.forKeyComparer must look the keys up with the comparer") + + Assert.AreSame>( + EqualityComparer.Default, + builder.ValueComparer, + "The builder of HashMap.forKeyComparer must compare the values with the default comparer" + ) + + [] + member _.``ofBuilder returns the bindings and keeps the builder usable`` () = + let builder : HashMap.Builder = + (HashMap.forKeyComparer StringComparer.OrdinalIgnoreCase).builder() + builder["one"] <- 1 + let table = HashMap.ofBuilder builder + builder["two"] <- 2 + + CollectionAssert.AreEqual ([| struct ("one", 1) |], HashMap.toArray table, "HashMap.ofBuilder must return the bindings") + Assert.AreEqual(2, builder.Count, "HashMap.ofBuilder must keep the builder usable") + Assert.AreSame>( + StringComparer.OrdinalIgnoreCase, + HashMap.keyComparer table, + "HashMap.ofBuilder must keep the comparers" + ) + + [] + member _.``ofBuilder throws for a null builder`` () = + let ex = + Assert.ThrowsExactly( + Action (fun () -> + HashMap.ofBuilder (Unchecked.defaultof.Builder>) + |> ignore + ), + "HashMap.ofBuilder must reject a null builder" + ) + + Assert.AreEqual("builder", ex.ParamName, "HashMap.ofBuilder must name the builder parameter") + + [] + member _.``toBuilder holds the bindings and leaves the map unchanged`` () = + let table = (HashMap.forKeyComparer StringComparer.OrdinalIgnoreCase).ofArray [| "one", 1; "two", 2 |] + + let builder = HashMap.toBuilder table + builder["THREE"] <- 3 + builder.Remove "ONE" |> ignore + + Assert.AreEqual(2, builder.Count, "HashMap.toBuilder must hold the bindings and use the key comparer") + Assert.AreSame>( + StringComparer.OrdinalIgnoreCase, + builder.KeyComparer, + "HashMap.toBuilder must keep the key comparer" + ) + Assert.AreEqual(2, HashMap.count table, "HashMap.toBuilder must not change the map") + Assert.IsTrue (HashMap.containsKey "one" table, "HashMap.toBuilder must not change the map") + + [] + member _.``a builder of a map returns the map itself when nothing changed`` () = + let table = HashMap.ofArray [| struct ("one", 1) |] + Assert.AreSame (table, HashMap.ofBuilder (HashMap.toBuilder table), "A builder that changed nothing must return its map") diff --git a/tests/FSharp.Collections.Immutable.Tests/HashMap/HashMapComputationExpressionTests.fs b/tests/FSharp.Collections.Immutable.Tests/HashMap/HashMapComputationExpressionTests.fs new file mode 100644 index 00000000..76f4a356 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/HashMap/HashMapComputationExpressionTests.fs @@ -0,0 +1,403 @@ +namespace FSharp.Collections.Immutable.Tests + +open System +open System.Collections.Generic +open FSharp.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// The `hashMap { }` computation expression. +[] +type HashMapComputationExpressionTests () = + + /// The bindings of a map of integer keys, sorted by key. + static let bindings (table : HashMap) = MapInputs.SortByKey (HashMap.toArray table) + + /// The expressions whose keys ignore the case and that bind "one" to the first value and then "ONE" to the second one, + /// by the member that sets the second binding. + static let equalValueExpressions : Map string -> HashMap> = + let ignoreCase = HashMapCE StringComparer.OrdinalIgnoreCase + + Map.ofArray [| + "yield of a struct pair", + (fun first second -> ignoreCase { + yield struct ("one", first) + yield struct ("ONE", second) + }) + "yield of a key/value pair", + (fun first second -> ignoreCase { + yield struct ("one", first) + yield KeyValuePair ("ONE", second) + }) + "yield! of a sequence of struct pairs", + (fun first second -> ignoreCase { + yield struct ("one", first) + yield! Seq.singleton (struct ("ONE", second)) + }) + "yield! of an array of struct pairs", + (fun first second -> ignoreCase { + yield struct ("one", first) + yield! [| struct ("ONE", second) |] + }) + "yield! of key/value pairs", + (fun first second -> ignoreCase { + yield struct ("one", first) + yield! Seq.singleton (KeyValuePair ("ONE", second)) + }) + "yield! of a hash map", + (fun first second -> ignoreCase { + yield struct ("one", first) + yield! HashMap.singleton "ONE" second + }) + |] + + /// The expressions that take a sequence, by name, with the name of the sequence parameter and the expression on a null + /// sequence. The expression rejects the sequence when the member runs, and not when the buffer does. + static let nullSequenceExpressions : Map unit))> = + Map.ofArray [| + "yield! of a sequence of struct pairs", + struct ("pairs", + fun () -> + hashMap { yield! Unchecked.defaultof> } + |> ignore) + "yield! of a sequence of key/value pairs", + struct ("pairs", + fun () -> + hashMap { yield! Unchecked.defaultof>> } + |> ignore) + "for", + struct ("source", + fun () -> + hashMap { + for item in Unchecked.defaultof> do + yield struct (item, "x") + } + |> ignore) + |] + + /// The expressions that take a sequence, by name, with the name of the sequence parameter and the expression on a + /// default flat list that is given as the sequence. A default flat list converts to a sequence without a check and fails + /// with InvalidOperationException when the sequence is enumerated. The last row gives a sequence of objects, which a + /// default flat list of strings is by the covariance of seq: the element type of the sequence is not the one of the + /// flat list. + static let defaultSequenceExpressions : Map unit))> = + let defaultStructPairs = Unchecked.defaultof> :> seq<_> + let defaultPairs = Unchecked.defaultof>> :> seq<_> + let defaultKeys = Unchecked.defaultof> :> seq<_> + + Map.ofArray [| + "yield! of a sequence of struct pairs", struct ("pairs", fun () -> hashMap { yield! defaultStructPairs } |> ignore) + "yield! of a sequence of key/value pairs", struct ("pairs", fun () -> hashMap { yield! defaultPairs } |> ignore) + "for", + struct ("source", + fun () -> + hashMap { + for item in defaultKeys do + yield struct (item, "x") + } + |> ignore) + "for (default flat list of strings as objects)", + struct ("source", + fun () -> + hashMap { + for item in FlatListOracles.defaultStringsAsObjects do + yield struct (item, "x") + } + |> ignore) + |] + + /// The expressions that take an array or a hash map, by name, with the name of the parameter and the expression on a null + /// one. A null array or hash map is not a sequence for the member, which would fail with NullReferenceException when + /// the buffer runs; the expression rejects it when the member runs. + static let nullCollectionExpressions : Map unit))> = + Map.ofArray [| + "yield! of an array of struct pairs", + struct ("pairs", + fun () -> + hashMap { yield! Unchecked.defaultof } + |> ignore) + "yield! of a hash map", + struct ("table", + fun () -> + hashMap { yield! Unchecked.defaultof> } + |> ignore) + |] + + /// The names of the members that set a binding, as the rows of the test of the values that the default comparer finds + /// equal. + static member EqualValueExpressions = equalValueExpressions.Keys + + /// The names of the expressions that take an array or a hash map and are given a null one, as the rows of their test. + static member NullCollectionExpressions = nullCollectionExpressions.Keys + + /// The names of the expressions that take a sequence and are given a null one, as the rows of their test. + static member NullSequenceExpressions = nullSequenceExpressions.Keys + + /// The names of the expressions that take a sequence and are given a default flat list, as the rows of their test. + static member DefaultSequenceExpressions = defaultSequenceExpressions.Keys + + [] + member _.``an expression without bindings is empty`` () = + let table : HashMap = hashMap { () } + Assert.IsTrue (HashMap.isEmpty table, "hashMap { () } must be empty") + + [] + member _.``yield sets struct pairs and key value pairs`` () = + let table = hashMap { + yield struct (1, "one") + yield KeyValuePair (2, "two") + } + + CollectionAssert.AreEqual ( + [| struct (1, "one"); struct (2, "two") |], + bindings table, + "hashMap must set the yielded bindings" + ) + + [] + member _.``implicit yields set the bindings`` () = + let table = hashMap { + struct (1, "one") + struct (2, "two") + } + + CollectionAssert.AreEqual ( + [| struct (1, "one"); struct (2, "two") |], + bindings table, + "hashMap must set the implicitly yielded bindings" + ) + + [] + member _.``a later binding of a key replaces an earlier one`` () = + let table = hashMap { + yield struct (1, "one") + yield struct (1, "uno") + yield KeyValuePair (1, "eins") + } + + CollectionAssert.AreEqual ([| struct (1, "eins") |], bindings table, "hashMap must keep the last binding of a key") + + [] + member _.``a value that the default comparer finds equal keeps the earlier binding`` (expression : string) = + // The indexer of the builder keeps the earlier binding, its key included (ADR 0001, D14); the copies are equal in + // the default comparer of the values and tell the two values apart + let first = MapInputs.Copy "uno" + let table = equalValueExpressions[expression] first (MapInputs.Copy "uno") + + let struct (key, value) = + Assert.ContainsSingle (HashMap.toArray table, "hashMap must merge the bindings of the equal keys") + + Assert.AreEqual("one", key, "hashMap must keep the earlier key") + Assert.AreSame (first, value, "hashMap must keep the earlier value") + + [] + member _.``yield! sets the bindings of sequences, arrays, maps and other expressions`` () = + let inner = hashMap { yield struct (7, "seven") } + let dictionary = Dictionary() + dictionary[6] <- "six" + + let table = hashMap { + yield! [ struct (1, "one"); struct (2, "two") ] + yield! [| struct (3, "three") |] + yield! [ KeyValuePair (4, "four") ] + yield! HashMap.singleton 5 "five" + yield! dictionary + yield! inner + } + + CollectionAssert.AreEqual ([| 1..7 |], MapInputs.KeysOf (bindings table), "hashMap must set the bindings of every yield!") + + [] + member _.``for sets the bindings of the body for each item`` () = + let table = hashMap { + for item in 1..3 do + yield struct (item, string (item * 10)) + } + + CollectionAssert.AreEqual ( + [| struct (1, "10"); struct (2, "20"); struct (3, "30") |], + bindings table, + "hashMap must run the body of for for each item" + ) + + [] + member _.``while sets the bindings while the guard holds`` () = + let mutable count = 0 + + let table = hashMap { + while count < 3 do + count <- count + 1 + yield struct (count, string count) + } + + CollectionAssert.AreEqual ( + [| 1; 2; 3 |], + MapInputs.KeysOf (bindings table), + "hashMap must run the body of while while the guard holds" + ) + + [] + member _.``if without else and following bindings combine`` () = + let table = hashMap { + if false then + yield struct (0, "zero") + + yield struct (1, "one") + + if true then + yield struct (2, "two") + } + + CollectionAssert.AreEqual ( + [| 1; 2 |], + MapInputs.KeysOf (bindings table), + "hashMap must combine conditional and plain bindings" + ) + + [] + member _.``try with sets the bindings of the handler after an exception`` () = + let table = hashMap { + try + yield struct (1, "one") + failwith "fails" + with _ -> + yield struct (2, "two") + } + + CollectionAssert.AreEqual ( + [| 1; 2 |], + MapInputs.KeysOf (bindings table), + "hashMap must keep the bindings before the exception and add the handler's" + ) + + [] + member _.``try finally runs the compensation`` () = + let mutable compensated = false + + let table = hashMap { + try + yield struct (1, "one") + finally + compensated <- true + } + + Assert.IsTrue (compensated, "hashMap must run the compensation of try finally") + CollectionAssert.AreEqual ( + [| 1 |], + MapInputs.KeysOf (bindings table), + "hashMap must keep the bindings of the body of try finally" + ) + + [] + member _.``use disposes the resource after the body`` () = + let mutable disposed = false + + let table = hashMap { + use _resource = { + new IDisposable with + member _.Dispose () = disposed <- true + } + + yield struct (1, "one") + } + + Assert.IsTrue (disposed, "hashMap must dispose the resource of use") + CollectionAssert.AreEqual ( + [| 1 |], + MapInputs.KeysOf (bindings table), + "hashMap must keep the bindings of the body of use" + ) + + [] + member _.``hashMap compares with the default comparers`` () = + let table = hashMap { yield struct ("one", 1) } + Assert.AreSame>( + EqualityComparer.Default, + HashMap.keyComparer table, + "hashMap must compare the keys with the default comparer" + ) + Assert.AreSame>( + EqualityComparer.Default, + HashMap.valueComparer table, + "hashMap must compare the values with the default comparer" + ) + + [] + member _.``HashMapCE compares the keys with the comparer that it takes`` () = + let ignoreCase = HashMapCE StringComparer.OrdinalIgnoreCase + + let table = ignoreCase { + yield struct ("one", 1) + yield struct ("ONE", 2) + yield struct ("two", 3) + } + + Assert.AreSame>( + StringComparer.OrdinalIgnoreCase, + HashMap.keyComparer table, + "HashMapCE must keep the key comparer" + ) + Assert.AreEqual(2, HashMap.count table, "HashMapCE must merge the keys that the comparer considers equal") + Assert.AreEqual(2, HashMap.find "One" table, "HashMapCE must keep the last binding of equal keys") + + [] + member _.``yield! and for reject a null sequence`` (name : string) = + let struct (paramName, run) = nullSequenceExpressions[name] + FlatListAssert.RejectsNull ($"hashMap %s{name}", paramName, run) + + [] + member _.``yield! rejects a null array and a null hash map`` (name : string) = + let struct (paramName, run) = nullCollectionExpressions[name] + FlatListAssert.RejectsNull ($"hashMap %s{name}", paramName, run) + + [] + member _.``yield! and for reject a default flat list given as a sequence`` (name : string) = + // The member rejects it as the functions of HashMap do, when the expression runs it and not when the buffer does + let struct (paramName, run) = defaultSequenceExpressions[name] + FlatListAssert.RejectsNull ($"hashMap %s{name}", paramName, run) + + [] + member _.``yield! and for take a flat list that is not default as a sequence, also one of strings as objects`` () = + let structPairs = FlatList.ofArray [| struct (1, "one"); struct (2, "two") |] :> seq<_> + let pairs = FlatList.ofArray [| KeyValuePair (3, "three") |] :> seq<_> + let keys = FlatList.ofArray [| 4 |] :> seq<_> + + let table = hashMap { + yield! structPairs + yield! pairs + + for key in keys do + yield struct (key, "four") + } + + CollectionAssert.AreEqual ( + [| struct (1, "one"); struct (2, "two"); struct (3, "three"); struct (4, "four") |], + bindings table, + "hashMap must take the elements of a flat list in yield! and for" + ) + + // A flat list of strings is a sequence of objects, which a type test for the flat list of objects does not see + let widened = FlatListOracles.stringsAsObjects (FlatList.ofArray [| "a"; "b" |]) + + let objectTable = hashMap { + for item in widened do + yield struct (item, 1) + } + + CollectionAssert.AreEquivalent ( + [| "a" :> obj; "b" :> obj |], + HashMap.keys objectTable |> Seq.toArray, + "hashMap must take the elements of a flat list of strings as objects in for" + ) + + [] + member _.``HashMapCE rejects a null key comparer`` () = + let ex = + Assert.ThrowsExactly( + Action (fun () -> + HashMapCE(Unchecked.defaultof>) + |> ignore + ), + "HashMapCE must reject a null key comparer" + ) + + Assert.AreEqual("keyComparer", ex.ParamName, "HashMapCE must name the key comparer") diff --git a/tests/FSharp.Collections.Immutable.Tests/HashMap/HashMapContractTests.fs b/tests/FSharp.Collections.Immutable.Tests/HashMap/HashMapContractTests.fs new file mode 100644 index 00000000..fd1e86cf --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/HashMap/HashMapContractTests.fs @@ -0,0 +1,29 @@ +namespace FSharp.Collections.Immutable.Tests + +open FSharp.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// The reading contract for `HashMap`. +[] +type HashMapReadingTests () = + inherit MapContract> (HashMapOps.reading) + +/// The transformation contract for `HashMap`. +[] +type HashMapTransformTests () = + inherit MapTransformContract> (HashMapOps.transform) + +/// The mutation contract for `HashMap`. +[] +type HashMapMutationTests () = + inherit MapMutationContract> (HashMapOps.mutation) + +/// The creation contract for `HashMap` and its extension members, with the default comparers. +[] +type HashMapCreationTests () = + inherit MapCreationContract> (HashMapOps.creation) + +/// The creation contract for the members of `CustomComparersHashMap`, with a key comparer. +[] +type CustomComparersHashMapCreationTests () = + inherit MapCreationContract> (HashMapOps.customComparersCreation) diff --git a/tests/FSharp.Collections.Immutable.Tests/HashMap/HashMapExtensionsTests.fs b/tests/FSharp.Collections.Immutable.Tests/HashMap/HashMapExtensionsTests.fs new file mode 100644 index 00000000..96763107 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/HashMap/HashMapExtensionsTests.fs @@ -0,0 +1,141 @@ +namespace FSharp.Collections.Immutable.Tests + +open System +open System.Collections.Generic +open FSharp.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// The static members of `HashMapExtensions`, which `HashMap.ofArray`, `HashMap.ofList` and `HashMap.ofSeq` reach through +/// the abbreviation, called as user code calls them (ADR 0001, D13). +[] +type HashMapExtensionsTests () = + + /// The creations of a hash map from bindings that repeat the key "a", by the member and the shape that they call. + static let repeatedKeyCreations : Map HashMap> = + Map.ofArray [| + "ofArray of key/value pairs", + (fun () -> HashMap.ofArray [| KeyValuePair ("a", 1); KeyValuePair ("b", 2); KeyValuePair ("a", 3) |]) + "ofArray of reference tuples", (fun () -> HashMap.ofArray [| "a", 1; "b", 2; "a", 3 |]) + "ofArray of struct tuples", (fun () -> HashMap.ofArray [| struct ("a", 1); struct ("b", 2); struct ("a", 3) |]) + "ofList of key/value pairs", + (fun () -> HashMap.ofList [ KeyValuePair ("a", 1); KeyValuePair ("b", 2); KeyValuePair ("a", 3) ]) + "ofList of reference tuples", (fun () -> HashMap.ofList [ "a", 1; "b", 2; "a", 3 ]) + "ofList of struct tuples", (fun () -> HashMap.ofList [ struct ("a", 1); struct ("b", 2); struct ("a", 3) ]) + "ofSeq of key/value pairs", + (fun () -> HashMap.ofSeq (Seq.ofArray [| KeyValuePair ("a", 1); KeyValuePair ("b", 2); KeyValuePair ("a", 3) |])) + "ofSeq of reference tuples", (fun () -> HashMap.ofSeq (Seq.ofArray [| "a", 1; "b", 2; "a", 3 |])) + "ofSeq of struct tuples", + (fun () -> HashMap.ofSeq (Seq.ofArray [| struct ("a", 1); struct ("b", 2); struct ("a", 3) |])) + |] + + /// The creations of a hash map of "one" and "two" in a pipeline, by the source of the bindings. + static let pipelines : Map HashMap> = + Map.ofArray [| + "an array of key/value pairs", + (fun () -> + [| KeyValuePair ("one", 1); KeyValuePair ("two", 2) |] + |> HashMap.ofArray + ) + "a list of reference tuples", (fun () -> [ "one", 1; "two", 2 ] |> HashMap.ofList) + "an array of struct tuples", + (fun () -> + [| struct ("one", 1); struct ("two", 2) |] + |> HashMap.ofArray + ) + "a sequence of reference tuples", (fun () -> Seq.ofArray [| "one", 1; "two", 2 |] |> HashMap.ofSeq) + "a dictionary", (fun () -> dict [| "one", 1; "two", 2 |] |> HashMap.ofSeq) + "an F# map", (fun () -> Map.ofArray [| "one", 1; "two", 2 |] |> HashMap.ofSeq) + "a hash map", (fun () -> HashMap.ofArray [| "one", 1; "two", 2 |] |> HashMap.ofSeq) + |] + + /// The bindings of a hash map as reference tuples, sorted by key, to compare them with those of an F# map. + static let sortedBindings (table : HashMap) = + HashMap.toArray table + |> Array.map (fun struct (key, value) -> key, value) + |> Array.sortBy fst + + /// The creations of a hash map from the given bindings, by the member and the shape that they call. + static let shapedCreations : Map HashMap> = + Map.ofArray [| + "ofArray of key/value pairs", (fun bindings -> HashMap.ofArray (Array.map MapInputs.ToKeyValuePair bindings)) + "ofArray of reference tuples", (fun bindings -> HashMap.ofArray (Array.map MapInputs.ToTuple bindings)) + "ofArray of struct tuples", (fun bindings -> HashMap.ofArray bindings) + "ofList of key/value pairs", + (fun bindings -> + HashMap.ofList ( + bindings + |> Array.map MapInputs.ToKeyValuePair + |> List.ofArray + ) + ) + "ofList of reference tuples", + (fun bindings -> HashMap.ofList (bindings |> Array.map MapInputs.ToTuple |> List.ofArray)) + "ofList of struct tuples", (fun bindings -> HashMap.ofList (List.ofArray bindings)) + "ofSeq of key/value pairs", (fun bindings -> HashMap.ofSeq (Seq.map MapInputs.ToKeyValuePair bindings)) + "ofSeq of reference tuples", (fun bindings -> HashMap.ofSeq (Seq.map MapInputs.ToTuple bindings)) + "ofSeq of struct tuples", (fun bindings -> HashMap.ofSeq (Seq.ofArray bindings)) + |] + + /// The names of the creations from bindings that repeat a key, as the rows of the test of the last binding. + static member RepeatedKeyCreations = repeatedKeyCreations.Keys + + /// The names of the sources of the pipelines: the rows of the test of the pipelines. + static member Pipelines = pipelines.Keys + + /// The names of the creations from the given bindings, as the rows of the test of the values that the default comparer + /// finds equal. + static member ShapedCreations = shapedCreations.Keys + + [] + member _.``every shape keeps the last binding of a key as Map does`` (creation : string) = + CollectionAssert.AreEqual ( + Map.toArray (Map.ofArray [| "a", 1; "b", 2; "a", 3 |]), + sortedBindings (repeatedKeyCreations[creation]()), + "The hash map must keep the last binding of a key, as Map does" + ) + + [] + member _.``every shape keeps the earlier binding of a key for a value that the default comparer finds equal`` + (creation : string) + = + // The indexer of the builder keeps the earlier binding, its key included (ADR 0001, D14); the copies are equal in + // the default comparers and tell the two bindings apart + let firstKey = MapInputs.Copy "one" + let firstValue = MapInputs.Copy "uno" + + let table = + shapedCreations[creation] [| struct (firstKey, firstValue); struct (MapInputs.Copy "one", MapInputs.Copy "uno") |] + + let struct (key, value) = + Assert.ContainsSingle (HashMap.toArray table, "The hash map must merge the bindings of the equal keys") + + Assert.AreSame (firstKey, key, "The hash map must keep the earlier key") + Assert.AreSame (firstValue, value, "The hash map must keep the earlier value") + + [] + member _.``the creation members resolve the shape of the bindings in a pipeline`` (source : string) = + CollectionAssert.AreEqual ( + [| "one", 1; "two", 2 |], + sortedBindings (pipelines[source]()), + "The pipeline must create the bindings of its source" + ) + + [] + member _.``ofSeq returns a hash map with the default comparers itself`` () = + let table = HashMap.ofArray [| "one", 1; "two", 2 |] + Assert.AreSame (table, HashMap.ofSeq table, "HashMap.ofSeq must return a hash map with the default comparers itself") + + [] + member _.``ofSeq gives a hash map with other comparers the default comparers`` () = + let ignoreCase = + (HashMap.forKeyComparer StringComparer.OrdinalIgnoreCase).ofArray [| "one", 1; "two", 2 |] + let table = HashMap.ofSeq ignoreCase + + Assert.AreSame>( + EqualityComparer.Default, + HashMap.keyComparer table, + "HashMap.ofSeq must give a hash map the default key comparer" + ) + + CollectionAssert.AreEqual (sortedBindings ignoreCase, sortedBindings table, "HashMap.ofSeq must keep the bindings") + Assert.IsFalse (HashMap.containsKey "ONE" table, "HashMap.ofSeq must look the keys up with the default comparer") diff --git a/tests/FSharp.Collections.Immutable.Tests/HashMap/HashMapOps.fs b/tests/FSharp.Collections.Immutable.Tests/HashMap/HashMapOps.fs new file mode 100644 index 00000000..b19b97ff --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/HashMap/HashMapOps.fs @@ -0,0 +1,133 @@ +/// The functions of the `HashMap` module for the contract tests. +[] +module FSharp.Collections.Immutable.Tests.HashMapOps + +open System.Collections.Generic +open FSharp.Collections.Immutable + +/// An equality comparer of keys that compares their absolute values, for the comparer struct. +let absoluteKeyEquality = { + new IEqualityComparer with + member _.Equals (left, right) = abs left = abs right + member _.GetHashCode key = abs key +} + +/// The functions of the reading facet. `HashMap` names its map parameter `table`, as the `Map` module does. +let reading : MapOps> = { + Name = nameof HashMap + ParamName = id + OfSeq = HashMap.ofSeq + ToArray = HashMap.toArray + + Count = HashMap.count + IsEmpty = HashMap.isEmpty + Keys = fun table -> HashMap.keys table + Values = fun table -> HashMap.values table + + Find = HashMap.find + TryFind = HashMap.tryFind + + ContainsBinding = HashMap.containsBinding + ContainsKey = HashMap.containsKey + Exists = HashMap.exists + FindKey = HashMap.findKey + Forall = HashMap.forall + Pick = HashMap.pick + TryFindKey = HashMap.tryFindKey + TryPick = HashMap.tryPick + + Fold = HashMap.fold + FoldBack = HashMap.foldBack + Iter = HashMap.iter + + ToList = HashMap.toList + ToSeq = HashMap.toSeq +} + +/// The functions of the transformation facet. +let transform : MapTransformOps> = { + Name = nameof HashMap + ParamName = id + OfSeq = HashMap.ofSeq + ToArray = HashMap.toArray + + Choose = fun chooser table -> HashMap.choose chooser table |> HashMap.toArray + Filter = HashMap.filter + Map = fun mapping table -> HashMap.map mapping table |> HashMap.toArray + Partition = HashMap.partition +} + +/// The functions of the mutation facet: `addRange` and `setItems` of the module over key/value pairs, and of its nested +/// modules over tuples (ADR 0001, D13). +let mutation : MapMutationOps> = { + Name = nameof HashMap + ParamName = id + OfSeq = HashMap.ofSeq + ToArray = HashMap.toArray + + Add = HashMap.add + Change = HashMap.change + Clear = HashMap.clear + Remove = HashMap.remove + RemoveRange = HashMap.removeRange + SetItem = HashMap.setItem + + KeyValuePairs = { AddRange = HashMap.addRange; SetItems = HashMap.setItems } + Tuples = { AddRange = HashMap.Tuple.addRange; SetItems = HashMap.Tuple.setItems } + StructTuples = { + AddRange = HashMap.ValueTuple.addRange + SetItems = HashMap.ValueTuple.setItems + } +} + +/// The functions of the creation facet with the default comparers: the functions of the module and the overloads of the +/// extension members, one shape per field (ADR 0001, D13). +let creation : MapCreationOps> = { + Name = nameof HashMap + ParamName = id + OfSeq = HashMap.ofSeq + ToArray = HashMap.toArray + ContainsKey = HashMap.containsKey + EquivalentKey = id + + Empty = fun () -> HashMap.empty + KeyValuePairs = { OfArray = HashMap.ofArray; OfList = HashMap.ofList; OfSeq = HashMap.ofSeq } + Tuples = { OfArray = HashMap.ofArray; OfList = HashMap.ofList; OfSeq = HashMap.ofSeq } + StructTuples = { OfArray = HashMap.ofArray; OfList = HashMap.ofList; OfSeq = HashMap.ofSeq } + OfSeqBy = HashMap.ofSeqBy + Singleton = HashMap.singleton +} + +/// The comparer struct whose key comparer compares the absolute values of the keys. +let private absoluteKeys : CustomComparersHashMap = + HashMap.forKeyComparer absoluteKeyEquality + +/// The functions of the creation facet through the comparer struct with a key comparer of absolute values (ADR 0001, D12): +/// every created map finds a key by its negation. +let customComparersCreation : MapCreationOps> = { + Name = nameof CustomComparersHashMap + ParamName = id + OfSeq = HashMap.ofSeq + ToArray = HashMap.toArray + ContainsKey = HashMap.containsKey + EquivalentKey = fun key -> -key + + Empty = fun () -> absoluteKeys.empty + KeyValuePairs = { + OfArray = absoluteKeys.ofArray + OfList = absoluteKeys.ofList + OfSeq = absoluteKeys.ofSeq + } + Tuples = { + OfArray = absoluteKeys.ofArray + OfList = absoluteKeys.ofList + OfSeq = absoluteKeys.ofSeq + } + StructTuples = { + OfArray = absoluteKeys.ofArray + OfList = absoluteKeys.ofList + OfSeq = absoluteKeys.ofSeq + } + OfSeqBy = absoluteKeys.ofSeqBy + Singleton = absoluteKeys.singleton +} diff --git a/tests/FSharp.Collections.Immutable.Tests/HashMap/HashMapOptionTests.fs b/tests/FSharp.Collections.Immutable.Tests/HashMap/HashMapOptionTests.fs new file mode 100644 index 00000000..eb275290 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/HashMap/HashMapOptionTests.fs @@ -0,0 +1,145 @@ +namespace FSharp.Collections.Immutable.Tests + +open System +open FSharp.Collections.Immutable +open FSharp.Collections.Immutable.Tuple +open FSharp.Collections.Immutable.Option +open Microsoft.VisualStudio.TestTools.UnitTesting + +// The namespaces are opened in the order that gives the shapes of FSharp.Core: Tuple first, Option last. +// The type annotations of the results are the proof that the functions of the Option namespace shadow the default ones. + +/// The `HashMap` module of the `Option` namespace. +[] +type HashMapOptionTests () = + + static let table = FSharp.Collections.Immutable.HashMap.ofSeq MapInputs.Several + + static let oracle = MapInputs.ToMap MapInputs.Several + + /// A chooser that returns a value for the bindings of negative keys only. + static let negatedIfNegative (key : int) (value : string) = if key < 0 then Some (value.Length - key) else None + + /// The predicates of tryFindKey: one that some bindings satisfy and one that no binding satisfies. + static member Predicates = seq { + BindingPredicate.HasNegativeKey + BindingPredicate.Never + } + + [] + member _.``tryFind returns an option`` (key : int) = + let found : string option = HashMap.tryFind key table + + OverrideAssert.OptionMatches ( + FSharp.Collections.Immutable.HashMap.tryFind key table, + found, + "Option.HashMap.tryFind must match the default" + ) + + [] + member _.``tryGetKey returns an option`` (key : int) = + let found : int option = HashMap.tryGetKey key table + + OverrideAssert.OptionMatches ( + FSharp.Collections.Immutable.HashMap.tryGetKey key table, + found, + "Option.HashMap.tryGetKey must match the default" + ) + + [] + member _.``pick takes a chooser of options`` () = + let picked : int = HashMap.pick negatedIfNegative table + + Assert.AreEqual( + FSharp.Collections.Immutable.HashMap.pick (fun key value -> negatedIfNegative key value |> ValueOption.ofOption) table, + picked, + "Option.HashMap.pick must pick as the default does" + ) + + [] + member _.``tryFindKey returns an option`` (predicate : BindingPredicate) = + let found : int option = HashMap.tryFindKey predicate.Matches table + + OverrideAssert.OptionMatches ( + FSharp.Collections.Immutable.HashMap.tryFindKey predicate.Matches table, + found, + "Option.HashMap.tryFindKey must match the default" + ) + + [] + member _.``tryPick takes a chooser of options and returns an option`` () = + let found : int option = HashMap.tryPick negatedIfNegative table + + OverrideAssert.OptionMatches ( + FSharp.Collections.Immutable.HashMap.tryPick + (fun key value -> negatedIfNegative key value |> ValueOption.ofOption) + table, + found, + "Option.HashMap.tryPick must match the default" + ) + + let nothing : int option = HashMap.tryPick (fun _ _ -> None) table + Assert.AreEqual(None, nothing, "Option.HashMap.tryPick must return None when the chooser returns nothing") + + [] + member _.``change takes a function of options as Map.change does`` (key : int) = + let extend (current : string option) = current |> Option.map (fun value -> value + "!") + let changed : HashMap = HashMap.change key extend table + + CollectionAssert.AreEqual ( + MapInputs.OfMap (Map.change key extend oracle), + MapInputs.SortByKey (FSharp.Collections.Immutable.HashMap.toArray changed), + "Option.HashMap.change must change as Map.change does" + ) + + [] + member _.``change leaves the binding as it is for a value that the value comparer finds equal`` () = + // The function calls HashMap.change, which keeps the binding, its key included, for an equal value (ADR 0001, D14) + let comparers = + FSharp.Collections.Immutable.HashMap.forComparers StringComparer.OrdinalIgnoreCase StringComparer.OrdinalIgnoreCase + + let ignoreCase = comparers.ofArray [| "one", "uno" |] + let changed : HashMap = HashMap.change "ONE" (fun _ -> Some "UNO") ignoreCase + Assert.AreEqual( + Some "uno", + HashMap.tryFind "one" changed, + "Option.HashMap.change must keep the current value" + ) + Assert.AreEqual( + Some "one", + HashMap.tryGetKey "ONE" changed, + "Option.HashMap.change must keep the current key" + ) + Assert.AreSame (ignoreCase, changed, "Option.HashMap.change must return the input itself") + + [] + member _.``choose takes a chooser of options`` () = + let chosen : HashMap = HashMap.choose negatedIfNegative table + + CollectionAssert.AreEqual ( + oracle + |> Map.toArray + |> Array.choose (fun (key, value) -> + negatedIfNegative key value + |> Option.map (fun chosen -> struct (key, chosen)) + ), + MapInputs.SortByKey (FSharp.Collections.Immutable.HashMap.toArray chosen), + "Option.HashMap.choose must keep the chosen values" + ) + + [] + member _.``the functions without an option keep resolving to the other modules`` () = + let mapped : HashMap = HashMap.map (fun _ (value : string) -> value.Length) table + let pairs : (int * string) list = HashMap.toList table + + Assert.AreEqual( + MapInputs.Several.Length, + FSharp.Collections.Immutable.HashMap.count mapped, + "HashMap.map must resolve to the default module" + ) + + CollectionAssert.AreEqual ( + Map.toArray oracle, + List.toArray pairs |> Array.sortBy fst, + "HashMap.toList must resolve to the Tuple module" + ) diff --git a/tests/FSharp.Collections.Immutable.Tests/HashMap/HashMapTests.fs b/tests/FSharp.Collections.Immutable.Tests/HashMap/HashMapTests.fs new file mode 100644 index 00000000..1de11fbe --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/HashMap/HashMapTests.fs @@ -0,0 +1,807 @@ +namespace FSharp.Collections.Immutable.Tests + +open System +open System.Collections.Generic +open FSharp.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// +/// The functions that create a hash map from a span of key/value pairs, as the cases of the rows of their tests. +/// +/// +/// A span is a byref-like value, so it cannot be a value of a row or be captured by a closure. A case builds the span of an +/// array inside its own member, for the module function and for the member of the comparer struct. +/// +[] +type SpanCreation = + /// ofReadOnlySpan, which takes a ReadOnlySpan. + | OfReadOnlySpan + /// ofSpan, which takes a Span. + | OfSpan + + /// Every function, as the rows of the tests that run once per function. + static member All = seq { + SpanCreation.OfReadOnlySpan + SpanCreation.OfSpan + } + + /// The name of the function, for the messages of the assertions. + member this.Name = + match this with + | SpanCreation.OfReadOnlySpan -> nameof HashMap.ofReadOnlySpan + | SpanCreation.OfSpan -> nameof HashMap.ofSpan + + /// Creates a hash map with the default comparers from the span of an array. + member this.Create<'Key, 'Value when 'Key : not null> (pairs : KeyValuePair<'Key, 'Value> array) : HashMap<'Key, 'Value> = + match this with + | SpanCreation.OfReadOnlySpan -> HashMap.ofReadOnlySpan (ReadOnlySpan pairs) + | SpanCreation.OfSpan -> HashMap.ofSpan (Span pairs) + + /// Creates a hash map with the default comparers from an array, which F# converts to the span because the types of the + /// call are known; F# reports the conversion with FS3391, which is what the tests are about. + member this.CreateFromArray (pairs : KeyValuePair array) : HashMap = + #nowarn "3391" + match this with + | SpanCreation.OfReadOnlySpan -> HashMap.ofReadOnlySpan pairs + | SpanCreation.OfSpan -> HashMap.ofSpan pairs + #warnon "3391" + + /// Creates a hash map with the given comparers from the span of an array, with the member of the comparer struct. + member this.CreateWith<'Key, 'Value when 'Key : not null> + (comparers : CustomComparersHashMap<'Key, 'Value>, pairs : KeyValuePair<'Key, 'Value> array) + : HashMap<'Key, 'Value> + = + match this with + | SpanCreation.OfReadOnlySpan -> comparers.ofReadOnlySpan (ReadOnlySpan pairs) + | SpanCreation.OfSpan -> comparers.ofSpan (Span pairs) + + /// Creates a hash map with the given comparers from an array, which F# converts to the span because the types of the + /// struct are known. + member this.CreateFromArrayWith + (comparers : CustomComparersHashMap, pairs : KeyValuePair array) + : HashMap + = + match this with + | SpanCreation.OfReadOnlySpan -> comparers.ofReadOnlySpan pairs + | SpanCreation.OfSpan -> comparers.ofSpan pairs + +/// The behaviour of `HashMap` that the shared contracts do not cover: null inputs, comparers, shared results and the +/// semantics of the functions that wrap the BCL. +[] +type HashMapTests () = + + static let nullTable = Unchecked.defaultof> + + /// A value comparer that differs from the default one: it compares strings by their length. + static let byLength = + HashIdentity.FromFunctions (fun (value : string) -> value.Length) (fun left right -> left.Length = right.Length) + + /// A map whose keys ignore the case and whose values compare by their length. + static let customTable () = + (HashMap.forComparers StringComparer.OrdinalIgnoreCase byLength).ofArray [| "one", "uno"; "Two", "dos"; "three", "tres" |] + + /// A map with the default comparers. + static let defaultTable () = HashMap.ofArray [| struct ("one", "uno"); struct ("two", "dos") |] + + /// The functions that take a map, by name, on a null map. The rows of the tests are the names: a function has no + /// readable name in a row. + static let nullTableCalls : Map unit> = + Map.ofArray [| + nameof HashMap.add, (fun () -> HashMap.add "a" "b" nullTable |> ignore) + nameof HashMap.addRange, + (fun () -> + HashMap.addRange [| KeyValuePair ("a", "b") |] nullTable + |> ignore + ) + nameof HashMap.change, (fun () -> HashMap.change "a" id nullTable |> ignore) + nameof HashMap.choose, + (fun () -> + HashMap.choose (fun _ value -> ValueSome value) nullTable + |> ignore + ) + nameof HashMap.clear, (fun () -> HashMap.clear nullTable |> ignore) + nameof HashMap.containsBinding, (fun () -> HashMap.containsBinding "a" "b" nullTable |> ignore) + nameof HashMap.containsKey, (fun () -> HashMap.containsKey "a" nullTable |> ignore) + nameof HashMap.containsValue, (fun () -> HashMap.containsValue "a" nullTable |> ignore) + nameof HashMap.count, (fun () -> HashMap.count nullTable |> ignore) + nameof HashMap.exists, (fun () -> HashMap.exists (fun _ _ -> true) nullTable |> ignore) + nameof HashMap.filter, (fun () -> HashMap.filter (fun _ _ -> true) nullTable |> ignore) + nameof HashMap.find, (fun () -> HashMap.find "a" nullTable |> ignore) + nameof HashMap.findKey, (fun () -> HashMap.findKey (fun _ _ -> true) nullTable |> ignore) + nameof HashMap.fold, (fun () -> HashMap.fold (fun state _ _ -> state) 0 nullTable |> ignore) + nameof HashMap.foldBack, + (fun () -> + HashMap.foldBack (fun _ _ state -> state) nullTable 0 + |> ignore + ) + nameof HashMap.forall, (fun () -> HashMap.forall (fun _ _ -> true) nullTable |> ignore) + nameof HashMap.isEmpty, (fun () -> HashMap.isEmpty nullTable |> ignore) + nameof HashMap.iter, (fun () -> HashMap.iter (fun _ _ -> ()) nullTable) + nameof HashMap.keyComparer, (fun () -> HashMap.keyComparer nullTable |> ignore) + nameof HashMap.keys, (fun () -> HashMap.keys nullTable |> ignore) + nameof HashMap.map, (fun () -> HashMap.map (fun _ value -> value) nullTable |> ignore) + nameof HashMap.partition, (fun () -> HashMap.partition (fun _ _ -> true) nullTable |> ignore) + nameof HashMap.pick, + (fun () -> + HashMap.pick (fun _ value -> ValueSome value) nullTable + |> ignore + ) + nameof HashMap.remove, (fun () -> HashMap.remove "a" nullTable |> ignore) + nameof HashMap.removeRange, (fun () -> HashMap.removeRange [| "a" |] nullTable |> ignore) + nameof HashMap.setItem, (fun () -> HashMap.setItem "a" "b" nullTable |> ignore) + nameof HashMap.setItems, + (fun () -> + HashMap.setItems [| KeyValuePair ("a", "b") |] nullTable + |> ignore + ) + nameof HashMap.toArray, (fun () -> HashMap.toArray nullTable |> ignore) + nameof HashMap.toBuilder, (fun () -> HashMap.toBuilder nullTable |> ignore) + nameof HashMap.toList, (fun () -> HashMap.toList nullTable |> ignore) + nameof HashMap.toSeq, (fun () -> HashMap.toSeq nullTable |> ignore) + nameof HashMap.tryFind, (fun () -> HashMap.tryFind "a" nullTable |> ignore) + nameof HashMap.tryFindKey, (fun () -> HashMap.tryFindKey (fun _ _ -> true) nullTable |> ignore) + nameof HashMap.tryGetKey, (fun () -> HashMap.tryGetKey "a" nullTable |> ignore) + nameof HashMap.tryPick, + (fun () -> + HashMap.tryPick (fun _ value -> ValueSome value) nullTable + |> ignore + ) + nameof HashMap.update, (fun () -> HashMap.update ignore nullTable |> ignore) + nameof HashMap.valueComparer, (fun () -> HashMap.valueComparer nullTable |> ignore) + nameof HashMap.values, (fun () -> HashMap.values nullTable |> ignore) + "Tuple.addRange", (fun () -> HashMap.Tuple.addRange [| "a", "b" |] nullTable |> ignore) + "Tuple.setItems", (fun () -> HashMap.Tuple.setItems [| "a", "b" |] nullTable |> ignore) + "ValueTuple.addRange", + (fun () -> + HashMap.ValueTuple.addRange [| struct ("a", "b") |] nullTable + |> ignore + ) + "ValueTuple.setItems", + (fun () -> + HashMap.ValueTuple.setItems [| struct ("a", "b") |] nullTable + |> ignore + ) + |] + + /// The functions that take a sequence, by name, with the name of the sequence parameter and the function on a null + /// sequence. + static let nullSequenceCalls : Map unit))> = + let table = defaultTable () + + Map.ofArray [| + nameof HashMap.addRange, + struct ("pairs", + fun () -> + HashMap.addRange (Unchecked.defaultof>>) table + |> ignore) + "ofArray of key/value pairs", + struct ("elements", + fun () -> + HashMap.ofArray (Unchecked.defaultof array>) + |> ignore) + "ofArray of reference tuples", + struct ("elements", + fun () -> + HashMap.ofArray (Unchecked.defaultof<(string * string) array>) + |> ignore) + "ofArray of struct tuples", + struct ("elements", + fun () -> + HashMap.ofArray (Unchecked.defaultof) + |> ignore) + "ofSeq of key/value pairs", + struct ("elements", + fun () -> + HashMap.ofSeq (Unchecked.defaultof>>) + |> ignore) + "ofSeq of reference tuples", + struct ("elements", + fun () -> + HashMap.ofSeq (Unchecked.defaultof>) + |> ignore) + "ofSeq of struct tuples", + struct ("elements", + fun () -> + HashMap.ofSeq (Unchecked.defaultof>) + |> ignore) + nameof HashMap.ofSeqBy, + struct ("source", + fun () -> + HashMap.ofSeqBy id (Unchecked.defaultof>) + |> ignore) + nameof HashMap.removeRange, + struct ("keys", + fun () -> + HashMap.removeRange (Unchecked.defaultof>) table + |> ignore) + nameof HashMap.setItems, + struct ("pairs", + fun () -> + HashMap.setItems (Unchecked.defaultof>>) table + |> ignore) + "Tuple.addRange", + struct ("pairs", + fun () -> + HashMap.Tuple.addRange (Unchecked.defaultof>) table + |> ignore) + "Tuple.setItems", + struct ("pairs", + fun () -> + HashMap.Tuple.setItems (Unchecked.defaultof>) table + |> ignore) + "ValueTuple.addRange", + struct ("pairs", + fun () -> + HashMap.ValueTuple.addRange (Unchecked.defaultof>) table + |> ignore) + "ValueTuple.setItems", + struct ("pairs", + fun () -> + HashMap.ValueTuple.setItems (Unchecked.defaultof>) table + |> ignore) + |] + + /// The functions that take a sequence, by name, with the name of the sequence parameter and the function on a default + /// flat list that is given as the sequence. A default flat list converts to a sequence without a check and fails with + /// InvalidOperationException when the sequence is enumerated, so the other inputs are valid and the function would + /// enumerate it. The last rows give a sequence of objects, which a default flat list of strings is by the covariance of + /// seq: the element type of the sequence is not the one of the flat list. + static let defaultSequenceCalls : Map unit))> = + let table = defaultTable () + let objectTable = HashMap.ofArray [| KeyValuePair("one", "uno") |] + let defaultPairs = Unchecked.defaultof>> :> seq<_> + let defaultTuples = Unchecked.defaultof> :> seq<_> + let defaultStructTuples = Unchecked.defaultof> :> seq<_> + let defaultKeys = Unchecked.defaultof> :> seq<_> + + Map.ofArray [| + nameof HashMap.addRange, struct ("pairs", fun () -> HashMap.addRange defaultPairs table |> ignore) + "ofSeq of key/value pairs", struct ("elements", fun () -> HashMap.ofSeq defaultPairs |> ignore) + "ofSeq of reference tuples", struct ("elements", fun () -> HashMap.ofSeq defaultTuples |> ignore) + "ofSeq of struct tuples", struct ("elements", fun () -> HashMap.ofSeq defaultStructTuples |> ignore) + nameof HashMap.ofSeqBy, struct ("source", fun () -> HashMap.ofSeqBy id defaultKeys |> ignore) + nameof HashMap.removeRange, struct ("keys", fun () -> HashMap.removeRange defaultKeys table |> ignore) + nameof HashMap.setItems, struct ("pairs", fun () -> HashMap.setItems defaultPairs table |> ignore) + "Tuple.addRange", struct ("pairs", fun () -> HashMap.Tuple.addRange defaultTuples table |> ignore) + "Tuple.setItems", struct ("pairs", fun () -> HashMap.Tuple.setItems defaultTuples table |> ignore) + "ValueTuple.addRange", + struct ("pairs", + fun () -> + HashMap.ValueTuple.addRange defaultStructTuples table + |> ignore) + "ValueTuple.setItems", + struct ("pairs", + fun () -> + HashMap.ValueTuple.setItems defaultStructTuples table + |> ignore) + $"%s{nameof HashMap.ofSeqBy} (default flat list of strings as objects)", + struct ("source", + fun () -> + HashMap.ofSeqBy id FlatListOracles.defaultStringsAsObjects + |> ignore) + $"%s{nameof HashMap.removeRange} (default flat list of strings as objects)", + struct ("keys", + fun () -> + HashMap.removeRange FlatListOracles.defaultStringsAsObjects objectTable + |> ignore) + |] + + /// The creation functions without comparers, by name and shape of the bindings. + static let defaultCreations : Map HashMap> = + Map.ofArray [| + nameof HashMap.empty, (fun () -> HashMap.empty) + "ofArray of key/value pairs", (fun () -> HashMap.ofArray [| KeyValuePair ("a", "b") |]) + "ofArray of reference tuples", (fun () -> HashMap.ofArray [| ("a", "b") |]) + "ofArray of struct tuples", (fun () -> HashMap.ofArray [| struct ("a", "b") |]) + "ofList of key/value pairs", (fun () -> HashMap.ofList [ KeyValuePair ("a", "b") ]) + "ofList of reference tuples", (fun () -> HashMap.ofList [ ("a", "b") ]) + "ofList of struct tuples", (fun () -> HashMap.ofList [ struct ("a", "b") ]) + "ofSeq of key/value pairs", (fun () -> HashMap.ofSeq (dict [| "a", "b" |])) + "ofSeq of reference tuples", (fun () -> HashMap.ofSeq (Seq.singleton ("a", "b"))) + "ofSeq of struct tuples", (fun () -> HashMap.ofSeq (Seq.singleton (struct ("a", "b")))) + nameof HashMap.build, (fun () -> HashMap.build (fun builder -> builder["a"] <- "b")) + nameof HashMap.ofReadOnlySpan, (fun () -> HashMap.ofReadOnlySpan (ReadOnlySpan [| KeyValuePair ("a", "b") |])) + nameof HashMap.ofSeqBy, (fun () -> HashMap.ofSeqBy id [| "a" |]) + nameof HashMap.ofSpan, (fun () -> HashMap.ofSpan (Span [| KeyValuePair ("a", "b") |])) + nameof HashMap.singleton, (fun () -> HashMap.singleton "a" "b") + |] + + /// The transformations that keep the type of the values, by name, on the map whose keys ignore the case. + static let sameTypeTransformations : Map -> HashMap> = + Map.ofArray [| + nameof HashMap.add, HashMap.add "four" "cuatro" + nameof HashMap.addRange, HashMap.addRange [| KeyValuePair ("four", "cuatro") |] + nameof HashMap.change, HashMap.change "one" (fun _ -> ValueNone) + nameof HashMap.clear, HashMap.clear + nameof HashMap.filter, HashMap.filter (fun (key : string) _ -> key.Length > 3) + ($"%s{nameof HashMap.partition} (accepted)", + fun table -> + let struct (accepted, _) = HashMap.partition (fun (key : string) _ -> key.Length > 3) table + accepted) + ($"%s{nameof HashMap.partition} (rejected)", + fun table -> + let struct (_, rejected) = HashMap.partition (fun (key : string) _ -> key.Length > 3) table + rejected) + nameof HashMap.remove, HashMap.remove "one" + nameof HashMap.removeRange, HashMap.removeRange [| "one"; "two" |] + nameof HashMap.setItem, HashMap.setItem "four" "cuatro" + nameof HashMap.setItems, HashMap.setItems [| KeyValuePair ("four", "cuatro") |] + nameof HashMap.update, HashMap.update (fun (builder : HashMap.Builder) -> builder["four"] <- "cuatro") + "Tuple.addRange", HashMap.Tuple.addRange [| "four", "cuatro" |] + "Tuple.setItems", HashMap.Tuple.setItems [| "four", "cuatro" |] + "ValueTuple.addRange", HashMap.ValueTuple.addRange [| struct ("four", "cuatro") |] + "ValueTuple.setItems", HashMap.ValueTuple.setItems [| struct ("four", "cuatro") |] + |] + + /// The transformations that change the type of the values, by name, on the map whose keys ignore the case. + static let typeChangingTransformations : Map -> HashMap> = + Map.ofArray [| + nameof HashMap.choose, HashMap.choose (fun _ (value : string) -> ValueSome value.Length) + nameof HashMap.map, HashMap.map (fun _ (value : string) -> value.Length) + |] + + /// The key comparer of the result of every transformation, by name. + static let resultKeyComparers : Map -> IEqualityComparer> = + Map.ofSeq ( + seq { + for KeyValue (name, transform) in sameTypeTransformations do + name, transform >> HashMap.keyComparer + + for KeyValue (name, transform) in typeChangingTransformations do + name, transform >> HashMap.keyComparer + } + ) + + /// The functions that change nothing in the map with the default comparers, by name. + static let unchangingCalls : Map -> HashMap> = + Map.ofArray [| + nameof HashMap.add, HashMap.add "one" "uno" + nameof HashMap.addRange, HashMap.addRange [| KeyValuePair ("one", "uno") |] + $"%s{nameof HashMap.change} of a key with a binding", HashMap.change "one" id + $"%s{nameof HashMap.change} of a key without a binding", HashMap.change "three" id + nameof HashMap.filter, HashMap.filter (fun _ _ -> true) + ($"%s{nameof HashMap.partition} (accepted)", + fun table -> + let struct (accepted, _) = HashMap.partition (fun _ _ -> true) table + accepted) + nameof HashMap.remove, HashMap.remove "three" + nameof HashMap.removeRange, HashMap.removeRange [| "three" |] + nameof HashMap.setItem, HashMap.setItem "two" "dos" + nameof HashMap.setItems, HashMap.setItems [| KeyValuePair ("two", "dos") |] + nameof HashMap.update, HashMap.update ignore + "Tuple.addRange", HashMap.Tuple.addRange [| "one", "uno" |] + "Tuple.setItems", HashMap.Tuple.setItems [| "two", "dos" |] + "ValueTuple.addRange", HashMap.ValueTuple.addRange [| struct ("one", "uno") |] + "ValueTuple.setItems", HashMap.ValueTuple.setItems [| struct ("two", "dos") |] + |] + + /// The functions that bind "ONE" to "abc" in the map whose keys ignore the case and whose values compare by their + /// length, by name: the map binds "one" to "uno", a value that the value comparer finds equal to "abc". + static let equalValueCalls : Map -> HashMap> = + Map.ofArray [| + nameof HashMap.add, HashMap.add "ONE" "abc" + nameof HashMap.addRange, HashMap.addRange [| KeyValuePair ("ONE", "abc") |] + nameof HashMap.change, HashMap.change "ONE" (fun _ -> ValueSome "abc") + nameof HashMap.setItem, HashMap.setItem "ONE" "abc" + nameof HashMap.setItems, HashMap.setItems [| KeyValuePair ("ONE", "abc") |] + "Tuple.addRange", HashMap.Tuple.addRange [| "ONE", "abc" |] + "Tuple.setItems", HashMap.Tuple.setItems [| "ONE", "abc" |] + "ValueTuple.addRange", HashMap.ValueTuple.addRange [| struct ("ONE", "abc") |] + "ValueTuple.setItems", HashMap.ValueTuple.setItems [| struct ("ONE", "abc") |] + |] + + /// The names of the functions that take a map, as the rows of their test. + static member NullTableCalls = nullTableCalls.Keys + + /// The names of the functions that take a sequence, as the rows of their test. + static member NullSequenceCalls = nullSequenceCalls.Keys + + /// The names of the functions that take a sequence and are given a default flat list, as the rows of their test. + static member DefaultSequenceCalls = defaultSequenceCalls.Keys + + /// The names of the creation functions without comparers, as the rows of their test. + static member DefaultCreations = defaultCreations.Keys + + /// The names of the transformations that keep the type of the values, as the rows of their tests. + static member SameTypeTransformations = sameTypeTransformations.Keys + + /// The names of the transformations that change the type of the values, as the rows of their test. + static member TypeChangingTransformations = typeChangingTransformations.Keys + + /// The names of every transformation, as the rows of the test of the key comparer. + static member Transformations = resultKeyComparers.Keys + + /// The names of the functions that change nothing, as the rows of their test. + static member UnchangingCalls = unchangingCalls.Keys + + /// The names of the functions that set a value that the value comparer finds equal, as the rows of their test. + static member EqualValueCalls = equalValueCalls.Keys + + /// Every input of the creation tests, the bindings that repeat keys included: the inputs of the span functions. + static member SpanInputs = seq { + yield! MapInputs.All + MapInputs.RepeatedKeys + } + + /// Every function that creates a hash map from a span with every input of the creation tests: the rows of the test of + /// the bindings. + static member SpanCreationsAndInputs = seq { + for creation in SpanCreation.All do + for input in HashMapTests.SpanInputs do + struct (creation, input) + } + + [] + member _.``functions reject a null map`` (name : string) = + // Map throws NullReferenceException for a null map; this library checks its arguments (ADR 0001, D6) + let ex = + Assert.ThrowsExactly(Action nullTableCalls[name], $"HashMap.%s{name} must reject a null map") + + Assert.AreEqual("table", ex.ParamName, $"HashMap.%s{name} must name the map parameter") + + [] + member _.``functions reject a null sequence`` (name : string) = + let struct (paramName, call) = nullSequenceCalls[name] + + let ex = + Assert.ThrowsExactly(Action call, $"HashMap.%s{name} must reject a null sequence") + + Assert.AreEqual(paramName, ex.ParamName, $"HashMap.%s{name} must name the sequence parameter") + + [] + member _.``functions reject a default flat list given as a sequence`` (name : string) = + // The exception names the parameter of the function, not the one of the BCL member that it would have called + let struct (paramName, call) = defaultSequenceCalls[name] + FlatListAssert.RejectsNull ($"HashMap.%s{name}", paramName, call) + + [] + member _.``a flat list that is not default passes as a sequence, also one of strings as a sequence of objects`` () = + let flatList (elements : 'T array) = FlatList.ofArray elements :> seq<'T> + let table = defaultTable () + + let bindingsOf (map : HashMap) = + HashMap.toArray map + |> Array.sortBy (fun struct (key, _) -> key) + + let expected = [| struct ("four", "cuatro"); struct ("one", "uno"); struct ("two", "dos") |] + + CollectionAssert.AreEqual ( + expected, + bindingsOf (HashMap.addRange (flatList [| KeyValuePair ("four", "cuatro") |]) table), + "HashMap.addRange must take the elements of a flat list" + ) + + CollectionAssert.AreEqual ( + [| struct ("one", "uno"); struct ("two", "dos"); struct ("zero", "cero") |], + bindingsOf (HashMap.setItems (flatList [| KeyValuePair ("zero", "cero") |]) table), + "HashMap.setItems must take the elements of a flat list" + ) + + CollectionAssert.AreEqual ( + [| struct ("two", "dos") |], + bindingsOf (HashMap.removeRange (flatList [| "one" |]) table), + "HashMap.removeRange must take the elements of a flat list" + ) + + CollectionAssert.AreEqual ( + [| struct ("a", "b") |], + bindingsOf (HashMap.ofSeq (flatList [| KeyValuePair ("a", "b") |])), + "HashMap.ofSeq must take the key/value pairs of a flat list" + ) + + CollectionAssert.AreEqual ( + [| struct ("a", "b") |], + bindingsOf (HashMap.ofSeq (flatList [| ("a", "b") |])), + "HashMap.ofSeq must take the reference tuples of a flat list" + ) + + CollectionAssert.AreEqual ( + [| struct ("a", "b") |], + bindingsOf (HashMap.ofSeq (flatList [| struct ("a", "b") |])), + "HashMap.ofSeq must take the struct tuples of a flat list" + ) + + CollectionAssert.AreEqual ( + [| struct ("ab", "ab") |], + bindingsOf (HashMap.ofSeqBy id (flatList [| "ab" |])), + "HashMap.ofSeqBy must take the elements of a flat list" + ) + + // A flat list of strings is a sequence of objects, which a type test for the flat list of objects does not see + let widened = FlatListOracles.stringsAsObjects (FlatList.ofArray [| "one" |]) + let objectTable = + HashMap.ofArray [| KeyValuePair("one", "uno"); KeyValuePair("two", "dos") |] + + CollectionAssert.AreEqual ( + [| "two" :> obj |], + HashMap.keys (HashMap.removeRange widened objectTable) + |> Seq.toArray, + "HashMap.removeRange must take the elements of a flat list of strings as objects" + ) + + CollectionAssert.AreEqual ( + [| "one" :> obj |], + HashMap.keys (HashMap.ofSeqBy id widened) |> Seq.toArray, + "HashMap.ofSeqBy must take the elements of a flat list of strings as objects" + ) + + [] + member _.``find throws the exception of Map.find, not the one of the indexer`` () = + let table = defaultTable () + + let ex = + Assert.ThrowsExactly( + Action (fun () -> HashMap.find "three" table |> ignore), + "HashMap.find must reject a key without a binding" + ) + + Assert.AreEqual( + KeyNotFoundException().Message, + ex.Message, + "HashMap.find must throw with the message of Map.find, which does not name the key" + ) + + [] + member _.``the creation functions without comparers use the default comparers`` (name : string) = + let table = defaultCreations[name]() + + Assert.AreSame>( + EqualityComparer.Default, + HashMap.keyComparer table, + $"HashMap.%s{name} must compare the keys with the default comparer" + ) + + Assert.AreSame>( + EqualityComparer.Default, + HashMap.valueComparer table, + $"HashMap.%s{name} must compare the values with the default comparer" + ) + + [] + member _.``ofSeqBy rejects a repeated key with another value and accepts a repeated element, as the BCL does`` () = + Assert.ThrowsExactly( + Action (fun () -> + HashMap.ofSeqBy (fun (value : int) -> string (value % 2)) [| 1; 3 |] + |> ignore + ), + "HashMap.ofSeqBy must reject a repeated key with another value, as ImmutableDictionary.ToImmutableDictionary does" + ) + |> ignore + + let table = HashMap.ofSeqBy (fun (value : int) -> string value) [| 1; 1 |] + Assert.AreEqual(1, HashMap.count table, "HashMap.ofSeqBy must keep one binding of a repeated element") + + [] + member _.``ofSeqBy keeps the first of the elements of a key that the default comparer finds equal`` () = + // ImmutableDictionary.ToImmutableDictionary keeps the first binding of a key for an equal value (ADR 0001, D14) + let first = MapInputs.Copy "uno" + let table = HashMap.ofSeqBy (fun (value : string) -> value.Length) [| first; MapInputs.Copy "uno" |] + + Assert.AreSame ( + first, + HashMap.find 3 table, + "HashMap.ofSeqBy must keep the first of the elements that the default comparer finds equal" + ) + + [] + member _.``the transformations keep the key comparer`` (name : string) = + Assert.AreSame>( + StringComparer.OrdinalIgnoreCase, + resultKeyComparers[name](customTable ()), + $"HashMap.%s{name} must keep the key comparer" + ) + + [] + member _.``the transformations that keep the type of the values keep the value comparer`` (name : string) = + Assert.AreSame ( + byLength, + HashMap.valueComparer (sameTypeTransformations[name](customTable ())), + $"HashMap.%s{name} must keep the value comparer" + ) + + [] + member _.``the transformations that change the type of the values give the new values the default comparer`` (name : string) = + Assert.AreSame>( + EqualityComparer.Default, + HashMap.valueComparer (typeChangingTransformations[name](customTable ())), + $"HashMap.%s{name} must compare the new values with the default comparer" + ) + + [] + member _.``containsValue compares with the value comparer`` () = + let table = customTable () + Assert.IsTrue (HashMap.containsValue "abc" table, "HashMap.containsValue must find a value of the same length") + Assert.IsTrue (HashMap.containsValue "tres" table, "HashMap.containsValue must find an equal value") + Assert.IsFalse (HashMap.containsValue "abcdef" table, "HashMap.containsValue must not find a value of another length") + + [] + member _.``tryGetKey returns the key that the map holds`` () = + let table = customTable () + Assert.AreEqual( + ValueSome "Two", + HashMap.tryGetKey "TWO" table, + "HashMap.tryGetKey must return the stored key" + ) + + Assert.AreEqual( + ValueNone, + HashMap.tryGetKey "four" table, + "HashMap.tryGetKey must return nothing for a missing key" + ) + + [] + member _.``keys and values are read-only views that ask the map`` () = + let table = customTable () + let keys = HashMap.keys table + Assert.HasCount (3, keys, "HashMap.keys must have a key per binding") + Assert.IsTrue (keys.Contains "THREE", "HashMap.keys must look the keys up with the key comparer") + Assert.IsTrue (keys.IsReadOnly, "HashMap.keys must be read-only") + + Assert.ThrowsExactly(Action (fun () -> keys.Add "four"), "HashMap.keys must reject a change") + |> ignore + + let values = HashMap.values table + Assert.HasCount (3, values, "HashMap.values must have a value per binding") + Assert.IsTrue (values.Contains "xyz", "HashMap.values must look the values up with the value comparer") + Assert.IsTrue (values.IsReadOnly, "HashMap.values must be read-only") + + [] + member _.``the functions that change nothing return the input itself`` (name : string) = + let table = defaultTable () + Assert.AreSame ( + table, + unchangingCalls[name] table, + $"HashMap.%s{name} must return the input itself when it changes nothing" + ) + + [] + member _.``a value that the value comparer finds equal leaves the binding as it is`` (name : string) = + // ImmutableDictionary.SetItem and AddRange keep the binding, its key included, for an equal value (ADR 0001, D14) + let table = customTable () + let result = equalValueCalls[name] table + Assert.AreEqual("uno", HashMap.find "one" result, $"HashMap.%s{name} must keep the current value") + + Assert.AreEqual( + ValueSome "one", + HashMap.tryGetKey "ONE" result, + $"HashMap.%s{name} must keep the current key" + ) + + Assert.AreSame (table, result, $"HashMap.%s{name} must return the input itself") + + [] + member _.``clear returns an empty map itself`` () = + let empty = HashMap.clear (defaultTable ()) + Assert.AreSame (empty, HashMap.clear empty, "HashMap.clear must return an empty input itself") + + [] + member _.``add throws what ImmutableDictionary.Add throws for a key that is bound to another value`` () = + let table = defaultTable () + + ExceptionAssert.Same ( + id, + (fun () -> table.Add ("one", "other") |> ignore), + (fun () -> HashMap.add "one" "other" table |> ignore), + "HashMap.add must throw what ImmutableDictionary.Add throws" + ) + + [] + member _.``add rejects a value that the value comparer finds different`` () = + // The comparer finds "uno" and "longer" different, as it finds "uno" and "abc" equal + Assert.ThrowsExactly( + Action (fun () -> HashMap.add "ONE" "longer" (customTable ()) |> ignore), + "HashMap.add must reject a value that the value comparer finds different from the current one" + ) + |> ignore + + [] + member _.``setItem replaces the value of a key that add rejects`` () = + let table = defaultTable () + let replaced = HashMap.setItem "one" "other" table + Assert.AreEqual("other", HashMap.find "one" replaced, "HashMap.setItem must replace the value of the key") + Assert.AreEqual("uno", HashMap.find "one" table, "HashMap.setItem must not change the input") + + [] + member _.``containsBinding compares the value with the value comparer where tryFind and equality do not`` () = + let table = + (HashMap.forComparers StringComparer.Ordinal StringComparer.OrdinalIgnoreCase).ofArray [| "one", "Uno" |] + + Assert.IsTrue ( + HashMap.containsBinding "one" "UNO" table, + "HashMap.containsBinding must find a value that the value comparer finds equal" + ) + + Assert.IsFalse ( + HashMap.tryFind "one" table + |> ValueOption.exists (fun stored -> stored = "UNO"), + "F# equality finds \"Uno\" and \"UNO\" different, which is why the function asks the value comparer" + ) + + Assert.IsFalse (HashMap.containsBinding "one" "DOS" table, "HashMap.containsBinding must not find another value") + Assert.IsFalse ( + HashMap.containsBinding "ONE" "Uno" table, + "HashMap.containsBinding must look the key up with the key comparer" + ) + Assert.IsFalse ( + HashMap.containsBinding "two" "Uno" table, + "HashMap.containsBinding must not find a key without a binding" + ) + + [] + member _.``containsBinding finds the key in the key comparer and the value in the value comparer`` + (key : string, value : string, expected : bool) + = + // The keys ignore the case and the values compare by their length: "one" is bound to "uno" + Assert.AreEqual( + expected, + HashMap.containsBinding key value (customTable ()), + "HashMap.containsBinding must compare the key and the value with the comparers of the map" + ) + + [] + member _.``containsBinding leaves a null key to the key comparer as ImmutableDictionary.Contains does`` () = + let nullKey = Unchecked.defaultof + + Assert.IsFalse ( + HashMap.containsBinding nullKey "uno" (defaultTable ()), + "HashMap.containsBinding must give false for a null key with the default key comparer" + ) + + Assert.ThrowsExactly( + Action (fun () -> HashMap.containsKey nullKey (defaultTable ()) |> ignore), + "HashMap.containsKey must reject a null key, which HashMap.containsBinding leaves to the key comparer" + ) + |> ignore + + Assert.ThrowsExactly( + Action (fun () -> + HashMap.containsBinding nullKey "uno" (customTable ()) + |> ignore + ), + "HashMap.containsBinding must throw for a null key when the key comparer rejects it" + ) + |> ignore + + [] + member _.``the span functions create the bindings of a span as Map.ofSeq does`` + (creation : SpanCreation, input : struct (int * string) array) + = + let table = creation.Create (Array.map MapInputs.ToKeyValuePair input) + + CollectionAssert.AreEqual ( + MapInputs.OfMap (MapInputs.ToMap input), + MapInputs.SortByKey (HashMap.toArray table), + $"HashMap.%s{creation.Name} must keep the last binding of each key, as Map.ofSeq does" + ) + + [)>] + member _.``the span functions copy the span`` (creation : SpanCreation) = + let pairs = [| KeyValuePair ("one", "uno") |] + let table = creation.Create pairs + pairs[0] <- KeyValuePair ("one", "changed") + Assert.AreEqual("uno", HashMap.find "one" table, $"HashMap.%s{creation.Name} must copy the span") + + [)>] + member _.``the span functions take an array when the types of the call are known`` (creation : SpanCreation) = + let table = creation.CreateFromArray [| KeyValuePair ("one", "uno") |] + + CollectionAssert.AreEqual ( + [| struct ("one", "uno") |], + HashMap.toArray table, + $"HashMap.%s{creation.Name} must take an array when the types of the call are known" + ) + + [)>] + member _.``the span functions keep the first of the bindings of a key with equal values`` (creation : SpanCreation) = + // The default comparers find the copies equal; the first key and the first value stay (ADR 0001, D14) + let firstKey = MapInputs.Copy "one" + let firstValue = MapInputs.Copy "uno" + + let table = + creation.Create [| KeyValuePair (firstKey, firstValue); KeyValuePair (MapInputs.Copy "one", MapInputs.Copy "uno") |] + + let struct (key, value) = + Assert.ContainsSingle (HashMap.toArray table, $"HashMap.%s{creation.Name} must merge the bindings of equal keys") + + Assert.AreSame (firstKey, key, $"HashMap.%s{creation.Name} must keep the first key") + Assert.AreSame (firstValue, value, $"HashMap.%s{creation.Name} must keep the first value") diff --git a/tests/FSharp.Collections.Immutable.Tests/HashMap/HashMapTupleTests.fs b/tests/FSharp.Collections.Immutable.Tests/HashMap/HashMapTupleTests.fs new file mode 100644 index 00000000..e9468f84 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/HashMap/HashMapTupleTests.fs @@ -0,0 +1,134 @@ +namespace FSharp.Collections.Immutable.Tests + +open System +open System.Collections.Generic +open FSharp.Collections.Immutable +open FSharp.Collections.Immutable.Tuple +open Microsoft.VisualStudio.TestTools.UnitTesting + +// The type annotations of the results are the proof that the functions of the Tuple namespace shadow the default ones. + +/// The `HashMap` module of the `Tuple` namespace. +[] +type HashMapTupleTests () = + + /// The bindings of the inputs as reference pairs. + static let pairs (bindings : struct (int * string) array) = bindings |> Array.map MapInputs.ToTuple + + static let table = FSharp.Collections.Immutable.HashMap.ofSeq MapInputs.Several + + /// The reference pairs of a map, sorted by key. + static let sortedPairs (table : HashMap) = HashMap.toArray table |> Array.sortBy fst + + [] + member _.``partition returns a reference tuple`` () = + let (accepted : HashMap), (rejected : HashMap) = + HashMap.partition (fun key _ -> key > 0) table + + let expectedAccepted, expectedRejected = + Map.partition (fun key _ -> key > 0) (Map.ofArray (pairs MapInputs.Several)) + + CollectionAssert.AreEqual ( + Map.toArray expectedAccepted, + sortedPairs accepted, + "Tuple.HashMap.partition must return the accepted bindings" + ) + + CollectionAssert.AreEqual ( + Map.toArray expectedRejected, + sortedPairs rejected, + "Tuple.HashMap.partition must return the rejected bindings" + ) + + [] + member _.``toArray returns reference tuples in the order of enumeration`` () = + let array : (int * string) array = HashMap.toArray table + + CollectionAssert.AreEqual ( + pairs (FSharp.Collections.Immutable.HashMap.toArray table), + array, + "Tuple.HashMap.toArray must follow the enumeration" + ) + + [] + member _.``toList returns reference tuples in the order of enumeration`` () = + let list : (int * string) list = HashMap.toList table + + CollectionAssert.AreEqual ( + pairs (FSharp.Collections.Immutable.HashMap.toArray table), + List.toArray list, + "Tuple.HashMap.toList must follow the enumeration" + ) + + [] + member _.``toSeq returns reference tuples in the order of enumeration`` () = + let sequence : seq = HashMap.toSeq table + + CollectionAssert.AreEqual ( + pairs (FSharp.Collections.Immutable.HashMap.toArray table), + Seq.toArray sequence, + "Tuple.HashMap.toSeq must follow the enumeration" + ) + + [] + member _.``the functions without a tuple keep resolving to the default module`` () = + let found : string voption = HashMap.tryFind 5 table + let replacements : KeyValuePair array = [| KeyValuePair (5, "five again") |] + let set : HashMap = HashMap.setItems replacements table + Assert.AreEqual(ValueSome "five", found, "HashMap.tryFind must resolve to the default module") + + Assert.AreEqual( + "five again", + FSharp.Collections.Immutable.HashMap.find 5 set, + "HashMap.setItems must resolve to the default module" + ) + + [] + member _.``the creation members keep resolving to the extensions of the type`` () = + // The module of the namespace has no ofArray, ofList or ofSeq, so they reach the overloads of HashMapExtensions + let bindings = pairs MapInputs.RepeatedKeys + let expected = Map.toArray (Map.ofArray bindings) + let ofArray : HashMap = HashMap.ofArray bindings + let ofList : HashMap = HashMap.ofList (List.ofArray bindings) + let ofSeq : HashMap = bindings |> HashMap.ofSeq + CollectionAssert.AreEqual (expected, sortedPairs ofArray, "HashMap.ofArray must take reference tuples") + CollectionAssert.AreEqual (expected, sortedPairs ofList, "HashMap.ofList must take reference tuples") + CollectionAssert.AreEqual (expected, sortedPairs ofSeq, "HashMap.ofSeq must take reference tuples") + + [] + member _.``the nested modules of the default module resolve after opening the namespace`` () = + // The module of the namespace has no nested modules, so HashMap.Tuple and HashMap.ValueTuple reach the default module + let tuplesAdded : HashMap = HashMap.Tuple.addRange [| 1, "one"; 100, "hundred" |] table + let tuplesSet : HashMap = HashMap.Tuple.setItems [| 5, "five again" |] table + let structsAdded : HashMap = HashMap.ValueTuple.addRange [| struct (1, "one") |] table + let structsSet : HashMap = HashMap.ValueTuple.setItems [| struct (5, "five again") |] table + + CollectionAssert.AreEqual ( + Map.toArray (Map.ofArray [| yield! pairs MapInputs.Several; 1, "one"; 100, "hundred" |]), + sortedPairs tuplesAdded, + "HashMap.Tuple.addRange must add the bindings" + ) + + Assert.AreEqual( + "five again", + FSharp.Collections.Immutable.HashMap.find 5 tuplesSet, + "HashMap.Tuple.setItems must set the bindings" + ) + + Assert.AreEqual( + "one", + FSharp.Collections.Immutable.HashMap.find 1 structsAdded, + "HashMap.ValueTuple.addRange must add the bindings" + ) + + Assert.AreEqual( + "five again", + FSharp.Collections.Immutable.HashMap.find 5 structsSet, + "HashMap.ValueTuple.setItems must set the bindings" + ) + + Assert.ThrowsExactly( + Action (fun () -> HashMap.Tuple.addRange [| 5, "other" |] table |> ignore), + "HashMap.Tuple.addRange must reject another value of a key, as the BCL AddRange does" + ) + |> ignore diff --git a/tests/FSharp.Collections.Immutable.Tests/HashMap/IMapContractTests.fs b/tests/FSharp.Collections.Immutable.Tests/HashMap/IMapContractTests.fs new file mode 100644 index 00000000..cad119b5 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/HashMap/IMapContractTests.fs @@ -0,0 +1,19 @@ +namespace FSharp.Collections.Immutable.Tests + +open FSharp.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// The reading contract for `IMap`, over hash maps. +[] +type IMapReadingTests () = + inherit MapContract> (IMapOps.reading) + +/// The transformation contract for `IMap`, over hash maps. +[] +type IMapTransformTests () = + inherit MapTransformContract> (IMapOps.transform) + +/// The mutation contract for `IMap`, over hash maps. +[] +type IMapMutationTests () = + inherit MapMutationContract> (IMapOps.mutation) diff --git a/tests/FSharp.Collections.Immutable.Tests/HashMap/IMapOps.fs b/tests/FSharp.Collections.Immutable.Tests/HashMap/IMapOps.fs new file mode 100644 index 00000000..7f2c51b1 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/HashMap/IMapOps.fs @@ -0,0 +1,73 @@ +/// The functions of the `IMap` module for the contract tests. +/// The interface module has no creation functions, so the maps under test are hash maps seen through the interface. +[] +module FSharp.Collections.Immutable.Tests.IMapOps + +open FSharp.Collections.Immutable + +let private ofSeq (elements : seq) = HashMap.ofSeq elements :> IMap + +/// The functions of the reading facet. `IMap` names its map parameter `table`, as the `Map` module does. +let reading : MapOps> = { + Name = nameof IMap + ParamName = id + OfSeq = ofSeq + ToArray = IMap.toArray + + Count = IMap.count + IsEmpty = IMap.isEmpty + Keys = IMap.keys + Values = IMap.values + + Find = IMap.find + TryFind = IMap.tryFind + + ContainsBinding = IMap.containsBinding + ContainsKey = IMap.containsKey + Exists = IMap.exists + FindKey = IMap.findKey + Forall = IMap.forall + Pick = IMap.pick + TryFindKey = IMap.tryFindKey + TryPick = IMap.tryPick + + Fold = IMap.fold + FoldBack = IMap.foldBack + Iter = IMap.iter + + ToList = IMap.toList + ToSeq = IMap.toSeq +} + +/// The functions of the transformation facet; `choose` and `map` return hash maps. +let transform : MapTransformOps> = { + Name = nameof IMap + ParamName = id + OfSeq = ofSeq + ToArray = IMap.toArray + + Choose = fun chooser table -> IMap.choose chooser table |> HashMap.toArray + Filter = IMap.filter + Map = fun mapping table -> IMap.map mapping table |> HashMap.toArray + Partition = IMap.partition +} + +/// The functions of the mutation facet: `addRange` and `setItems` of the module over key/value pairs, and of its nested +/// modules over tuples (ADR 0001, D13). +let mutation : MapMutationOps> = { + Name = nameof IMap + ParamName = id + OfSeq = ofSeq + ToArray = IMap.toArray + + Add = IMap.add + Change = IMap.change + Clear = IMap.clear + Remove = IMap.remove + RemoveRange = IMap.removeRange + SetItem = IMap.setItem + + KeyValuePairs = { AddRange = IMap.addRange; SetItems = IMap.setItems } + Tuples = { AddRange = IMap.Tuple.addRange; SetItems = IMap.Tuple.setItems } + StructTuples = { AddRange = IMap.ValueTuple.addRange; SetItems = IMap.ValueTuple.setItems } +} diff --git a/tests/FSharp.Collections.Immutable.Tests/HashMap/IMapOptionTests.fs b/tests/FSharp.Collections.Immutable.Tests/HashMap/IMapOptionTests.fs new file mode 100644 index 00000000..934e4fa5 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/HashMap/IMapOptionTests.fs @@ -0,0 +1,133 @@ +namespace FSharp.Collections.Immutable.Tests + +open System +open FSharp.Collections.Immutable +open FSharp.Collections.Immutable.Tuple +open FSharp.Collections.Immutable.Option +open Microsoft.VisualStudio.TestTools.UnitTesting + +// The namespaces are opened in the order that gives the shapes of FSharp.Core: Tuple first, Option last. +// The type annotations of the results are the proof that the functions of the Option namespace shadow the default ones. + +/// The `IMap` module of the `Option` namespace. +[] +type IMapOptionTests () = + + static let table = FSharp.Collections.Immutable.HashMap.ofSeq MapInputs.Several :> IMap + + static let oracle = MapInputs.ToMap MapInputs.Several + + /// A chooser that returns a value for the bindings of negative keys only. + static let negatedIfNegative (key : int) (value : string) = if key < 0 then Some (value.Length - key) else None + + /// The predicates of tryFindKey: one that some bindings satisfy and one that no binding satisfies. + static member Predicates = seq { + BindingPredicate.HasNegativeKey + BindingPredicate.Never + } + + [] + member _.``tryFind returns an option`` (key : int) = + let found : string option = IMap.tryFind key table + + OverrideAssert.OptionMatches ( + FSharp.Collections.Immutable.IMap.tryFind key table, + found, + "Option.IMap.tryFind must match the default" + ) + + [] + member _.``tryGetKey returns an option`` (key : int) = + let found : int option = IMap.tryGetKey key table + + OverrideAssert.OptionMatches ( + FSharp.Collections.Immutable.IMap.tryGetKey key table, + found, + "Option.IMap.tryGetKey must match the default" + ) + + [] + member _.``pick takes a chooser of options`` () = + let picked : int = IMap.pick negatedIfNegative table + + Assert.AreEqual( + FSharp.Collections.Immutable.IMap.pick (fun key value -> negatedIfNegative key value |> ValueOption.ofOption) table, + picked, + "Option.IMap.pick must pick as the default does" + ) + + [] + member _.``tryFindKey returns an option`` (predicate : BindingPredicate) = + let found : int option = IMap.tryFindKey predicate.Matches table + + OverrideAssert.OptionMatches ( + FSharp.Collections.Immutable.IMap.tryFindKey predicate.Matches table, + found, + "Option.IMap.tryFindKey must match the default" + ) + + [] + member _.``tryPick takes a chooser of options and returns an option`` () = + let found : int option = IMap.tryPick negatedIfNegative table + + OverrideAssert.OptionMatches ( + FSharp.Collections.Immutable.IMap.tryPick (fun key value -> negatedIfNegative key value |> ValueOption.ofOption) table, + found, + "Option.IMap.tryPick must match the default" + ) + + [] + member _.``change takes a function of options as Map.change does`` (key : int) = + let extend (current : string option) = current |> Option.map (fun value -> value + "!") + let changed : IMap = IMap.change key extend table + + CollectionAssert.AreEqual ( + MapInputs.OfMap (Map.change key extend oracle), + MapInputs.SortByKey (FSharp.Collections.Immutable.IMap.toArray changed), + "Option.IMap.change must change as Map.change does" + ) + + [] + member _.``change leaves the binding as it is for a value that the value comparer finds equal`` () = + // The function calls IMap.change, whose tests cover both implementations; the immutable dictionaries keep the + // binding, its key included, for an equal value (ADR 0001, D14) + let comparers = + FSharp.Collections.Immutable.HashMap.forComparers StringComparer.OrdinalIgnoreCase StringComparer.OrdinalIgnoreCase + + let ignoreCase : IMap = comparers.ofArray [| "one", "uno" |] + let changed : IMap = IMap.change "ONE" (fun _ -> Some "UNO") ignoreCase + Assert.AreEqual(Some "uno", IMap.tryFind "one" changed, "Option.IMap.change must keep the current value") + Assert.AreEqual(Some "one", IMap.tryGetKey "ONE" changed, "Option.IMap.change must keep the current key") + Assert.AreSame (ignoreCase, changed, "Option.IMap.change must return the input itself") + + [] + member _.``choose takes a chooser of options`` () = + let chosen : HashMap = IMap.choose negatedIfNegative table + + CollectionAssert.AreEqual ( + oracle + |> Map.toArray + |> Array.choose (fun (key, value) -> + negatedIfNegative key value + |> Option.map (fun chosen -> struct (key, chosen)) + ), + MapInputs.SortByKey (FSharp.Collections.Immutable.HashMap.toArray chosen), + "Option.IMap.choose must keep the chosen values" + ) + + [] + member _.``the functions without an option keep resolving to the other modules`` () = + let mapped : HashMap = IMap.map (fun _ (value : string) -> value.Length) table + let pairs : (int * string) list = IMap.toList table + + Assert.AreEqual( + MapInputs.Several.Length, + FSharp.Collections.Immutable.HashMap.count mapped, + "IMap.map must resolve to the default module" + ) + + CollectionAssert.AreEqual ( + Map.toArray oracle, + List.toArray pairs |> Array.sortBy fst, + "IMap.toList must resolve to the Tuple module" + ) diff --git a/tests/FSharp.Collections.Immutable.Tests/HashMap/IMapTests.fs b/tests/FSharp.Collections.Immutable.Tests/HashMap/IMapTests.fs new file mode 100644 index 00000000..a59b2135 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/HashMap/IMapTests.fs @@ -0,0 +1,542 @@ +namespace FSharp.Collections.Immutable.Tests + +open System +open System.Collections.Generic +open System.Collections.Immutable +open FSharp.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// +/// The implementations of the maps that the tests of IMap see through the interface, as the cases of their rows. +/// +[] +type MapImplementation = + /// An . + | Hash + /// An . + | Sorted + + /// Every implementation, as the rows of the tests that run once per implementation. + static member All = seq { + MapImplementation.Hash + MapImplementation.Sorted + } + +/// The behaviour of `IMap` that the shared contracts do not cover: null inputs, the implementation and the comparers of the +/// results, and the functions that wrap the interface. +[] +type IMapTests () = + + static let nullTable = Unchecked.defaultof> + + static let bindings = [| KeyValuePair ("one", "uno"); KeyValuePair ("Two", "dos"); KeyValuePair ("three", "tres") |] + + /// A map of the given implementation whose keys and values ignore the case, seen through the interface. + static let tableOf (implementation : MapImplementation) : IMap = + match implementation with + | MapImplementation.Hash -> + (HashMap.forComparers StringComparer.OrdinalIgnoreCase StringComparer.OrdinalIgnoreCase).ofArray bindings + | MapImplementation.Sorted -> + ImmutableSortedDictionary.CreateRange (StringComparer.OrdinalIgnoreCase, StringComparer.OrdinalIgnoreCase, bindings) + + /// The key comparer of the immutable dictionaries; the interface does not expose it. + static let keyComparerOf (table : IMap) : obj | null = + match table with + | :? ImmutableDictionary as hash -> hash.KeyComparer + | :? ImmutableSortedDictionary as sorted -> sorted.KeyComparer + | _ -> null + + /// The functions that take a map, by name, on a null map. The rows of the tests are the names: a function has no + /// readable name in a row. + static let nullTableCalls : Map unit> = + Map.ofArray [| + nameof IMap.add, (fun () -> IMap.add "a" "b" nullTable |> ignore) + nameof IMap.addRange, + (fun () -> + IMap.addRange [| KeyValuePair ("a", "b") |] nullTable + |> ignore + ) + nameof IMap.change, (fun () -> IMap.change "a" id nullTable |> ignore) + nameof IMap.choose, + (fun () -> + IMap.choose (fun _ value -> ValueSome value) nullTable + |> ignore + ) + nameof IMap.clear, (fun () -> IMap.clear nullTable |> ignore) + nameof IMap.containsBinding, (fun () -> IMap.containsBinding "a" "b" nullTable |> ignore) + nameof IMap.containsKey, (fun () -> IMap.containsKey "a" nullTable |> ignore) + nameof IMap.count, (fun () -> IMap.count nullTable |> ignore) + nameof IMap.exists, (fun () -> IMap.exists (fun _ _ -> true) nullTable |> ignore) + nameof IMap.filter, (fun () -> IMap.filter (fun _ _ -> true) nullTable |> ignore) + nameof IMap.find, (fun () -> IMap.find "a" nullTable |> ignore) + nameof IMap.findKey, (fun () -> IMap.findKey (fun _ _ -> true) nullTable |> ignore) + nameof IMap.fold, (fun () -> IMap.fold (fun state _ _ -> state) 0 nullTable |> ignore) + nameof IMap.foldBack, (fun () -> IMap.foldBack (fun _ _ state -> state) nullTable 0 |> ignore) + nameof IMap.forall, (fun () -> IMap.forall (fun _ _ -> true) nullTable |> ignore) + nameof IMap.isEmpty, (fun () -> IMap.isEmpty nullTable |> ignore) + nameof IMap.iter, (fun () -> IMap.iter (fun _ _ -> ()) nullTable) + nameof IMap.keys, (fun () -> IMap.keys nullTable |> ignore) + nameof IMap.map, (fun () -> IMap.map (fun _ value -> value) nullTable |> ignore) + nameof IMap.partition, (fun () -> IMap.partition (fun _ _ -> true) nullTable |> ignore) + nameof IMap.pick, + (fun () -> + IMap.pick (fun _ value -> ValueSome value) nullTable + |> ignore + ) + nameof IMap.remove, (fun () -> IMap.remove "a" nullTable |> ignore) + nameof IMap.removeRange, (fun () -> IMap.removeRange [| "a" |] nullTable |> ignore) + nameof IMap.setItem, (fun () -> IMap.setItem "a" "b" nullTable |> ignore) + nameof IMap.setItems, + (fun () -> + IMap.setItems [| KeyValuePair ("a", "b") |] nullTable + |> ignore + ) + nameof IMap.toArray, (fun () -> IMap.toArray nullTable |> ignore) + nameof IMap.toList, (fun () -> IMap.toList nullTable |> ignore) + nameof IMap.toSeq, (fun () -> IMap.toSeq nullTable |> ignore) + nameof IMap.tryFind, (fun () -> IMap.tryFind "a" nullTable |> ignore) + nameof IMap.tryFindKey, (fun () -> IMap.tryFindKey (fun _ _ -> true) nullTable |> ignore) + nameof IMap.tryGetKey, (fun () -> IMap.tryGetKey "a" nullTable |> ignore) + nameof IMap.tryPick, + (fun () -> + IMap.tryPick (fun _ value -> ValueSome value) nullTable + |> ignore + ) + nameof IMap.values, (fun () -> IMap.values nullTable |> ignore) + "Tuple.addRange", (fun () -> IMap.Tuple.addRange [| "a", "b" |] nullTable |> ignore) + "Tuple.setItems", (fun () -> IMap.Tuple.setItems [| "a", "b" |] nullTable |> ignore) + "ValueTuple.addRange", + (fun () -> + IMap.ValueTuple.addRange [| struct ("a", "b") |] nullTable + |> ignore + ) + "ValueTuple.setItems", + (fun () -> + IMap.ValueTuple.setItems [| struct ("a", "b") |] nullTable + |> ignore + ) + |] + + /// The functions that take a sequence, by name, with the name of the sequence parameter and the function on a null + /// sequence. + static let nullSequenceCalls : Map unit))> = + let table = tableOf MapImplementation.Hash + + Map.ofArray [| + nameof IMap.addRange, + struct ("pairs", + fun () -> + IMap.addRange (Unchecked.defaultof>>) table + |> ignore) + nameof IMap.removeRange, + struct ("keys", + fun () -> + IMap.removeRange (Unchecked.defaultof>) table + |> ignore) + nameof IMap.setItems, + struct ("pairs", + fun () -> + IMap.setItems (Unchecked.defaultof>>) table + |> ignore) + "Tuple.addRange", + struct ("pairs", + fun () -> + IMap.Tuple.addRange (Unchecked.defaultof>) table + |> ignore) + "Tuple.setItems", + struct ("pairs", + fun () -> + IMap.Tuple.setItems (Unchecked.defaultof>) table + |> ignore) + "ValueTuple.addRange", + struct ("pairs", + fun () -> + IMap.ValueTuple.addRange (Unchecked.defaultof>) table + |> ignore) + "ValueTuple.setItems", + struct ("pairs", + fun () -> + IMap.ValueTuple.setItems (Unchecked.defaultof>) table + |> ignore) + |] + + /// The functions that take a sequence, by name, with the name of the sequence parameter and the function on a default + /// flat list that is given as the sequence. A default flat list converts to a sequence without a check and fails with + /// InvalidOperationException when the sequence is enumerated, so the other inputs are valid and the function would + /// enumerate it. The last row gives a sequence of objects, which a default flat list of strings is by the covariance of + /// seq: the element type of the sequence is not the one of the flat list. + static let defaultSequenceCalls : Map unit))> = + let table = tableOf MapImplementation.Hash + let objectTable = HashMap.ofArray [| KeyValuePair("one", "uno") |] :> IMap + let defaultPairs = Unchecked.defaultof>> :> seq<_> + let defaultTuples = Unchecked.defaultof> :> seq<_> + let defaultStructTuples = Unchecked.defaultof> :> seq<_> + let defaultKeys = Unchecked.defaultof> :> seq<_> + + Map.ofArray [| + nameof IMap.addRange, struct ("pairs", fun () -> IMap.addRange defaultPairs table |> ignore) + nameof IMap.removeRange, struct ("keys", fun () -> IMap.removeRange defaultKeys table |> ignore) + nameof IMap.setItems, struct ("pairs", fun () -> IMap.setItems defaultPairs table |> ignore) + "Tuple.addRange", struct ("pairs", fun () -> IMap.Tuple.addRange defaultTuples table |> ignore) + "Tuple.setItems", struct ("pairs", fun () -> IMap.Tuple.setItems defaultTuples table |> ignore) + "ValueTuple.addRange", struct ("pairs", fun () -> IMap.ValueTuple.addRange defaultStructTuples table |> ignore) + "ValueTuple.setItems", struct ("pairs", fun () -> IMap.ValueTuple.setItems defaultStructTuples table |> ignore) + $"%s{nameof IMap.removeRange} (default flat list of strings as objects)", + struct ("keys", + fun () -> + IMap.removeRange FlatListOracles.defaultStringsAsObjects objectTable + |> ignore) + |] + + /// The transformations that keep the type of the values, by name. + static let sameTypeTransformations : Map -> IMap> = + Map.ofArray [| + nameof IMap.add, IMap.add "four" "cuatro" + nameof IMap.addRange, IMap.addRange [| KeyValuePair ("four", "cuatro") |] + nameof IMap.change, IMap.change "one" (fun _ -> ValueNone) + nameof IMap.clear, IMap.clear + nameof IMap.filter, IMap.filter (fun (key : string) _ -> key.Length > 3) + ($"%s{nameof IMap.partition} (accepted)", + fun table -> + let struct (accepted, _) = IMap.partition (fun (key : string) _ -> key.Length > 3) table + accepted) + ($"%s{nameof IMap.partition} (rejected)", + fun table -> + let struct (_, rejected) = IMap.partition (fun (key : string) _ -> key.Length > 3) table + rejected) + nameof IMap.remove, IMap.remove "one" + nameof IMap.removeRange, IMap.removeRange [| "one"; "two" |] + nameof IMap.setItem, IMap.setItem "four" "cuatro" + nameof IMap.setItems, IMap.setItems [| KeyValuePair ("four", "cuatro") |] + "Tuple.addRange", IMap.Tuple.addRange [| "four", "cuatro" |] + "Tuple.setItems", IMap.Tuple.setItems [| "four", "cuatro" |] + "ValueTuple.addRange", IMap.ValueTuple.addRange [| struct ("four", "cuatro") |] + "ValueTuple.setItems", IMap.ValueTuple.setItems [| struct ("four", "cuatro") |] + |] + + /// The transformations that change the type of the values, by name. + static let typeChangingTransformations : Map -> HashMap> = + Map.ofArray [| + nameof IMap.choose, IMap.choose (fun _ (value : string) -> ValueSome value.Length) + nameof IMap.map, IMap.map (fun _ (value : string) -> value.Length) + |] + + /// The functions that bind "ONE" to "UNO" in a map whose keys and values ignore the case, by name: the map binds "one" + /// to "uno", a value that the value comparer finds equal to "UNO". + static let equalValueCalls : Map -> IMap> = + Map.ofArray [| + nameof IMap.add, IMap.add "ONE" "UNO" + nameof IMap.addRange, IMap.addRange [| KeyValuePair ("ONE", "UNO") |] + nameof IMap.change, IMap.change "ONE" (fun _ -> ValueSome "UNO") + nameof IMap.setItem, IMap.setItem "ONE" "UNO" + nameof IMap.setItems, IMap.setItems [| KeyValuePair ("ONE", "UNO") |] + "Tuple.addRange", IMap.Tuple.addRange [| "ONE", "UNO" |] + "Tuple.setItems", IMap.Tuple.setItems [| "ONE", "UNO" |] + "ValueTuple.addRange", IMap.ValueTuple.addRange [| struct ("ONE", "UNO") |] + "ValueTuple.setItems", IMap.ValueTuple.setItems [| struct ("ONE", "UNO") |] + |] + + /// The transformations that change the type of the values and give every binding the same result, by name. + static let constantTransformations : Map -> HashMap> = + Map.ofArray [| + nameof IMap.choose, IMap.choose (fun _ _ -> ValueSome 0) + nameof IMap.map, IMap.map (fun _ _ -> 0) + |] + + /// The names of the functions that take a map, as the rows of their test. + static member NullTableCalls = nullTableCalls.Keys + + /// The names of the functions that take a sequence, as the rows of their test. + static member NullSequenceCalls = nullSequenceCalls.Keys + + /// The names of the functions that take a sequence and are given a default flat list, as the rows of their test. + static member DefaultSequenceCalls = defaultSequenceCalls.Keys + + /// Every implementation with each transformation that keeps the type of the values. + static member ImplementationsAndSameTypeTransformations = seq { + for implementation in MapImplementation.All do + for name in sameTypeTransformations.Keys do + struct (implementation, name) + } + + /// Every implementation with each transformation that changes the type of the values. + static member ImplementationsAndTypeChangingTransformations = seq { + for implementation in MapImplementation.All do + for name in typeChangingTransformations.Keys do + struct (implementation, name) + } + + /// Every implementation with each function that sets a value that the value comparer finds equal to the current one. + static member ImplementationsAndEqualValueCalls = seq { + for implementation in MapImplementation.All do + for name in equalValueCalls.Keys do + struct (implementation, name) + } + + /// The names of the transformations that give every binding the same result, as the rows of their test. + static member ConstantTransformations = constantTransformations.Keys + + [] + member _.``functions reject a null map`` (name : string) = + let ex = + Assert.ThrowsExactly(Action nullTableCalls[name], $"IMap.%s{name} must reject a null map") + Assert.AreEqual("table", ex.ParamName, $"IMap.%s{name} must name the map parameter") + + [] + member _.``functions reject a null sequence`` (name : string) = + let struct (paramName, call) = nullSequenceCalls[name] + let ex = + Assert.ThrowsExactly(Action call, $"IMap.%s{name} must reject a null sequence") + Assert.AreEqual(paramName, ex.ParamName, $"IMap.%s{name} must name the sequence parameter") + + [] + member _.``functions reject a default flat list given as a sequence`` (name : string) = + // The exception names the parameter of the function, not the one of the BCL member that it would have called + let struct (paramName, call) = defaultSequenceCalls[name] + FlatListAssert.RejectsNull ($"IMap.%s{name}", paramName, call) + + [)>] + member _.``a flat list that is not default passes as a sequence, also one of strings as a sequence of objects`` + (implementation : MapImplementation) + = + let flatList (elements : 'T array) = FlatList.ofArray elements :> seq<'T> + let table = tableOf implementation + + let bindingsOf (map : IMap) = + IMap.toArray map + |> Array.sortBy (fun struct (key, _) -> key) + + CollectionAssert.AreEqual ( + [| + struct ("Two", "dos") + struct ("four", "cuatro") + struct ("one", "uno") + struct ("three", "tres") + |], + bindingsOf (IMap.addRange (flatList [| KeyValuePair ("four", "cuatro") |]) table), + "IMap.addRange must take the elements of a flat list" + ) + + CollectionAssert.AreEqual ( + [| + struct ("Two", "dos") + struct ("one", "uno") + struct ("three", "tres") + struct ("zero", "cero") + |], + bindingsOf (IMap.setItems (flatList [| KeyValuePair ("zero", "cero") |]) table), + "IMap.setItems must take the elements of a flat list" + ) + + CollectionAssert.AreEqual ( + [| struct ("Two", "dos"); struct ("three", "tres") |], + bindingsOf (IMap.removeRange (flatList [| "one" |]) table), + "IMap.removeRange must take the elements of a flat list" + ) + + // A flat list of strings is a sequence of objects, which a type test for the flat list of objects does not see + let widened = FlatListOracles.stringsAsObjects (FlatList.ofArray [| "one" |]) + let objectTable = + HashMap.ofArray [| KeyValuePair("one", "uno"); KeyValuePair("two", "dos") |] + :> IMap + + CollectionAssert.AreEqual ( + [| "two" :> obj |], + IMap.keys (IMap.removeRange widened objectTable) + |> Seq.toArray, + "IMap.removeRange must take the elements of a flat list of strings as objects" + ) + + [] + member _.``the transformations that keep the type of the values keep the implementation and the comparers`` + (implementation : MapImplementation, name : string) + = + let table = tableOf implementation + let result = sameTypeTransformations[name] table + Assert.AreEqual(table.GetType (), result.GetType (), $"IMap.%s{name} must keep the implementation of the map") + Assert.AreSame(StringComparer.OrdinalIgnoreCase, keyComparerOf result, $"IMap.%s{name} must keep the key comparer") + + [] + member _.``map and choose return hash maps with the default comparers`` (implementation : MapImplementation, name : string) = + let result = typeChangingTransformations[name](tableOf implementation) + + Assert.AreSame>( + EqualityComparer.Default, + HashMap.keyComparer result, + $"IMap.%s{name} must compare the keys of its hash map with the default comparer" + ) + + Assert.AreSame>( + EqualityComparer.Default, + HashMap.valueComparer result, + $"IMap.%s{name} must compare the values of its hash map with the default comparer" + ) + + Assert.AreEqual(3, HashMap.find "Two" result, $"IMap.%s{name} must keep the keys of the map") + + [] + member _.``map keeps the last value of keys that only the default comparer considers equal`` () = + let first = String ([| 'a'; 'b' |]) + let second = String ([| 'a'; 'b' |]) + + let table : IMap = + (HashMap.forKeyComparer HashIdentity.Reference).ofArray [| struct (first, 1); struct (second, 2) |] + + Assert.AreEqual(2, IMap.count table, "The keys must be distinct in the reference comparer of the map") + let struct (_, lastValue) = IMap.toArray table |> Array.last + let mapped = IMap.map (fun _ value -> value * 10) table + Assert.AreEqual(1, HashMap.count mapped, "IMap.map must merge the keys that the default comparer considers equal") + Assert.AreEqual( + lastValue * 10, + HashMap.find "ab" mapped, + "IMap.map must keep the last value in the order of enumeration" + ) + + [] + member _.``map and choose keep the first of the keys that only the default comparer considers equal for an equal result`` + (name : string) + = + // The builder of the result keeps the first binding of a key for an equal value (ADR 0001, D14) + let table : IMap = + (HashMap.forKeyComparer HashIdentity.Reference).ofArray [| + struct (MapInputs.Copy "ab", 1) + struct (MapInputs.Copy "ab", 2) + |] + + let struct (firstKey, _) = IMap.toArray table |> Array.head + + let struct (key, _) = + Assert.ContainsSingle ( + HashMap.toArray (constantTransformations[name] table), + $"IMap.%s{name} must merge the equal keys" + ) + + Assert.AreSame (firstKey, key, $"IMap.%s{name} must keep the first of the equal keys in the order of enumeration") + + [)>] + member _.``filter and partition return the input itself when every binding stays`` (implementation : MapImplementation) = + let table = tableOf implementation + Assert.AreSame ( + table, + IMap.filter (fun _ _ -> true) table, + "IMap.filter must return the input itself when every binding stays" + ) + let struct (accepted, rejected) = IMap.partition (fun _ _ -> true) table + + Assert.AreSame (table, accepted, "IMap.partition must return the input itself when every binding satisfies the predicate") + + Assert.IsTrue (IMap.isEmpty rejected, "IMap.partition must return an empty map of the other bindings") + + [] + member _.``a value that the value comparer finds equal leaves the binding as it is`` + (implementation : MapImplementation, name : string) + = + // The immutable dictionaries keep the binding, its key included, for an equal value (ADR 0001, D14) + let table = tableOf implementation + let result = equalValueCalls[name] table + Assert.AreEqual("uno", IMap.find "one" result, $"IMap.%s{name} must keep the current value") + Assert.AreEqual(ValueSome "one", IMap.tryGetKey "ONE" result, $"IMap.%s{name} must keep the current key") + Assert.AreSame (table, result, $"IMap.%s{name} must return the input itself") + + [)>] + member _.``tryGetKey returns the key that the map holds`` (implementation : MapImplementation) = + let table = tableOf implementation + Assert.AreEqual(ValueSome "Two", IMap.tryGetKey "TWO" table, "IMap.tryGetKey must return the stored key") + Assert.AreEqual( + ValueNone, + IMap.tryGetKey "four" table, + "IMap.tryGetKey must return nothing for a missing key" + ) + + [] + member _.``the functions follow the order of enumeration of the implementation`` () = + let table = tableOf MapImplementation.Sorted + let expectedKeys = [| "one"; "three"; "Two" |] + CollectionAssert.AreEqual (expectedKeys, Seq.toArray (IMap.keys table), "IMap.keys must follow the order of a sorted map") + + CollectionAssert.AreEqual ( + [| "uno"; "tres"; "dos" |], + Seq.toArray (IMap.values table), + "IMap.values must follow the order of a sorted map" + ) + + CollectionAssert.AreEqual ( + expectedKeys, + IMap.foldBack (fun key _ keys -> Array.append [| key |] keys) table [||], + "IMap.foldBack must visit a sorted map from its last key" + ) + + Assert.AreEqual( + ValueSome "three", + IMap.tryFindKey (fun (key : string) _ -> key.StartsWith ("t", StringComparison.OrdinalIgnoreCase)) table, + "IMap.tryFindKey must return the first key of a sorted map that satisfies the predicate" + ) + + [)>] + member _.``add throws what the implementation throws for a value that the value comparer finds different`` + (implementation : MapImplementation) + = + let table = tableOf implementation + + ExceptionAssert.Same ( + id, + (fun () -> table.Add ("ONE", "dos") |> ignore), + (fun () -> IMap.add "ONE" "dos" table |> ignore), + "IMap.add must throw what IImmutableDictionary.Add throws" + ) + + [)>] + member _.``setItem replaces the value of a key that add rejects`` (implementation : MapImplementation) = + let table = tableOf implementation + let replaced = IMap.setItem "ONE" "dos" table + Assert.AreEqual("dos", IMap.find "one" replaced, "IMap.setItem must replace the value of the key") + Assert.AreEqual("uno", IMap.find "one" table, "IMap.setItem must not change the input") + + [)>] + member _.``containsBinding compares the value with the value comparer of the map where tryFind and equality do not`` + (implementation : MapImplementation) + = + // The keys and the values ignore the case: "one" is bound to "uno" + let table = tableOf implementation + + Assert.IsTrue ( + IMap.containsBinding "ONE" "UNO" table, + "IMap.containsBinding must find a value that the value comparer finds equal" + ) + + Assert.IsFalse ( + IMap.tryFind "ONE" table + |> ValueOption.exists (fun stored -> stored = "UNO"), + "F# equality finds \"uno\" and \"UNO\" different, which is why the function asks the map" + ) + + Assert.IsFalse (IMap.containsBinding "ONE" "DOS" table, "IMap.containsBinding must not find another value") + Assert.IsFalse (IMap.containsBinding "four" "UNO" table, "IMap.containsBinding must not find a key without a binding") + + [] + member _.``containsBinding leaves a null key to the implementation`` () = + let nullKey = Unchecked.defaultof + let defaultHash : IMap = HashMap.ofArray [| "one", "uno" |] + + Assert.IsFalse ( + IMap.containsBinding nullKey "uno" defaultHash, + "IMap.containsBinding must give false for a null key of a hash map with the default key comparer" + ) + + Assert.ThrowsExactly( + Action (fun () -> + IMap.containsBinding nullKey "uno" (tableOf MapImplementation.Hash) + |> ignore + ), + "IMap.containsBinding must throw for a null key of a hash map whose key comparer rejects it" + ) + |> ignore + + Assert.ThrowsExactly( + Action (fun () -> + IMap.containsBinding nullKey "uno" (tableOf MapImplementation.Sorted) + |> ignore + ), + "IMap.containsBinding must throw for a null key of a sorted map, as ImmutableSortedDictionary.Contains does" + ) + |> ignore diff --git a/tests/FSharp.Collections.Immutable.Tests/HashMap/IMapTupleTests.fs b/tests/FSharp.Collections.Immutable.Tests/HashMap/IMapTupleTests.fs new file mode 100644 index 00000000..0eca53b9 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/HashMap/IMapTupleTests.fs @@ -0,0 +1,113 @@ +namespace FSharp.Collections.Immutable.Tests + +open System +open FSharp.Collections.Immutable +open FSharp.Collections.Immutable.Tuple +open Microsoft.VisualStudio.TestTools.UnitTesting + +// The type annotations of the results are the proof that the functions of the Tuple namespace shadow the default ones. + +/// The `IMap` module of the `Tuple` namespace. +[] +type IMapTupleTests () = + + /// The bindings of the inputs as reference pairs. + static let pairs (bindings : struct (int * string) array) = bindings |> Array.map MapInputs.ToTuple + + static let table = FSharp.Collections.Immutable.HashMap.ofSeq MapInputs.Several :> IMap + + /// The reference pairs of a map, sorted by key. + static let sortedPairs (table : IMap) = IMap.toArray table |> Array.sortBy fst + + [] + member _.``partition returns a reference tuple`` () = + let (accepted : IMap), (rejected : IMap) = + IMap.partition (fun key _ -> key > 0) table + + let expectedAccepted, expectedRejected = + Map.partition (fun key _ -> key > 0) (Map.ofArray (pairs MapInputs.Several)) + + CollectionAssert.AreEqual ( + Map.toArray expectedAccepted, + sortedPairs accepted, + "Tuple.IMap.partition must return the accepted bindings" + ) + + CollectionAssert.AreEqual ( + Map.toArray expectedRejected, + sortedPairs rejected, + "Tuple.IMap.partition must return the rejected bindings" + ) + + [] + member _.``toArray returns reference tuples in the order of enumeration`` () = + let array : (int * string) array = IMap.toArray table + + CollectionAssert.AreEqual ( + pairs (FSharp.Collections.Immutable.IMap.toArray table), + array, + "Tuple.IMap.toArray must follow the enumeration" + ) + + [] + member _.``toList returns reference tuples in the order of enumeration`` () = + let list : (int * string) list = IMap.toList table + + CollectionAssert.AreEqual ( + pairs (FSharp.Collections.Immutable.IMap.toArray table), + List.toArray list, + "Tuple.IMap.toList must follow the enumeration" + ) + + [] + member _.``toSeq returns reference tuples in the order of enumeration`` () = + let sequence : seq = IMap.toSeq table + + CollectionAssert.AreEqual ( + pairs (FSharp.Collections.Immutable.IMap.toArray table), + Seq.toArray sequence, + "Tuple.IMap.toSeq must follow the enumeration" + ) + + [] + member _.``the functions without a tuple keep resolving to the default module`` () = + let found : string voption = IMap.tryFind 5 table + Assert.AreEqual(ValueSome "five", found, "IMap.tryFind must resolve to the default module") + + [] + member _.``the nested modules of the default module resolve after opening the namespace`` () = + // The module of the namespace has no nested modules, so IMap.Tuple and IMap.ValueTuple reach the default module + let tuplesAdded : IMap = IMap.Tuple.addRange [| 1, "one"; 100, "hundred" |] table + let tuplesSet : IMap = IMap.Tuple.setItems [| 5, "five again" |] table + let structsAdded : IMap = IMap.ValueTuple.addRange [| struct (1, "one") |] table + let structsSet : IMap = IMap.ValueTuple.setItems [| struct (5, "five again") |] table + + CollectionAssert.AreEqual ( + Map.toArray (Map.ofArray [| yield! pairs MapInputs.Several; 1, "one"; 100, "hundred" |]), + sortedPairs tuplesAdded, + "IMap.Tuple.addRange must add the bindings" + ) + + Assert.AreEqual( + "five again", + FSharp.Collections.Immutable.IMap.find 5 tuplesSet, + "IMap.Tuple.setItems must set the bindings" + ) + + Assert.AreEqual( + "one", + FSharp.Collections.Immutable.IMap.find 1 structsAdded, + "IMap.ValueTuple.addRange must add the bindings" + ) + + Assert.AreEqual( + "five again", + FSharp.Collections.Immutable.IMap.find 5 structsSet, + "IMap.ValueTuple.setItems must set the bindings" + ) + + Assert.ThrowsExactly( + Action (fun () -> IMap.Tuple.addRange [| 5, "other" |] table |> ignore), + "IMap.Tuple.addRange must reject another value of a key, as the BCL AddRange does" + ) + |> ignore From bd8c3575b45f757804ce3a39438a9d33f0f7f548 Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Sun, 18 Oct 2026 13:02:12 +0200 Subject: [PATCH 3/9] docs: `HashMap` and `IMap` in the changelog, the collection table and the tutorial The tutorial creates its hash map from struct tuples, which `HashMap.ofSeq` takes now. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 70 ++++++++++++++++++++++++++++ README.md | 3 +- docsSrc/Explanations/Background.md | 3 +- docsSrc/Tutorials/Getting_Started.md | 10 +++- docsSrc/index.md | 3 +- 5 files changed, 84 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8373ad55..7872abdb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,64 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 semantics and exceptions. Its `tryFind`, `tryFindIndex` and `tryPick` return `voption`, `choose` and `tryPick` take functions that return `voption`, and `groupBy`, `partition`, `partitionWith` and `zip` return struct tuples; the `Parallel` modules of the `Option` and `Tuple` namespaces repeat them with options and reference tuples. +* `HashMap` has every function of the FSharp.Core `Map` module that applies to a hash map, with its semantics and + exceptions, except `add`, which stays the `Add` of `ImmutableDictionary`, as in 1.0.0; plus `choose` and `singleton`, + the builder functions `build` and `update`, and the wrappers of the `ImmutableDictionary` members `addRange`, + `containsBinding`, `containsValue`, `ofReadOnlySpan`, `ofSeqBy`, `removeRange`, `setItem`, `setItems`, `tryGetKey` and + `valueComparer`, which keep the semantics of the BCL. +* The transformations of `HashMap` keep the key comparer of the map, and the value comparer when the type of the values + does not change. +* `HashMap.containsBinding` and `IMap.containsBinding` tell whether a key is bound to a value, comparing the value with the + value comparer of the map, as `Contains` of a `KeyValuePair` does. `tryFind` followed by `=` compares with F# equality, + which can differ from the value comparer, and an `IMap` does not expose its value comparer. +* `HashMap.ofReadOnlySpan` and `HashMap.ofSpan` create a hash map from a `ReadOnlySpan` or a `Span` of + `KeyValuePair<'Key, 'Value>` with `ImmutableDictionary.CreateRangeWithOverwrite`, which takes a read-only span only, and + the members `ofReadOnlySpan` and `ofSpan` of `CustomComparersHashMap` do so with its comparers. A repeated key never + throws: the last binding wins, unless the value comparer finds the two values equal. F# converts a `Span` to the + parameter of `ofReadOnlySpan` only when it has inferred the types of the call, which is why `ofSpan` exists; an array + converts to either parameter under the same condition. +* `HashMap.build` creates a hash map with a builder that an action fills, `HashMap.update` changes a hash map with a + builder, and the member `build` of `CustomComparersHashMap` does the first with its comparers. +* `HashMap.ofArray` and `HashMap.ofList` are new, and `HashMap.ofSeq`, which took `KeyValuePair`s, takes tuples too. The + three take the bindings as `KeyValuePair`s, reference tuples or struct tuples: they are static members of + `ImmutableDictionary<'Key, 'Value>` in the auto-opened `HashMapExtensions` module, one overload per shape, and every + shape keeps the last binding of a repeated key, as `Map.ofSeq` does, unless the value comparer finds the two values + equal. They take a dictionary, an F# `Map` or another hash map too. C# does not see them as members of + `ImmutableDictionary`; it calls the public static methods `HashMapExtensions.OfArray`, `OfList` and `OfSeq`, or the + members of the struct below. A reflection over the `HashMap` module does not list them. To reject another value of a + repeated key, as `ImmutableDictionary.CreateRange` does, add the bindings to an empty map: + `HashMap.empty |> HashMap.addRange pairs`. +* `HashMap.forComparers` and `HashMap.forKeyComparer` return a `CustomComparersHashMap<'Key, 'Value>` struct whose members + `build`, `builder`, `empty`, `ofArray`, `ofList`, `ofReadOnlySpan`, `ofSeq`, `ofSeqBy`, `ofSpan` and `singleton` create + hash maps with its comparers: `(HashMap.forKeyComparer StringComparer.OrdinalIgnoreCase).ofSeq pairs`. A `null` + comparer throws `ArgumentNullException`. +* `ofSeq` of that struct also gives an existing hash map other comparers, as `WithComparers` does, with three differences. + It takes O(1) only when the key comparer of the map is the instance of the struct, and rebuilds the map otherwise. + Keys that the new comparer makes equal merge, the later binding winning, where `WithComparers` throws. And + `forKeyComparer` resets the value comparer, where `WithComparers` keeps it; to keep it, pass the value comparer of the + map: `HashMap.forComparers keyComparer (HashMap.valueComparer table)`. +* `HashMap.addRange` and `HashMap.setItems` take `KeyValuePair`s; the nested modules `HashMap.Tuple` and + `HashMap.ValueTuple` take reference and struct tuples: `HashMap.Tuple.addRange pairs table`. +* `HashMap` and `IMap` keep the convention of `ImmutableDictionary` where it differs from `Map`: 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, in `add`, `change`, + `setItem`, `setItems`, `addRange`, the creation functions and members, and `hashMap { }`. `add` is the `Add` of + `ImmutableDictionary`, as it was in 1.0.0: it throws `ArgumentException` for a key that is bound to a value that the + value comparer finds different, and returns the input itself for the same binding; `setItem` replaces the value. +* `change` with a function that keeps an existing value adds a binding only when the key has none, as + `ImmutableInterlocked.TryAdd` does. The other single-key idioms of the BCL need no new function: `tryFind` is + `TryGetValue`, and `tryFind` followed by `ValueOption.defaultValue` is `GetValueOrDefault`. `ToImmutableDictionary` with + an element selector has no function; to reject a repeated key like the BCL does, add the mapped pairs to an empty map, + `HashMap.empty |> HashMap.addRange (source |> Seq.map (fun x -> KeyValuePair (key x, element x)))`, and `HashMap.ofSeq` + of the mapped pairs lets the last binding win. Maps have no builder module: call `Add`, `AddRange`, `Remove`, + `RemoveRange`, the indexer and the other members on the builder. +* The `IMap` module works on any `IImmutableDictionary<'Key, 'Value>` with the same functions, except the creation, builder + and comparer functions and `containsValue`, which the interface does not declare, and has the nested modules `Tuple` and + `ValueTuple` too. Its `add` is the `Add` of the interface and its `containsBinding` the `Contains` of a `KeyValuePair`. + Its transformations keep the implementation and the comparers of the map; `map` and `choose` return a `HashMap`. +* The `FSharp.Collections.Immutable.Option` and `FSharp.Collections.Immutable.Tuple` namespaces have `HashMap` and `IMap` + modules that repeat the functions that use value options or return struct tuples with options and reference tuples. +* `hashMap { }` builds a hash map from `yield struct (key, value)` and `yield KeyValuePair (key, value)`; `HashMapCE` takes + a key comparer, and a `null` one throws `ArgumentNullException`. ### Changed @@ -44,6 +102,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `FlatList.sub` has the meaning of `Array.sub`: `sub array startIndex count` returns `count` elements from `startIndex`. It was `sub start stop list`. * The nested `FlatList.Builder` module is now the `FlatListBuilder` module. +* `HashMap.tryFind` returns a `voption`, where it returned an `option`; + `FSharp.Collections.Immutable.Option.HashMap.tryFind` returns the `option`. +* `HashMap.ofSeq` keeps the last binding of a repeated key, where it threw `ArgumentException` for a key that is bound to + two different values. +* `HashMap.ofSeq` throws `ArgumentNullException` for a default flat list given as a sequence, where it threw + `InvalidOperationException`. +* `HashMap.filter` returns the bindings that satisfy the predicate, where it returned all the bindings of the hash map. ### Removed @@ -61,6 +126,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 compared in F# structural equality. The removed functions did not remove the elements that satisfy a predicate: use `filter` with the negated predicate for that. * `FlatList.removeRange`: use `removeManyAt`. +* `HashMap.length`, `append` and `except`: use `count`, `addRange` and `removeRange`. `addRange pairs table` takes the + bindings before the hash map, where `append map pairs` took the hash map first. +* `HashMap.check`: no replacement is needed, because every function of the module throws `ArgumentNullException` for a + `null` hash map. +* The `HashMapBuilder<'Key, 'Value>` type abbreviation: use `HashMap<'Key, 'Value>.Builder`. ## [1.0.0] - 2020-02-23 diff --git a/README.md b/README.md index 950057e1..9c1a830c 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,8 @@ Please also join the [F# Open Source Group](http://fsharp.github.com) | `ImmutableList` | `ImmutableList<'T>` | – | | `Stack` | `ImmutableStack<'T>` | – | | `Queue` | `ImmutableQueue<'T>` | – | -| `HashMap` | `ImmutableDictionary<'Key, 'Value>` | – | +| `HashMap` | `ImmutableDictionary<'Key, 'Value>` | `hashMap { }` | +| `IMap` | `IImmutableDictionary<'Key, 'Value>` | – | | `SortedMap` | `ImmutableSortedDictionary<'Key, 'Value>` | – | | `HashSet` | `ImmutableHashSet<'T>` | – | | `SortedSet` | `ImmutableSortedSet<'T>` | – | diff --git a/docsSrc/Explanations/Background.md b/docsSrc/Explanations/Background.md index 822820fa..d5cfbb4a 100644 --- a/docsSrc/Explanations/Background.md +++ b/docsSrc/Explanations/Background.md @@ -30,7 +30,8 @@ While F# has its own built-in immutable collections, System.Collections.Immutabl | `ImmutableList` | `ImmutableList<'T>` | – | | `Stack` | `ImmutableStack<'T>` | – | | `Queue` | `ImmutableQueue<'T>` | – | -| `HashMap` | `ImmutableDictionary<'Key, 'Value>` | – | +| `HashMap` | `ImmutableDictionary<'Key, 'Value>` | `hashMap { }` | +| `IMap` | `IImmutableDictionary<'Key, 'Value>` | – | | `SortedMap` | `ImmutableSortedDictionary<'Key, 'Value>` | – | | `HashSet` | `ImmutableHashSet<'T>` | – | | `SortedSet` | `ImmutableSortedSet<'T>` | – | diff --git a/docsSrc/Tutorials/Getting_Started.md b/docsSrc/Tutorials/Getting_Started.md index eea423e8..bfef8a26 100644 --- a/docsSrc/Tutorials/Getting_Started.md +++ b/docsSrc/Tutorials/Getting_Started.md @@ -27,13 +27,19 @@ let list1 = ImmutableList.ofSeq [1; 2; 3] // Create an immutable hash set let set1 = HashSet.ofSeq ["a"; "b"] -// Create an immutable dictionary -let dict1 = HashMap.ofSeq [KeyValuePair ("key1", 42); KeyValuePair ("key2", 100)] +// Create an immutable dictionary from tuples, key/value pairs or struct tuples +let dict1 = HashMap.ofSeq [ "key1", 42; "key2", 100 ] + +// Create an immutable dictionary whose keys ignore the case; the later binding of "KEY1" wins +let dict2 = (HashMap.forKeyComparer System.StringComparer.OrdinalIgnoreCase).ofSeq [ "key1", 42; "KEY1", 100 ] printfn "List: %A" (list1 |> Seq.toList) printfn "Set: %A" (set1 |> Seq.toList) printfn "Dict: %A" (dict1 |> Seq.toList) +printfn "Dict ignoring the case: %A" (dict2 |> Seq.toList) +``` + ## Next Steps - Check out the [How-To Guides](../How_Tos/Doing_A_Thing.html) for common scenarios diff --git a/docsSrc/index.md b/docsSrc/index.md index 276ad8fe..3b8f9b32 100644 --- a/docsSrc/index.md +++ b/docsSrc/index.md @@ -12,7 +12,8 @@ F# bindings for [System.Collections.Immutable](https://learn.microsoft.com/en-us | `ImmutableList` | `ImmutableList<'T>` | – | | `Stack` | `ImmutableStack<'T>` | – | | `Queue` | `ImmutableQueue<'T>` | – | -| `HashMap` | `ImmutableDictionary<'Key, 'Value>` | – | +| `HashMap` | `ImmutableDictionary<'Key, 'Value>` | `hashMap { }` | +| `IMap` | `IImmutableDictionary<'Key, 'Value>` | – | | `SortedMap` | `ImmutableSortedDictionary<'Key, 'Value>` | – | | `HashSet` | `ImmutableHashSet<'T>` | – | | `SortedSet` | `ImmutableSortedSet<'T>` | – | From e60e687e2eb072527a431256109b6209e113628a Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Wed, 21 Oct 2026 03:52:50 +0200 Subject: [PATCH 4/9] refactor: the `Tuple` and `Option` modules of `HashMap` and `IMap` call the default modules by their short names Each of these files opens `FSharp.Collections.Immutable`, and inside `FSharp.Collections.Immutable.Tuple.HashMap` the name `HashMap` reaches the default module, also for the functions that both modules define, so the full namespace in front of it added nothing. The compiled code is the same: the opcodes and the called methods of every method of both builds match one to one. Co-Authored-By: Claude Sonnet 5.5 --- .../HashMap/HashMapOption.fs | 23 ++++++------------- .../HashMap/HashMapTuple.fs | 2 +- .../HashMap/IMapOption.fs | 20 ++++++---------- .../HashMap/IMapTuple.fs | 2 +- 4 files changed, 16 insertions(+), 31 deletions(-) diff --git a/src/FSharp.Collections.Immutable/HashMap/HashMapOption.fs b/src/FSharp.Collections.Immutable/HashMap/HashMapOption.fs index 90d29890..98c502a4 100644 --- a/src/FSharp.Collections.Immutable/HashMap/HashMapOption.fs +++ b/src/FSharp.Collections.Immutable/HashMap/HashMapOption.fs @@ -9,40 +9,31 @@ open FSharp.Collections.Immutable // ---- Access ---- [] -let tryFind (key : 'Key) (table : HashMap<'Key, 'Value>) = - FSharp.Collections.Immutable.HashMap.tryFind key table - |> ValueOption.toOption +let tryFind (key : 'Key) (table : HashMap<'Key, 'Value>) = HashMap.tryFind key table |> ValueOption.toOption [] -let tryGetKey (key : 'Key) (table : HashMap<'Key, 'Value>) = - FSharp.Collections.Immutable.HashMap.tryGetKey key table - |> ValueOption.toOption +let tryGetKey (key : 'Key) (table : HashMap<'Key, 'Value>) = HashMap.tryGetKey key table |> ValueOption.toOption // ---- Search ---- [] let pick (chooser : 'Key -> 'Value -> 'U option) (table : HashMap<'Key, 'Value>) = - FSharp.Collections.Immutable.HashMap.pick (fun key value -> chooser key value |> ValueOption.ofOption) table + HashMap.pick (fun key value -> chooser key value |> ValueOption.ofOption) table [] -let tryFindKey predicate (table : HashMap<'Key, 'Value>) = - FSharp.Collections.Immutable.HashMap.tryFindKey predicate table - |> ValueOption.toOption +let tryFindKey predicate (table : HashMap<'Key, 'Value>) = HashMap.tryFindKey predicate table |> ValueOption.toOption [] let tryPick (chooser : 'Key -> 'Value -> 'U option) (table : HashMap<'Key, 'Value>) = - FSharp.Collections.Immutable.HashMap.tryPick (fun key value -> chooser key value |> ValueOption.ofOption) table + HashMap.tryPick (fun key value -> chooser key value |> ValueOption.ofOption) table |> ValueOption.toOption // ---- Transformation ---- [] let change (key : 'Key) (f : 'Value option -> 'Value option) (table : HashMap<'Key, 'Value>) = - FSharp.Collections.Immutable.HashMap.change - key - (fun current -> f (ValueOption.toOption current) |> ValueOption.ofOption) - table + HashMap.change key (fun current -> f (ValueOption.toOption current) |> ValueOption.ofOption) table [] let choose (chooser : 'Key -> 'Value -> 'U option) (table : HashMap<'Key, 'Value>) = - FSharp.Collections.Immutable.HashMap.choose (fun key value -> chooser key value |> ValueOption.ofOption) table + HashMap.choose (fun key value -> chooser key value |> ValueOption.ofOption) table diff --git a/src/FSharp.Collections.Immutable/HashMap/HashMapTuple.fs b/src/FSharp.Collections.Immutable/HashMap/HashMapTuple.fs index c3172dca..98c2ab31 100644 --- a/src/FSharp.Collections.Immutable/HashMap/HashMapTuple.fs +++ b/src/FSharp.Collections.Immutable/HashMap/HashMapTuple.fs @@ -10,7 +10,7 @@ open FSharp.Collections.Immutable [] let partition predicate (table : HashMap<'Key, 'Value>) = - let struct (accepted, rejected) = FSharp.Collections.Immutable.HashMap.partition predicate table + let struct (accepted, rejected) = HashMap.partition predicate table accepted, rejected // ---- Conversion ---- diff --git a/src/FSharp.Collections.Immutable/HashMap/IMapOption.fs b/src/FSharp.Collections.Immutable/HashMap/IMapOption.fs index 7692dcaa..2a5532a8 100644 --- a/src/FSharp.Collections.Immutable/HashMap/IMapOption.fs +++ b/src/FSharp.Collections.Immutable/HashMap/IMapOption.fs @@ -9,37 +9,31 @@ open FSharp.Collections.Immutable // ---- Access ---- [] -let tryFind (key : 'Key) (table : IMap<'Key, 'Value>) = - FSharp.Collections.Immutable.IMap.tryFind key table - |> ValueOption.toOption +let tryFind (key : 'Key) (table : IMap<'Key, 'Value>) = IMap.tryFind key table |> ValueOption.toOption [] -let tryGetKey (key : 'Key) (table : IMap<'Key, 'Value>) = - FSharp.Collections.Immutable.IMap.tryGetKey key table - |> ValueOption.toOption +let tryGetKey (key : 'Key) (table : IMap<'Key, 'Value>) = IMap.tryGetKey key table |> ValueOption.toOption // ---- Search ---- [] let pick (chooser : 'Key -> 'Value -> 'U option) (table : IMap<'Key, 'Value>) = - FSharp.Collections.Immutable.IMap.pick (fun key value -> chooser key value |> ValueOption.ofOption) table + IMap.pick (fun key value -> chooser key value |> ValueOption.ofOption) table [] -let tryFindKey predicate (table : IMap<'Key, 'Value>) = - FSharp.Collections.Immutable.IMap.tryFindKey predicate table - |> ValueOption.toOption +let tryFindKey predicate (table : IMap<'Key, 'Value>) = IMap.tryFindKey predicate table |> ValueOption.toOption [] let tryPick (chooser : 'Key -> 'Value -> 'U option) (table : IMap<'Key, 'Value>) = - FSharp.Collections.Immutable.IMap.tryPick (fun key value -> chooser key value |> ValueOption.ofOption) table + IMap.tryPick (fun key value -> chooser key value |> ValueOption.ofOption) table |> ValueOption.toOption // ---- Transformation ---- [] let change (key : 'Key) (f : 'Value option -> 'Value option) (table : IMap<'Key, 'Value>) = - FSharp.Collections.Immutable.IMap.change key (fun current -> f (ValueOption.toOption current) |> ValueOption.ofOption) table + IMap.change key (fun current -> f (ValueOption.toOption current) |> ValueOption.ofOption) table [] let choose (chooser : 'Key -> 'Value -> 'U option) (table : IMap<'Key, 'Value>) = - FSharp.Collections.Immutable.IMap.choose (fun key value -> chooser key value |> ValueOption.ofOption) table + IMap.choose (fun key value -> chooser key value |> ValueOption.ofOption) table diff --git a/src/FSharp.Collections.Immutable/HashMap/IMapTuple.fs b/src/FSharp.Collections.Immutable/HashMap/IMapTuple.fs index 1b15dfc5..dc9e306e 100644 --- a/src/FSharp.Collections.Immutable/HashMap/IMapTuple.fs +++ b/src/FSharp.Collections.Immutable/HashMap/IMapTuple.fs @@ -10,7 +10,7 @@ open FSharp.Collections.Immutable [] let partition predicate (table : IMap<'Key, 'Value>) = - let struct (accepted, rejected) = FSharp.Collections.Immutable.IMap.partition predicate table + let struct (accepted, rejected) = IMap.partition predicate table accepted, rejected // ---- Conversion ---- From 6b919d7b8f16273db150a0da5c4dc543df6e34e5 Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Sun, 8 Nov 2026 23:14:10 +0100 Subject: [PATCH 5/9] fixup! test: shared contracts of the map modules, run for `HashMap` and `IMap` A documentation comment with a tag starts with an explicit `` (the project instructions): the compiler escapes the tags of a comment that has no ``. The two cases of `MapImplementation`, the two cases of `SpanCreation` and `MapInputs.RepeatedKeys` had comments with `` or `` and no ``. The tests check the size of a collection with `Assert.HasCount`, `Assert.IsEmpty` and `Assert.ContainsSingle`, as the instructions ask, and not with `Assert.AreEqual` of its count. The 13 assertions are in the tests of the builders, of the creation with comparers, of the computation expression, of `HashMap` and of `IMap`; they report the bindings when they fail. Co-Authored-By: Claude Sonnet 5.5 --- .../Contracts/MapInputs.fs | 2 ++ .../HashMap/CustomComparersHashMapTests.fs | 6 +++--- .../HashMap/HashMapBuilderTests.fs | 13 +++++++------ .../HashMap/HashMapComputationExpressionTests.fs | 2 +- .../HashMap/HashMapTests.fs | 7 ++++--- .../HashMap/IMapTests.fs | 9 +++++---- 6 files changed, 22 insertions(+), 17 deletions(-) diff --git a/tests/FSharp.Collections.Immutable.Tests/Contracts/MapInputs.fs b/tests/FSharp.Collections.Immutable.Tests/Contracts/MapInputs.fs index 568cef6b..adad89cb 100644 --- a/tests/FSharp.Collections.Immutable.Tests/Contracts/MapInputs.fs +++ b/tests/FSharp.Collections.Immutable.Tests/Contracts/MapInputs.fs @@ -53,7 +53,9 @@ type MapInputs = MapInputs.Several } + /// /// Bindings that repeat keys with other values; in the FSharp.Core Map module the later binding of a key wins. + /// static member RepeatedKeys = [| struct (1, "one") struct (2, "two") diff --git a/tests/FSharp.Collections.Immutable.Tests/HashMap/CustomComparersHashMapTests.fs b/tests/FSharp.Collections.Immutable.Tests/HashMap/CustomComparersHashMapTests.fs index 6bdbb030..649a7272 100644 --- a/tests/FSharp.Collections.Immutable.Tests/HashMap/CustomComparersHashMapTests.fs +++ b/tests/FSharp.Collections.Immutable.Tests/HashMap/CustomComparersHashMapTests.fs @@ -354,7 +354,7 @@ type CustomComparersHashMapTests () = "The hash map of HashMap.forKeyComparer must have the key comparer" ) - Assert.AreEqual(2, HashMap.count table, "The comparer must merge the keys that differ in their case") + Assert.HasCount (2, table, "The comparer must merge the keys that differ in their case") Assert.AreEqual(2, HashMap.find "One" table, "The hash map must keep the last binding of keys that are equal") Assert.AreEqual(3, HashMap.find "TWO" table, "The hash map must look the keys up with the comparer") @@ -505,7 +505,7 @@ type CustomComparersHashMapTests () = "ofSeq must give the hash map the key comparer of the struct" ) - Assert.AreEqual(2, HashMap.count ignoreCase, "ofSeq must merge the keys that the comparer of the struct makes equal") + Assert.HasCount (2, ignoreCase, "ofSeq must merge the keys that the comparer of the struct makes equal") Assert.AreEqual( last, @@ -523,7 +523,7 @@ type CustomComparersHashMapTests () = |> Array.find (fun struct (key, _) -> String.Equals (key, "a", StringComparison.OrdinalIgnoreCase)) let ignoreCase = (HashMap.forComparers StringComparer.OrdinalIgnoreCase byLength).ofSeq table - Assert.AreEqual(2, HashMap.count ignoreCase, "ofSeq must merge the keys that the comparer of the struct makes equal") + Assert.HasCount (2, ignoreCase, "ofSeq must merge the keys that the comparer of the struct makes equal") Assert.AreEqual( ValueSome firstKey, diff --git a/tests/FSharp.Collections.Immutable.Tests/HashMap/HashMapBuilderTests.fs b/tests/FSharp.Collections.Immutable.Tests/HashMap/HashMapBuilderTests.fs index 143f1d19..305c1cb8 100644 --- a/tests/FSharp.Collections.Immutable.Tests/HashMap/HashMapBuilderTests.fs +++ b/tests/FSharp.Collections.Immutable.Tests/HashMap/HashMapBuilderTests.fs @@ -103,13 +103,13 @@ type HashMapBuilderTests () = "HashMap.update must keep the key comparer" ) - Assert.AreEqual(2, HashMap.count table, "HashMap.update must not change the input") + Assert.HasCount (2, table, "HashMap.update must not change the input") Assert.IsTrue (HashMap.containsKey "one" table, "HashMap.update must not change the input") [] member _.``builder starts empty with the default comparers`` () = let builder = HashMap.builder() - Assert.AreEqual(0, builder.Count, "HashMap.builder must start without bindings") + Assert.IsEmpty (builder, "HashMap.builder must start without bindings") Assert.AreSame>( EqualityComparer.Default, builder.KeyComparer, @@ -139,7 +139,8 @@ type HashMapBuilderTests () = (HashMap.forKeyComparer StringComparer.OrdinalIgnoreCase).builder() builder["one"] <- 1 builder["ONE"] <- 2 - Assert.AreEqual(1, builder.Count, "The builder of HashMap.forKeyComparer must look the keys up with the comparer") + Assert.ContainsSingle (builder, "The builder of HashMap.forKeyComparer must look the keys up with the comparer") + |> ignore Assert.AreSame>( EqualityComparer.Default, @@ -156,7 +157,7 @@ type HashMapBuilderTests () = builder["two"] <- 2 CollectionAssert.AreEqual ([| struct ("one", 1) |], HashMap.toArray table, "HashMap.ofBuilder must return the bindings") - Assert.AreEqual(2, builder.Count, "HashMap.ofBuilder must keep the builder usable") + Assert.HasCount (2, builder, "HashMap.ofBuilder must keep the builder usable") Assert.AreSame>( StringComparer.OrdinalIgnoreCase, HashMap.keyComparer table, @@ -184,13 +185,13 @@ type HashMapBuilderTests () = builder["THREE"] <- 3 builder.Remove "ONE" |> ignore - Assert.AreEqual(2, builder.Count, "HashMap.toBuilder must hold the bindings and use the key comparer") + Assert.HasCount (2, builder, "HashMap.toBuilder must hold the bindings and use the key comparer") Assert.AreSame>( StringComparer.OrdinalIgnoreCase, builder.KeyComparer, "HashMap.toBuilder must keep the key comparer" ) - Assert.AreEqual(2, HashMap.count table, "HashMap.toBuilder must not change the map") + Assert.HasCount (2, table, "HashMap.toBuilder must not change the map") Assert.IsTrue (HashMap.containsKey "one" table, "HashMap.toBuilder must not change the map") [] diff --git a/tests/FSharp.Collections.Immutable.Tests/HashMap/HashMapComputationExpressionTests.fs b/tests/FSharp.Collections.Immutable.Tests/HashMap/HashMapComputationExpressionTests.fs index 76f4a356..282ec9b8 100644 --- a/tests/FSharp.Collections.Immutable.Tests/HashMap/HashMapComputationExpressionTests.fs +++ b/tests/FSharp.Collections.Immutable.Tests/HashMap/HashMapComputationExpressionTests.fs @@ -336,7 +336,7 @@ type HashMapComputationExpressionTests () = HashMap.keyComparer table, "HashMapCE must keep the key comparer" ) - Assert.AreEqual(2, HashMap.count table, "HashMapCE must merge the keys that the comparer considers equal") + Assert.HasCount (2, table, "HashMapCE must merge the keys that the comparer considers equal") Assert.AreEqual(2, HashMap.find "One" table, "HashMapCE must keep the last binding of equal keys") [] diff --git a/tests/FSharp.Collections.Immutable.Tests/HashMap/HashMapTests.fs b/tests/FSharp.Collections.Immutable.Tests/HashMap/HashMapTests.fs index 1de11fbe..4c8efdfa 100644 --- a/tests/FSharp.Collections.Immutable.Tests/HashMap/HashMapTests.fs +++ b/tests/FSharp.Collections.Immutable.Tests/HashMap/HashMapTests.fs @@ -14,9 +14,9 @@ open Microsoft.VisualStudio.TestTools.UnitTesting /// [] type SpanCreation = - /// ofReadOnlySpan, which takes a ReadOnlySpan. + /// ofReadOnlySpan, which takes a ReadOnlySpan. | OfReadOnlySpan - /// ofSpan, which takes a Span. + /// ofSpan, which takes a Span. | OfSpan /// Every function, as the rows of the tests that run once per function. @@ -569,7 +569,8 @@ type HashMapTests () = |> ignore let table = HashMap.ofSeqBy (fun (value : int) -> string value) [| 1; 1 |] - Assert.AreEqual(1, HashMap.count table, "HashMap.ofSeqBy must keep one binding of a repeated element") + Assert.ContainsSingle (table, "HashMap.ofSeqBy must keep one binding of a repeated element") + |> ignore [] member _.``ofSeqBy keeps the first of the elements of a key that the default comparer finds equal`` () = diff --git a/tests/FSharp.Collections.Immutable.Tests/HashMap/IMapTests.fs b/tests/FSharp.Collections.Immutable.Tests/HashMap/IMapTests.fs index a59b2135..971e2a04 100644 --- a/tests/FSharp.Collections.Immutable.Tests/HashMap/IMapTests.fs +++ b/tests/FSharp.Collections.Immutable.Tests/HashMap/IMapTests.fs @@ -11,9 +11,9 @@ open Microsoft.VisualStudio.TestTools.UnitTesting /// [] type MapImplementation = - /// An . + /// An . | Hash - /// An . + /// An . | Sorted /// Every implementation, as the rows of the tests that run once per implementation. @@ -382,10 +382,11 @@ type IMapTests () = let table : IMap = (HashMap.forKeyComparer HashIdentity.Reference).ofArray [| struct (first, 1); struct (second, 2) |] - Assert.AreEqual(2, IMap.count table, "The keys must be distinct in the reference comparer of the map") + Assert.HasCount (2, table, "The keys must be distinct in the reference comparer of the map") let struct (_, lastValue) = IMap.toArray table |> Array.last let mapped = IMap.map (fun _ value -> value * 10) table - Assert.AreEqual(1, HashMap.count mapped, "IMap.map must merge the keys that the default comparer considers equal") + Assert.ContainsSingle (mapped, "IMap.map must merge the keys that the default comparer considers equal") + |> ignore Assert.AreEqual( lastValue * 10, HashMap.find "ab" mapped, From a59cc8afca7af3dfa007188becf1ba4c1747bf01 Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Sun, 8 Nov 2026 23:14:10 +0100 Subject: [PATCH 6/9] fixup! feat: `HashMap` and `IMap` modules with the vocabulary of the FSharp.Core `Map` module The helpers of the branch get the documentation of `Arrays` and `ParallelArrays` (the review of #40 and #31): an explicit ``, ``, `` and `` for every function, `` for every function of FSharp.Core, and a remark that says why the function exists. - `Dictionaries`: the summaries, the descriptions of the parameters and of the results are those of the functions of the FSharp.Core `Map` module where the signatures agree. Each remark says what differs: the FSharp.Core function takes an F# map, the bindings here are the pairs of a dictionary of the BCL, `tryFindKey` and `tryPick` use value options, `foldBack` copies the bindings because an enumerator moves only forwards, `choose`, `map`, `filter` and `partition` fill a target that the caller creates with the comparers it wants, and `toArray`, `toList` and `toSeq` take the constructor of the tuples, for the default and the `Tuple` modules. `setAll` says that the indexer keeps the stored binding for a value that the value comparer finds equal (ADR 0001, D14). `choose`, `splitKeyValuePair`, `splitTuple`, `setAll` and `toKeyValuePairs` say that FSharp.Core has no such function. - `Raise.keyNotFound`: the exception, the result and the difference from `indexNotFound`. No code changed: without the doc comments, both files are identical to the files before. Co-Authored-By: Claude Sonnet 5.5 --- .../Helper/Dictionaries.fs | 383 +++++++++++++++++- .../Helper/Raise.fs | 18 +- 2 files changed, 377 insertions(+), 24 deletions(-) diff --git a/src/FSharp.Collections.Immutable/Helper/Dictionaries.fs b/src/FSharp.Collections.Immutable/Helper/Dictionaries.fs index 9ce38262..dc38ba41 100644 --- a/src/FSharp.Collections.Immutable/Helper/Dictionaries.fs +++ b/src/FSharp.Collections.Immutable/Helper/Dictionaries.fs @@ -3,25 +3,57 @@ /// /// /// -/// The algorithms visit the bindings in the order in which the dictionary enumerates them, and the callers check their +/// The functions of the FSharp.Core module take an F# map, and the +/// maps of this library are dictionaries of the BCL, which enumerate their bindings as key/value pairs. Each algorithm here +/// does what its function of does over such a sequence of pairs. The +/// algorithms visit the bindings in the order in which the dictionary enumerates them, and the callers check their /// arguments first. They take the interfaces of the dictionaries, so that a map module, its interface module and its -/// Option and Tuple modules share one implementation. +/// and modules share +/// one implementation. /// /// /// The algorithms that build or take tuples take the constructor or the deconstructor of the tuples, so that the default -/// modules and the Tuple modules share them and neither pays for a second pass. +/// modules and the modules of the namespace share them and neither pays +/// for a second pass. /// /// /// A binding comes in three shapes (D13): a key/value pair, a reference tuple and a struct tuple. The algorithms that take -/// bindings take the deconstructor of their shape, splitKeyValuePair, splitTuple or id, so that -/// every map module and every comparer struct creates and changes its maps with one algorithm for the three shapes. +/// bindings take the deconstructor of their shape, +/// , +/// +/// or , so that every map +/// module and every comparer struct creates and changes its maps with one algorithm for the three shapes. +/// +/// +/// The summary of each function is the summary of the function of +/// word for word (FSharp.Core 10.1), and so are the descriptions of +/// the parameters and of the result where the signatures agree. The remarks say what differs and why the function exists. /// /// module internal FSharp.Collections.Immutable.Dictionaries open System.Collections.Generic -/// Whether a binding satisfies the predicate; stops at the first one that does. +/// +/// Returns true if the given predicate returns true for one of the bindings in the map. +/// +/// The function to test the input elements. +/// The bindings of the dictionary. +/// True if the predicate returns true for one of the key/value pairs. +/// +/// +/// This is an O(n) operation in the worst case, where n is the number of bindings in the map. The loop stops at the first +/// binding that satisfies the predicate. +/// +/// +/// It exists because +/// +/// takes an F# map, and a dictionary of the BCL is not one. The loop calls the curried +/// predicate with the key and the value of each pair. +/// +/// +/// +/// let exists (predicate : 'Key -> 'Value -> bool) (pairs : seq>) = use enumerator = pairs.GetEnumerator () let mutable found = false @@ -32,7 +64,25 @@ let exists (predicate : 'Key -> 'Value -> bool) (pairs : seq +/// Returns true if the given predicate returns true for all of the bindings in the map. +/// +/// The function to test the input elements. +/// The bindings of the dictionary. +/// True if the predicate evaluates to true for all of the bindings in the map. +/// +/// +/// This is an O(n) operation, where n is the number of bindings in the map. The loop stops at the first binding that does +/// not satisfy the predicate. +/// +/// +/// It exists because +/// +/// takes an F# map, and a dictionary of the BCL is not one. +/// +/// +/// +/// let forall (predicate : 'Key -> 'Value -> bool) (pairs : seq>) = use enumerator = pairs.GetEnumerator () let mutable holds = true @@ -43,7 +93,30 @@ let forall (predicate : 'Key -> 'Value -> bool) (pairs : seq +/// Returns the key of the first mapping in the collection that satisfies the given predicate. Returns ValueNone if +/// no such element exists. +/// +/// The function to test the input elements. +/// The bindings of the dictionary. +/// +/// The first key for which the predicate returns true or ValueNone if the predicate evaluates to false for each +/// key/value pair. +/// +/// +/// +/// This is an O(n) operation in the worst case, where n is the number of bindings in the map. +/// +/// +/// It exists because +/// +/// takes an F# map and returns an option, where the functions of this library return a +/// value option (ADR 0001, D3). The modules of the namespace convert the +/// result. +/// +/// +/// +/// let tryFindKey (predicate : 'Key -> 'Value -> bool) (pairs : seq>) = use enumerator = pairs.GetEnumerator () let mutable result = ValueNone @@ -56,7 +129,25 @@ let tryFindKey (predicate : 'Key -> 'Value -> bool) (pairs : seq +/// Searches the map looking for the first element where the given function returns a ValueSome value. +/// +/// The function to generate value options from the key/value pairs. +/// The bindings of the dictionary. +/// The first result, or ValueNone if the function returns ValueNone for each key/value pair. +/// +/// +/// This is an O(n) operation in the worst case, where n is the number of bindings in the map. +/// +/// +/// It exists because +/// +/// takes an F# map and a function that returns an option, where the functions of this +/// library take and return value options (ADR 0001, D3). +/// +/// +/// +/// let tryPick (chooser : 'Key -> 'Value -> 'U voption) (pairs : seq>) = use enumerator = pairs.GetEnumerator () let mutable result = ValueNone @@ -67,7 +158,26 @@ let tryPick (chooser : 'Key -> 'Value -> 'U voption) (pairs : seq +/// Folds over the bindings in the map +/// +/// The function to update the state given the input key/value pairs. +/// The initial state. +/// The bindings of the dictionary. +/// The final state value. +/// +/// +/// This is an O(n) operation, where n is the number of bindings in the map. The folder sees the bindings in the order in +/// which the dictionary enumerates them. +/// +/// +/// It exists because +/// +/// takes an F# map, and a dictionary of the BCL is not one. +/// +/// +/// +/// let fold (folder : 'State -> 'Key -> 'Value -> 'State) (state : 'State) (pairs : seq>) = let mutable state = state @@ -76,7 +186,31 @@ let fold (folder : 'State -> 'Key -> 'Value -> 'State) (state : 'State) (pairs : state -/// Applies the folder to each binding and the state, in the reverse order of enumeration. +/// +/// Folds over the bindings in the map. +/// +/// The function to update the state given the input key/value pairs. +/// The bindings of the dictionary. +/// The initial state. +/// The final state value. +/// +/// +/// This is an O(n) operation, where n is the number of bindings in the map. The folder sees the bindings in the reverse +/// order of the enumeration of the dictionary. For a dictionary that enumerates its keys in ascending order, such as a +/// sorted one, that is from the greatest key, as +/// +/// folds. A hash map has no order of its keys, so it folds +/// from the last binding that it enumerates. +/// +/// +/// It exists because +/// +/// takes an F# map, and a dictionary of the BCL is not one. An enumerator moves only +/// forwards, so the function copies the bindings into an array first. +/// +/// +/// +/// let foldBack (folder : 'Key -> 'Value -> 'State -> 'State) (pairs : seq>) (state : 'State) = // An enumerator only moves forwards, so the bindings are copied first let bindings = Seq.toArray pairs @@ -88,31 +222,127 @@ let foldBack (folder : 'Key -> 'Value -> 'State -> 'State) (pairs : seq +/// Applies the given function to each binding in the dictionary +/// +/// The function to apply to each key/value pair. +/// The bindings of the dictionary. +/// +/// +/// This is an O(n) operation, where n is the number of bindings in the map. The action sees the bindings in the order in +/// which the dictionary enumerates them. +/// +/// +/// It exists because +/// +/// takes an F# map, and a dictionary of the BCL is not one. +/// +/// +/// +/// let iter (action : 'Key -> 'Value -> unit) (pairs : seq>) = for pair in pairs do action pair.Key pair.Value -/// Sets the value that the chooser returns for a binding under its key in the target. +/// +/// Sets the value that the chooser returns for a binding under its key in the target, for each binding for which the +/// chooser returns a value. +/// +/// The function to generate value options from the key/value pairs. +/// The dictionary that receives the chosen values, empty when the function starts. +/// The bindings of the dictionary. +/// +/// +/// This is an O(n) operation, where n is the number of bindings in the map. +/// +/// +/// FSharp.Core has no such function for maps, so there is no summary to repeat. The function fills a target that the caller +/// creates, instead of returning a map, so that the caller picks the comparers of the result: the keys keep the comparer of +/// the input, and the values, which change their type, get the default comparer of their type (ADR 0001, D5, Comparers). +/// +/// +/// +/// let choose (chooser : 'Key -> 'Value -> 'U voption) (target : IDictionary<'Key, 'U>) (pairs : seq>) = for pair in pairs do match chooser pair.Key pair.Value with | ValueSome value -> target[pair.Key] <- value | ValueNone -> () -/// Sets the result of the mapping of each binding under its key in the target. +/// +/// Sets the result of the application of the given function to each binding under the key of the binding in the target. The +/// key passed to the function indicates the key of element being transformed. +/// +/// The function to transform the key/value pairs. +/// The dictionary that receives the transformed values, empty when the function starts. +/// The bindings of the dictionary. +/// +/// +/// This is an O(n) operation, where n is the number of bindings in the map, creating the bindings of a new map with the same +/// keys. +/// +/// +/// It exists because +/// +/// takes an F# map and returns one. The function fills a target that the caller creates, so that +/// the keys keep the comparer of the input and the values, which change their type, get the default comparer of their type +/// (ADR 0001, D5, Comparers). +/// +/// +/// +/// let map (mapping : 'Key -> 'Value -> 'U) (target : IDictionary<'Key, 'U>) (pairs : seq>) = for pair in pairs do target[pair.Key] <- mapping pair.Key pair.Value -/// Removes the bindings that do not satisfy the predicate from a target that starts with every binding. -/// The target is a builder over the same map, so that it shares what stays and returns the map itself when nothing goes. +/// +/// Removes the bindings for which the given predicate returns false from the target, which starts with every binding of the +/// map. +/// +/// The function to test the key/value pairs. +/// The dictionary to filter: a builder over the map that enumerates. +/// The bindings of the dictionary. +/// +/// +/// This is an O(n) operation, where n is the number of bindings in the map, plus the cost of the removals. +/// +/// +/// It exists because +/// +/// takes an F# map and builds a new one. The target is a builder over the same map, so that +/// the result shares what stays and the builder returns the map itself when nothing goes. +/// +/// +/// let filter (predicate : 'Key -> 'Value -> bool) (target : IDictionary<'Key, 'Value>) (pairs : seq>) = for pair in pairs do if not (predicate pair.Key pair.Value) then target.Remove pair.Key |> ignore -/// Moves the bindings that do not satisfy the predicate from a target that starts with every binding to an empty one. +/// +/// Moves the bindings for which the given predicate returns false from the first target, which starts with every binding of +/// the map, to the second one, which starts empty. +/// +/// The function to test the input elements. +/// +/// The dictionary of the bindings for which the predicate returns true: a builder over the map that +/// enumerates. +/// +/// The dictionary of the remaining bindings: an empty builder with the comparers of the map. +/// The bindings of the dictionary. +/// +/// +/// This is an O(n) operation, where n is the number of bindings in the map, plus the cost of the removals and of the +/// insertions. +/// +/// +/// It exists because +/// +/// takes an F# map and builds two new ones. The first target is a builder over the same +/// map, so that the first result shares what stays and the builder returns the map itself when every binding stays. +/// +/// +/// let partition (predicate : 'Key -> 'Value -> bool) (accepted : IDictionary<'Key, 'Value>) @@ -124,14 +354,54 @@ let partition accepted.Remove pair.Key |> ignore rejected[pair.Key] <- pair.Value +/// /// The key and the value of a key/value pair, the shape of the bindings that every .NET dictionary enumerates. +/// +/// The key/value pair. +/// The key and the value, as a struct tuple. +/// +/// FSharp.Core has no such function. It is one of the three deconstructors (D13) that the algorithms of this module take, +/// next to +/// +/// and . +/// let inline splitKeyValuePair (pair : KeyValuePair<'Key, 'Value>) = struct (pair.Key, pair.Value) +/// /// The key and the value of a reference tuple, the shape of the bindings of most F# code. +/// +/// The first item of the tuple. +/// The second item of the tuple. +/// The key and the value, as a struct tuple. +/// +/// FSharp.Core has no such function. It is one of the three deconstructors (D13) that the algorithms of this module take, +/// next to +/// +/// and . +/// let inline splitTuple (key : 'Key, value : 'Value) = struct (key, value) -/// Sets each element, split into a key and a value, in the target; a later binding of a key replaces an earlier one, -/// as in `Map.ofSeq`. +/// +/// Sets each element, split into a key and a value, in the target; a later binding of a key replaces an earlier one, as in +/// . +/// +/// The deconstructor of the shape of the elements. +/// The dictionary that receives the bindings. +/// The bindings, in any of the three shapes. +/// +/// +/// This is an O(n) operation, where n is the number of elements. +/// +/// +/// A later binding replaces an earlier one through the indexer of the target. When the value comparer of the target finds the +/// new value equal to the stored one, the indexer of the builder of +/// leaves the stored binding as it is, key included, and so does this function (ADR 0001, D14). +/// +/// +/// It exists because the three shapes of a binding (D13) need one algorithm, not three: the creation members of the comparer +/// structs and of the extensions pass the deconstructor of their shape. +/// +/// let inline setAll ([] split : 'Element -> struct ('Key * 'Value)) (target : IDictionary<'Key, 'Value>) @@ -141,7 +411,20 @@ let inline setAll let struct (key, value) = split element target[key] <- value +/// /// The elements, split into keys and values, as key/value pairs, computed on demand. +/// +/// The deconstructor of the shape of the elements. +/// The bindings, in any of the three shapes. +/// The key/value pairs, in the order of the elements; a pair is created when the sequence is read. +/// +/// FSharp.Core has no such function. It exists because the dictionaries of the BCL take key/value pairs, where the callers +/// receive bindings in the shape of their choice (D13): +/// +/// and +/// +/// take the result. +/// let inline toKeyValuePairs ([] split : 'Element -> struct ('Key * 'Value)) (elements : seq<'Element>) @@ -152,7 +435,27 @@ let inline toKeyValuePairs KeyValuePair (key, value) ) -/// The bindings as an array of tuples, in the order of enumeration. +/// +/// Returns an array of all key-value pairs in the mapping. +/// +/// The function that builds an element of the result from a key and a value. +/// The bindings of the dictionary. +/// The array of key/value pairs, in the order in which the dictionary enumerates them. +/// +/// +/// This is an O(n) operation, where n is the number of bindings in the map. The array has the size of the map from the +/// start, because the map knows its count. +/// +/// +/// It exists because +/// +/// takes an F# map and returns reference tuples. The default function of a map module +/// returns struct tuples, and the function of the namespace returns +/// reference tuples, so each passes the constructor of its tuples and neither pays for a second pass. +/// +/// +/// +/// let inline toArray ([] tuple : 'Key -> 'Value -> 'R) (pairs : IReadOnlyCollection>) @@ -166,11 +469,47 @@ let inline toArray result -/// The bindings as a list of tuples, in the order of enumeration. +/// +/// Returns a list of all key-value pairs in the mapping. +/// +/// The function that builds an element of the result from a key and a value. +/// The bindings of the dictionary. +/// The list of key/value pairs, in the order in which the dictionary enumerates them. +/// +/// +/// This is an O(n) operation, where n is the number of bindings in the map. +/// +/// +/// It exists because +/// +/// takes an F# map and returns reference tuples, for the reason given for +/// . +/// +/// +/// +/// let inline toList ([] tuple : 'Key -> 'Value -> 'R) (pairs : seq>) : 'R list = [ for pair in pairs -> tuple pair.Key pair.Value ] -/// The bindings as a sequence of tuples, in the order of enumeration, computed on demand. +/// +/// Views the collection as an enumerable sequence of pairs. +/// +/// The function that builds an element of the result from a key and a value. +/// The bindings of the dictionary. +/// The sequence of key/value pairs, in the order in which the dictionary enumerates them; computed on demand. +/// +/// +/// This is an O(n) operation, where n is the number of bindings in the map, when the sequence is read. +/// +/// +/// It exists because +/// +/// takes an F# map and returns reference tuples, for the reason given for +/// . +/// +/// +/// +/// let inline toSeq ([] tuple : 'Key -> 'Value -> 'R) (pairs : seq>) : seq<'R> = pairs |> Seq.map (fun pair -> tuple pair.Key pair.Value) diff --git a/src/FSharp.Collections.Immutable/Helper/Raise.fs b/src/FSharp.Collections.Immutable/Helper/Raise.fs index d31cdbc0..e2f570b6 100644 --- a/src/FSharp.Collections.Immutable/Helper/Raise.fs +++ b/src/FSharp.Collections.Immutable/Helper/Raise.fs @@ -65,8 +65,22 @@ let invalidOpListNotEnoughElements (index : int) : 'T = /// No element satisfies the predicate of a `find` or `pick` function. let indexNotFound () : 'T = raise (KeyNotFoundException ErrorStrings.KeyNotFoundAlt) -/// A map has no binding of a key, or no binding gives a value to `pick`: `Map.find` and `Map.pick` throw the exception -/// with its default message. +/// +/// Raises the exception of a map that has no binding of a key, or whose bindings give a chooser no value, as +/// and +/// do. +/// +/// Never: the function always raises. The result is generic so that the call can end a branch of any type. +/// +/// Always, with the default message of the exception: +/// and +/// +/// give it none of their own. +/// +/// +/// It is not , whose message is the one that the +/// functions of arrays and lists give to a search that finds nothing. +/// let keyNotFound () : 'T = raise (KeyNotFoundException ()) /// A collection has fewer elements than a function needs. From f58b40fcab748b8af0ddb49041a4c64264731744 Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Sun, 8 Nov 2026 23:30:44 +0100 Subject: [PATCH 7/9] fixup! feat: `HashMap` and `IMap` modules with the vocabulary of the FSharp.Core `Map` module The signature documentation refers to the functions and the types of FSharp.Core, of the BCL and of the library with ``, not with `` (the review of #31): `Map.add` (six places), `ImmutableInterlocked` and `ImmutableInterlocked.TryAdd` in `HashMap.fsi`, and `HashMap.tryFind` and `IMap.tryFind`. The names that stay in `` are the code samples, the compiled names of the extension members, and the names of the overloaded families `ofArray`, `ofList` and `ofSeq`, which have no single member to refer to. No code changed: without the doc comments, both files are identical to the files before. Co-Authored-By: Claude Sonnet 5.5 --- .../HashMap/HashMap.fsi | 14 +++++++------- src/FSharp.Collections.Immutable/HashMap/IMap.fsi | 8 ++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/FSharp.Collections.Immutable/HashMap/HashMap.fsi b/src/FSharp.Collections.Immutable/HashMap/HashMap.fsi index b7e8c7a3..735d2a1a 100644 --- a/src/FSharp.Collections.Immutable/HashMap/HashMap.fsi +++ b/src/FSharp.Collections.Immutable/HashMap/HashMap.fsi @@ -7,7 +7,7 @@ /// its semantics, its argument checks and its exceptions (ADR 0001), except /// . /// It is the of the BCL, as it was in 1.0.0: -/// it throws for a key that is bound to another value, where Map.add replaces the value, and +/// it throws for a key that is bound to another value, where replaces the value, and /// /// replaces it (ADR 0001, D14). The other functions wrap a member of /// and keep its semantics and its exceptions. @@ -36,9 +36,9 @@ /// The single-key idioms of the BCL are functions of the module or compositions of them. TryGetValue is /// , /// and GetValueOrDefault is HashMap.tryFind key table |> ValueOption.defaultValue defaultValue. Adding a -/// binding only when the key has none, as ImmutableInterlocked.TryAdd does, is +/// binding only when the key has none, as does, is /// -/// with a function that keeps an existing value. The other members of ImmutableInterlocked replace a field +/// with a function that keeps an existing value. The other members of replace a field /// atomically and stay direct calls with a reference to the field. /// /// @@ -459,7 +459,7 @@ val tryGetKey : key : 'Key -> table : HashMap<'Key, 'Value> -> 'Key voption /// /// /// The value comparer of the map decides whether the values are equal. No composition of the other functions asks it: -/// HashMap.tryFind followed by = compares with F# equality, which for a comparer that ignores the case finds +/// followed by = compares with F# equality, which for a comparer that ignores the case finds /// "Hello" and "HELLO" different. /// /// @@ -584,7 +584,7 @@ val tryPick : chooser : ('Key -> 'Value -> 'U voption) -> table : HashMap<'Key, /// /// O(log n). The function wraps and /// behaves as it does (ADR 0001, D14), as the 1.0.0 function did: it does not replace the value of a key, where -/// Map.add does. When the value comparer of the hash map finds the value equal to the current value of the key, the +/// does. When the value comparer of the hash map finds the value equal to the current value of the key, the /// binding stays as it is, its key included, and the input itself comes back. /// /// @@ -639,7 +639,7 @@ val addRange : pairs : seq> -> table : HashMap<'Key, /// /// /// Adding a binding only when the key has none is HashMap.change key (function ValueNone -> ValueSome value | existing -> existing), -/// which the BCL offers as ImmutableInterlocked.TryAdd for a field. +/// which the BCL offers as for a field. /// /// [] @@ -729,7 +729,7 @@ val removeRange : keys : seq<'Key> -> table : HashMap<'Key, 'Value> -> HashMap<' /// Thrown when is . /// /// O(log n). The function wraps and -/// replaces the value of a key, as Map.add does, where +/// replaces the value of a key, as does, where /// /// throws for a key that is bound to another value. When the value comparer of the hash map finds the value equal to the /// current value of the key, the binding stays as it is, its key included (ADR 0001, D14). diff --git a/src/FSharp.Collections.Immutable/HashMap/IMap.fsi b/src/FSharp.Collections.Immutable/HashMap/IMap.fsi index a59440f5..01f8dd0b 100644 --- a/src/FSharp.Collections.Immutable/HashMap/IMap.fsi +++ b/src/FSharp.Collections.Immutable/HashMap/IMap.fsi @@ -7,7 +7,7 @@ /// its semantics, its argument checks and its exceptions (ADR 0001), except /// . /// It is the of the interface: it throws for -/// a key that is bound to another value, where Map.add replaces the value, and +/// a key that is bound to another value, where replaces the value, and /// /// replaces it (ADR 0001, D14). The other functions wrap a member of /// and keep its semantics and its exceptions. @@ -170,7 +170,7 @@ val tryGetKey : key : 'Key -> table : IMap<'Key, 'Value> -> 'Key voption /// /// /// The interface hides the value comparer of the map, so this is the only function that tests a binding with it: finding -/// the value with IMap.tryFind and comparing with = uses F# equality instead. +/// the value with and comparing with = uses F# equality instead. /// /// [] @@ -275,7 +275,7 @@ val tryPick : chooser : ('Key -> 'Value -> 'U voption) -> table : IMap<'Key, 'Va /// O(log n) for the immutable dictionaries. The function wraps /// and behaves as it does (ADR 0001, D14), /// as -/// does: it does not replace the value of a key, where Map.add does. When the value comparer of the map finds the +/// does: it does not replace the value of a key, where does. When the value comparer of the map finds the /// value equal to the current value of the key, the binding stays as it is, its key included, and the input itself comes /// back. /// @@ -427,7 +427,7 @@ val removeRange : keys : seq<'Key> -> table : IMap<'Key, 'Value> -> IMap<'Key, ' /// /// O(log n) for the immutable dictionaries. The function wraps /// and replaces the value of a key, as -/// Map.add does, where +/// does, where /// /// throws for a key that is bound to another value. When the value comparer of an immutable dictionary finds the value /// equal to the current value of the key, the dictionary leaves the binding as it is, its key included, and returns the From 84e054b3213530605d0a94072a1def176a7766cf Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Sun, 8 Nov 2026 23:38:11 +0100 Subject: [PATCH 8/9] fixup! test: shared contracts of the map modules, run for `HashMap` and `IMap` The comments that got an explicit `` in the previous fixup refer to the functions, the properties and the types of FSharp.Core, of the BCL and of the library with ``, not with `` (the project instructions, the review of #31 and the review of CodeRabbit on #30). The `` that stay name code samples and the types of the test project, which have no documentation file to refer to. No code changed: without the doc comments, the files are identical to the files before. Co-Authored-By: Claude Sonnet 5.5 --- .../Contracts/MapInputs.fs | 9 +++++++-- .../HashMap/HashMapTests.fs | 10 ++++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/tests/FSharp.Collections.Immutable.Tests/Contracts/MapInputs.fs b/tests/FSharp.Collections.Immutable.Tests/Contracts/MapInputs.fs index adad89cb..a16e24b4 100644 --- a/tests/FSharp.Collections.Immutable.Tests/Contracts/MapInputs.fs +++ b/tests/FSharp.Collections.Immutable.Tests/Contracts/MapInputs.fs @@ -8,7 +8,10 @@ open System.Collections.Generic /// /// /// -/// The bindings map keys to values. The FSharp.Core Map +/// The bindings map +/// keys to +/// values. The FSharp.Core +/// /// module computes the expectations. A hash map enumerates its bindings in an unspecified order, so the contracts compare /// the bindings sorted by key, and compute the expectations of the functions that visit the bindings one by one over the /// order in which the map under test enumerates them (ADR 0001, D8). @@ -54,7 +57,9 @@ type MapInputs = } /// - /// Bindings that repeat keys with other values; in the FSharp.Core Map module the later binding of a key wins. + /// Bindings that repeat keys with other values; in the FSharp.Core + /// + /// module the later binding of a key wins. /// static member RepeatedKeys = [| struct (1, "one") diff --git a/tests/FSharp.Collections.Immutable.Tests/HashMap/HashMapTests.fs b/tests/FSharp.Collections.Immutable.Tests/HashMap/HashMapTests.fs index 4c8efdfa..8c443b35 100644 --- a/tests/FSharp.Collections.Immutable.Tests/HashMap/HashMapTests.fs +++ b/tests/FSharp.Collections.Immutable.Tests/HashMap/HashMapTests.fs @@ -14,9 +14,15 @@ open Microsoft.VisualStudio.TestTools.UnitTesting /// [] type SpanCreation = - /// ofReadOnlySpan, which takes a ReadOnlySpan. + /// + /// , which takes a + /// . + /// | OfReadOnlySpan - /// ofSpan, which takes a Span. + /// + /// , which takes a + /// . + /// | OfSpan /// Every function, as the rows of the tests that run once per function. From 33338e9dd3ae1012a10d7736e2cb062ac94ddfb6 Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Sun, 8 Nov 2026 23:43:39 +0100 Subject: [PATCH 9/9] fixup! feat: `HashMap` and `IMap` modules with the vocabulary of the FSharp.Core `Map` module The remarks of `Dictionaries.choose` and `Dictionaries.map` said that the keys keep the comparer of the input. That holds for `HashMap.choose` and `HashMap.map`, which create the target with `KeyComparer` of the table. `IMap.choose` and `IMap.map` cannot read the key comparer through the interface and create the target with the default one. The remarks now say so. No code changed: without the doc comments, the file is identical to the file before. Co-Authored-By: Claude Sonnet 5.5 --- .../Helper/Dictionaries.fs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/FSharp.Collections.Immutable/Helper/Dictionaries.fs b/src/FSharp.Collections.Immutable/Helper/Dictionaries.fs index dc38ba41..411406e5 100644 --- a/src/FSharp.Collections.Immutable/Helper/Dictionaries.fs +++ b/src/FSharp.Collections.Immutable/Helper/Dictionaries.fs @@ -257,8 +257,9 @@ let iter (action : 'Key -> 'Value -> unit) (pairs : seq /// /// FSharp.Core has no such function for maps, so there is no summary to repeat. The function fills a target that the caller -/// creates, instead of returning a map, so that the caller picks the comparers of the result: the keys keep the comparer of -/// the input, and the values, which change their type, get the default comparer of their type (ADR 0001, D5, Comparers). +/// creates, instead of returning a map, so that the caller picks the comparers of the result. The values change their type, +/// so they get the default comparer of their type (ADR 0001, D5, Comparers). The keys keep the comparer of the input in the +/// function of a hash map; the function of the interface cannot read that comparer and uses the default one. /// /// /// @@ -285,8 +286,9 @@ let choose (chooser : 'Key -> 'Value -> 'U voption) (target : IDictionary<'Key, /// It exists because /// /// takes an F# map and returns one. The function fills a target that the caller creates, so that -/// the keys keep the comparer of the input and the values, which change their type, get the default comparer of their type -/// (ADR 0001, D5, Comparers). +/// the caller picks the comparers of the result. The values change their type, so they get the default comparer of their +/// type (ADR 0001, D5, Comparers). The keys keep the comparer of the input in the function of a hash map; the function of +/// the interface cannot read that comparer and uses the default one. /// /// ///