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>` | – | 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..98c502a4 --- /dev/null +++ b/src/FSharp.Collections.Immutable/HashMap/HashMapOption.fs @@ -0,0 +1,39 @@ +[] +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>) = HashMap.tryFind 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>) = + HashMap.pick (fun key value -> chooser key value |> ValueOption.ofOption) table + +[] +let tryFindKey predicate (table : HashMap<'Key, 'Value>) = HashMap.tryFindKey predicate table |> ValueOption.toOption + +[] +let tryPick (chooser : 'Key -> 'Value -> 'U option) (table : HashMap<'Key, 'Value>) = + 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>) = + HashMap.change key (fun current -> f (ValueOption.toOption current) |> ValueOption.ofOption) table + +[] +let choose (chooser : 'Key -> 'Value -> 'U option) (table : HashMap<'Key, 'Value>) = + 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..98c2ab31 --- /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) = 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..2a5532a8 --- /dev/null +++ b/src/FSharp.Collections.Immutable/HashMap/IMapOption.fs @@ -0,0 +1,39 @@ +[] +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>) = IMap.tryFind 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>) = + IMap.pick (fun key value -> chooser key value |> ValueOption.ofOption) table + +[] +let tryFindKey predicate (table : IMap<'Key, 'Value>) = IMap.tryFindKey predicate table |> ValueOption.toOption + +[] +let tryPick (chooser : 'Key -> 'Value -> 'U option) (table : IMap<'Key, 'Value>) = + 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>) = + IMap.change key (fun current -> f (ValueOption.toOption current) |> ValueOption.ofOption) table + +[] +let choose (chooser : 'Key -> 'Value -> 'U option) (table : IMap<'Key, 'Value>) = + 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..dc9e306e --- /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) = 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 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