diff --git a/CHANGELOG.md b/CHANGELOG.md index 7872abdb..e54fd442 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -88,6 +88,56 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 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`. +* `FrozenMap` wraps `FrozenDictionary<'Key, 'Value>`, a map that is built once and read many times. It has the functions of + the FSharp.Core `Map` module that read a map or transform the whole map, with their semantics and exceptions, plus + `choose`, `containsValue` and `singleton`, and the wrappers `containsBinding`, `copyTo`, `keyComparer`, `ofReadOnlySpan`, + `ofSeqBy` and `ofSpan` of the members of `FrozenDictionary`, which keep the semantics of the BCL. It has no function that + adds or removes a binding: `choose`, `filter`, `map` and `partition` build a new frozen map with the key comparer of the + input. `keys` and `values` return flat lists over the arrays of the map, without a copy, and `FlatList.asSpan` views them + as spans. +* `FrozenMap.containsBinding` tells whether a key is bound to a value, as `Contains` of a `KeyValuePair` does. A frozen map + has no value comparer, so it compares the value with `EqualityComparer<'Value>.Default`, as `containsValue` does. + `tryFind` followed by `=` compares with F# equality, which differs for some types: `nan = nan` is `false`. +* `FrozenMap.ofReadOnlySpan` and `FrozenMap.ofSpan` create a frozen map from a `ReadOnlySpan>` or + a `Span>` with `FrozenDictionary.Create`, and the members `ofReadOnlySpan` and `ofSpan` of + `CustomComparersFrozenMap` do so with its key comparer. A repeated key never throws: the last binding wins and the first + key stays. A `Span` or an array converts to the parameter of `ofReadOnlySpan` only when the types of the call are known; + `ofSpan` takes a `Span` where they are not. +* `FrozenMap.copyTo` copies the bindings into an array of `KeyValuePair`s from an index, as `CopyTo` does, and throws what + it throws: `ArgumentNullException` for a `null` array, `ArgumentOutOfRangeException` for an index below zero or beyond + the length of the array, and `ArgumentException` when the array has no room for the bindings. +* `FrozenMap.ofArray`, `FrozenMap.ofList` and `FrozenMap.ofSeq` take the bindings as `KeyValuePair`s, reference tuples or + struct tuples: they are static members of `FrozenDictionary<'Key, 'Value>` in the auto-opened `FrozenMapExtensions` + module, one overload per shape, and every shape keeps the last binding of a repeated key, as `Map.ofSeq` does: the value + of the last binding, and the key of the first one when the comparer finds distinct keys equal, as `ToFrozenDictionary` + does. They take a dictionary, an F# `Map` or another frozen map too. C# does not see them as members of + `FrozenDictionary`; it calls the public static methods `FrozenMapExtensions.OfArray`, `OfList` and `OfSeq`, or the members + of the struct below. A reflection over the `FrozenMap` module does not list them. +* `FrozenMap.forKeyComparer` returns a `CustomComparersFrozenMap<'Key, 'Value>` struct whose members `empty`, `ofArray`, + `ofList`, `ofReadOnlySpan`, `ofSeq`, `ofSeqBy`, `ofSpan` and `singleton` create frozen maps with its key comparer: + `(FrozenMap.forKeyComparer StringComparer.OrdinalIgnoreCase).ofSeq pairs`. A `null` comparer throws + `ArgumentNullException`. +* `ofSeq` of that struct also gives an existing frozen map another key comparer: + `(FrozenMap.forKeyComparer comparer).ofSeq table`. A frozen map whose comparer equals the key comparer of the struct comes + back itself, as `ToFrozenDictionary` does. Any other frozen map is rebuilt, and keys that the new comparer makes equal + merge: the first key stays, with the value of the last binding. +* The single-key idioms of the BCL are functions of the module: `tryFind` is `TryGetValue`, and `tryFind` followed by + `ValueOption.defaultValue` is `GetValueOrDefault`. `ToFrozenDictionary` with an element selector has no function. To build + the same map, pass the elements mapped to `KeyValuePair`s to `FrozenMap.ofSeq`, which keeps the last binding of a repeated + key where the BCL rejects every repeated key; `FrozenMap.ofSeqBy` rejects them like the BCL, with the elements as the + values. `GetValueRefOrNullRef` and the indexer return a reference, which a function cannot: `find` and `tryFind` cover the + lookup. +* On `net10.0`, `FrozenMap.alternateLookup` and `FrozenMap.tryAlternateLookup` look the bindings up by keys of another type + through `FrozenDictionary.GetAlternateLookup` and `FrozenDictionary.TryGetAlternateLookup`; the type of the alternate + keys is their first type argument: `FrozenMap.alternateLookup table`. The BCL members come with .NET 9 and the + package has no `net9.0` target, so the two functions are not on `net8.0`. +* The `FSharp.Collections.Immutable.Option` and `FSharp.Collections.Immutable.Tuple` namespaces have `FrozenMap` modules + that repeat the functions that use value options or return struct tuples with options and reference tuples. +* `frozenMap { }` builds a frozen map from `yield struct (key, value)` and `yield KeyValuePair (key, value)`; `FrozenMapCE` + takes a key comparer, and a `null` one throws `ArgumentNullException`. +* `FrozenMap` keeps the convention of `FrozenDictionary` where it differs from `Map`: a later binding of a key replaces the + value of an earlier one and leaves its key, `ofSeqBy` rejects every repeated key, and `ofSeq` of a frozen map whose + comparer equals the requested one returns that map. ### Changed diff --git a/README.md b/README.md index 9c1a830c..5ffa1b93 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ Please also join the [F# Open Source Group](http://fsharp.github.com) | `HashMap` | `ImmutableDictionary<'Key, 'Value>` | `hashMap { }` | | `IMap` | `IImmutableDictionary<'Key, 'Value>` | – | | `SortedMap` | `ImmutableSortedDictionary<'Key, 'Value>` | – | +| `FrozenMap` | `FrozenDictionary<'Key, 'Value>` | `frozenMap { }` | | `HashSet` | `ImmutableHashSet<'T>` | – | | `SortedSet` | `ImmutableSortedSet<'T>` | – | | `IIndexedSeq` | `IReadOnlyList<'T>` | – | diff --git a/docsSrc/Explanations/Background.md b/docsSrc/Explanations/Background.md index d5cfbb4a..c7d2451d 100644 --- a/docsSrc/Explanations/Background.md +++ b/docsSrc/Explanations/Background.md @@ -33,6 +33,7 @@ While F# has its own built-in immutable collections, System.Collections.Immutabl | `HashMap` | `ImmutableDictionary<'Key, 'Value>` | `hashMap { }` | | `IMap` | `IImmutableDictionary<'Key, 'Value>` | – | | `SortedMap` | `ImmutableSortedDictionary<'Key, 'Value>` | – | +| `FrozenMap` | `FrozenDictionary<'Key, 'Value>` | `frozenMap { }` | | `HashSet` | `ImmutableHashSet<'T>` | – | | `SortedSet` | `ImmutableSortedSet<'T>` | – | | `IIndexedSeq` | `IReadOnlyList<'T>` | – | diff --git a/docsSrc/index.md b/docsSrc/index.md index 3b8f9b32..9b8f0a08 100644 --- a/docsSrc/index.md +++ b/docsSrc/index.md @@ -15,6 +15,7 @@ F# bindings for [System.Collections.Immutable](https://learn.microsoft.com/en-us | `HashMap` | `ImmutableDictionary<'Key, 'Value>` | `hashMap { }` | | `IMap` | `IImmutableDictionary<'Key, 'Value>` | – | | `SortedMap` | `ImmutableSortedDictionary<'Key, 'Value>` | – | +| `FrozenMap` | `FrozenDictionary<'Key, 'Value>` | `frozenMap { }` | | `HashSet` | `ImmutableHashSet<'T>` | – | | `SortedSet` | `ImmutableSortedSet<'T>` | – | | `IIndexedSeq` | `IReadOnlyList<'T>` | – | diff --git a/src/FSharp.Collections.Immutable/ComputationExpressions.fs b/src/FSharp.Collections.Immutable/ComputationExpressions.fs index 5a17dfba..4f150016 100644 --- a/src/FSharp.Collections.Immutable/ComputationExpressions.fs +++ b/src/FSharp.Collections.Immutable/ComputationExpressions.fs @@ -5,5 +5,8 @@ module FSharp.Collections.Immutable.ComputationExpressions let flatList = FlatListCE () +[] +let frozenMap<'Key when 'Key : not null> = FrozenMapCE<'Key>() + [] 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 7cfa666f..b68d0558 100644 --- a/src/FSharp.Collections.Immutable/ComputationExpressions.fsi +++ b/src/FSharp.Collections.Immutable/ComputationExpressions.fsi @@ -20,6 +20,29 @@ module FSharp.Collections.Immutable.ComputationExpressions /// val flatList : FlatListCE +/// +/// Builds a frozen map with the default key comparer. +/// +/// +/// A later binding of a key replaces the value of an earlier one, and the key of the earlier binding stays, as in +/// +/// (ADR 0001, D14). The expression freezes its bindings once, when it runs. The instance is generic in the type of the +/// keys, which each expression infers; takes a key comparer. +/// +/// +/// +/// let squares = frozenMap { +/// for i in 1..3 do +/// yield struct (i, i * i) +/// yield KeyValuePair (10, 100) +/// yield struct (1, -1) +/// } +/// +/// Evaluates to a frozen map of 1 to -1, 2 to 4, 3 to 9 and 10 to 100. +/// +[] +val frozenMap<'Key when 'Key : not null> : FrozenMapCE<'Key> + /// /// Builds a hash map with the default comparers. /// diff --git a/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj b/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj index 2ed46392..c109cf1f 100644 --- a/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj +++ b/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj @@ -10,7 +10,7 @@ FSharp.Collections.Immutable FSharp.Collections.Immutable - F# bindings for System.Collections.Immutable + F# bindings for System.Collections.Immutable and System.Collections.Frozen @@ -72,6 +72,27 @@ + + + FrozenMap.fsi + + + FrozenMap.fs + + + + + + + + + FrozenMap.fsi + + + FrozenMap.fs + + + diff --git a/src/FSharp.Collections.Immutable/FrozenMap/FrozenMap.CustomComparers.fs b/src/FSharp.Collections.Immutable/FrozenMap/FrozenMap.CustomComparers.fs new file mode 100644 index 00000000..b29fa858 --- /dev/null +++ b/src/FSharp.Collections.Immutable/FrozenMap/FrozenMap.CustomComparers.fs @@ -0,0 +1,109 @@ +namespace FSharp.Collections.Immutable + +open System +open System.Collections.Frozen +open System.Collections.Generic + +// The struct holds the key comparer and creates frozen maps with it (ADR 0001, D12). A struct cannot run code in a primary +// constructor, so the constructor that checks the comparer is explicit. Every struct also has a default value that no +// constructor checks; its field is null, and the property gives it the default comparer, which the members use. +[] +type CustomComparersFrozenMap<'Key, 'Value when 'Key : not null> = + + val private keyComparer : IEqualityComparer<'Key> | null + + new (keyComparer : IEqualityComparer<'Key>) + = + checkNotNull (nameof keyComparer) keyComparer + { keyComparer = keyComparer } + + /// Sets the bindings, split into keys and values, one by one in a dictionary with the key comparer and freezes it, so + /// that a later binding of a key replaces the value of an earlier one, as in `Map.ofSeq`, and the earlier key stays, as + /// in ToFrozenDictionary (D14); every shape of the bindings creates its maps here (D13). A frozen map has no builder; + /// ToFrozenDictionary reads a dictionary whose comparer equals the given one directly, without building another one. + member inline private this.Build ([] split : 'Binding -> struct ('Key * 'Value), elements : seq<'Binding>) = + let comparer = this.KeyComparer + let bindings = Dictionary<'Key, 'Value> comparer + Dictionaries.setAll split bindings elements + FrozenDictionary.ToFrozenDictionary (bindings, comparer) + + // ---- Creation ---- + + [] + member this.empty : FrozenMap<'Key, 'Value> = + // An empty source gives FrozenDictionary.Empty for the default comparer and an empty map of the comparer otherwise + FrozenDictionary.ToFrozenDictionary (Array.Empty>(), this.KeyComparer) + + [] + 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>) : FrozenMap<'Key, 'Value> = + // Create reads the span into a dictionary with the comparer, so that a later binding of a key replaces the value of + // an earlier one and the earlier key stays, as for the other shapes (ADR 0001, D14) + FrozenDictionary.Create (this.KeyComparer, span) + + [] + member this.ofSeq (elements : seq>) = + // A default flat list given as a sequence fails when it is enumerated, so it is rejected as a null sequence is (D6) + checkNotNullOrDefaultSeq (nameof elements) elements + + match elements with + // The keys of a frozen map are distinct in its own comparer, so a map whose comparer equals the comparer of the + // struct comes back itself, as ToFrozenDictionary returns it (ADR 0001, D14); another comparer can make keys equal, + // and then the keys merge: the earlier key stays and the later value wins + | :? FrozenDictionary<'Key, 'Value> as table when table.Comparer.Equals this.KeyComparer -> table + | _ -> 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>) : FrozenMap<'Key, 'Value> = + checkNotNullOrDefaultSeq (nameof source) source + FrozenDictionary.ToFrozenDictionary (source, Func<'Value, 'Key> projection, this.KeyComparer) + + // Span.op_Implicit is the conversion that F# applies by itself where the type of the parameter is known, with the warning + // FS3391; calling it explicitly avoids the warning + [] + member this.ofSpan (span : Span>) : FrozenMap<'Key, 'Value> = + this.ofReadOnlySpan (Span>.op_Implicit span) + + [] + member this.singleton (key : 'Key) (value : 'Value) : FrozenMap<'Key, 'Value> = + FrozenDictionary.ToFrozenDictionary ([| KeyValuePair (key, value) |], this.KeyComparer) + + // ---- Properties ---- + + member this.KeyComparer : IEqualityComparer<'Key> = + match this.keyComparer with + | null -> EqualityComparer<'Key>.Default + | comparer -> comparer diff --git a/src/FSharp.Collections.Immutable/FrozenMap/FrozenMap.CustomComparers.fsi b/src/FSharp.Collections.Immutable/FrozenMap/FrozenMap.CustomComparers.fsi new file mode 100644 index 00000000..c3de5ca6 --- /dev/null +++ b/src/FSharp.Collections.Immutable/FrozenMap/FrozenMap.CustomComparers.fsi @@ -0,0 +1,247 @@ +namespace FSharp.Collections.Immutable + +open System +open System.Collections.Generic + +/// +/// The equality comparer of the keys of frozen maps, with the creation functions of +/// that create frozen maps with it (ADR 0001, D12). +/// +/// +/// +/// +/// creates the struct. Its members have the names of the creation functions of the module and the key comparer of the +/// struct. A frozen map has no comparer of its values and no builder, so the struct holds the key comparer alone and has no +/// builder. +/// +/// +/// ofArray, ofList and ofSeq take the bindings as key/value pairs, reference tuples or struct +/// tuples (ADR 0001, D13); ofReadOnlySpan and ofSpan take a span of key/value pairs. A later binding of a +/// key replaces the value of an earlier one, as in +/// , +/// and the key of the earlier binding stays, as in +/// +/// (ADR 0001, D14). The kept key shows when the comparer of the struct finds two distinct keys equal. ofSeq also +/// takes an existing map, because a map is a sequence of key/value pairs, and gives its bindings the key comparer of the +/// struct; a frozen map whose comparer equals that comparer comes back itself, as in ToFrozenDictionary. +/// +/// +/// The struct only holds the comparer, so creating it allocates nothing. Its default value holds no comparer and stands for +/// the default comparer, , which the BCL takes for a +/// comparer (ADR 0001, D14). +/// +/// +/// +/// +/// let table = (FrozenMap.forKeyComparer StringComparer.OrdinalIgnoreCase).ofSeq [ "one", 1; "ONE", 2; "two", 3 ] +/// +/// Evaluates to a frozen map whose keys ignore the case, with the key "one" bound to 2 and the key "two" bound to 3. +/// +[] +type CustomComparersFrozenMap<'Key, 'Value when 'Key : not null> = + + val private keyComparer : IEqualityComparer<'Key> | null + + /// Holds the equality comparer of the keys. + /// The equality comparer of the keys. + /// Thrown when is . + new : keyComparer : IEqualityComparer<'Key> -> CustomComparersFrozenMap<'Key, 'Value> + + // ---- Creation ---- + + /// + /// The empty frozen map with the key comparer. + /// + /// + /// O(1). The property wraps + /// , + /// which returns for the default comparer. + /// + [] + member empty : FrozenMap<'Key, 'Value> + + /// + /// Creates a frozen map with the key comparer from an array of key/value pairs. + /// + /// The input array of key/value pairs. + /// The frozen map of the bindings. + /// Thrown when is . + /// + /// O(n). A later binding of a key replaces the value of an earlier one, whose key stays (ADR 0001, D14). + /// + [] + member ofArray : elements : KeyValuePair<'Key, 'Value> array -> FrozenMap<'Key, 'Value> + + /// + /// Creates a frozen map with the key comparer from an array of reference tuples. + /// + /// The input array of key/value tuples. + /// The frozen map of the bindings. + /// Thrown when is . + /// + /// O(n). A later binding of a key replaces the value of an earlier one, whose key stays (ADR 0001, D14). + /// + [] + member ofArray : elements : ('Key * 'Value) array -> FrozenMap<'Key, 'Value> + + /// + /// Creates a frozen map with the key comparer from an array of struct tuples. + /// + /// The input array of key/value tuples. + /// The frozen map of the bindings. + /// Thrown when is . + /// + /// O(n). A later binding of a key replaces the value of an earlier one, whose key stays (ADR 0001, D14). + /// + [] + member ofArray : elements : struct ('Key * 'Value) array -> FrozenMap<'Key, 'Value> + + /// + /// Creates a frozen map with the key comparer from a list of key/value pairs. + /// + /// The input list of key/value pairs. + /// The frozen map of the bindings. + /// + /// O(n). A later binding of a key replaces the value of an earlier one, whose key stays (ADR 0001, D14). + /// + [] + member ofList : elements : KeyValuePair<'Key, 'Value> list -> FrozenMap<'Key, 'Value> + + /// + /// Creates a frozen map with the key comparer from a list of reference tuples. + /// + /// The input list of key/value tuples. + /// The frozen map of the bindings. + /// + /// O(n). A later binding of a key replaces the value of an earlier one, whose key stays (ADR 0001, D14). + /// + [] + member ofList : elements : ('Key * 'Value) list -> FrozenMap<'Key, 'Value> + + /// + /// Creates a frozen map with the key comparer from a list of struct tuples. + /// + /// The input list of key/value tuples. + /// The frozen map of the bindings. + /// + /// O(n). A later binding of a key replaces the value of an earlier one, whose key stays (ADR 0001, D14). + /// + [] + member ofList : elements : struct ('Key * 'Value) list -> FrozenMap<'Key, 'Value> + + /// + /// Creates a frozen map with the key comparer from a read-only span of key/value pairs. + /// + /// The input span of key/value pairs. + /// The frozen map of the bindings. + /// + /// + /// O(n). The member wraps + /// + /// and reads the span itself, where ofArray and ofSeq need an array or a sequence, which copies the span + /// first. + /// + /// + /// A repeated key never throws. A later binding of a key replaces the value of an earlier one, whose key stays (ADR 0001, + /// D14). A or an array converts to the parameter only when the type arguments of the struct + /// are known; where the call leaves them to inference, F# reports FS0001. ofSpan and ofArray take a span + /// and an array whatever the call leaves to inference. + /// + /// + [] + member ofReadOnlySpan : span : ReadOnlySpan> -> FrozenMap<'Key, 'Value> + + /// + /// Creates a frozen map with the key comparer from a sequence of key/value pairs, such as a dictionary or another map. + /// + /// The input sequence of key/value pairs. + /// + /// The frozen map of the bindings. A frozen map whose comparer equals the key comparer of the struct comes back itself, + /// as in + /// + /// (ADR 0001, D14). + /// + /// Thrown when is or a default flat list. + /// + /// O(n); O(1) for a frozen map whose comparer equals the key comparer of the struct, because its keys are distinct in + /// that comparer. A later binding of a key replaces the value of an earlier one, and the key of the earlier binding + /// stays (ADR 0001, D14): of the keys that only the comparer of the struct considers equal, the map keeps the first one + /// in the order of enumeration with the value of the last one. + /// + [] + member ofSeq : elements : seq> -> FrozenMap<'Key, 'Value> + + /// + /// Creates a frozen map with the key comparer from a sequence of reference tuples. + /// + /// The input sequence of key/value tuples. + /// The frozen map of the bindings. + /// Thrown when is or a default flat list. + /// + /// O(n). A later binding of a key replaces the value of an earlier one, whose key stays (ADR 0001, D14). + /// + [] + member ofSeq : elements : seq<'Key * 'Value> -> FrozenMap<'Key, 'Value> + + /// + /// Creates a frozen map with the key comparer from a sequence of struct tuples. + /// + /// The input sequence of key/value tuples. + /// The frozen map of the bindings. + /// Thrown when is or a default flat list. + /// + /// O(n). A later binding of a key replaces the value of an earlier one, whose key stays (ADR 0001, D14). + /// + [] + member ofSeq : elements : seq -> FrozenMap<'Key, 'Value> + + /// + /// Creates a frozen map with the key comparer 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 frozen map of the elements under their keys. + /// Thrown when is or a default flat list. + /// + /// Thrown when two elements have keys that are equal in the key comparer, even when the elements are equal. + /// + /// + /// O(n). The member wraps + /// , + /// which rejects every repeated key (ADR 0001, D14). + /// + [] + member ofSeqBy : projection : ('Value -> 'Key) -> source : seq<'Value> -> FrozenMap<'Key, 'Value> + + /// + /// Creates a frozen map with the key comparer from a span of key/value pairs. + /// + /// The input span of key/value pairs. + /// The frozen map of the bindings. + /// + /// O(n). The member is ofReadOnlySpan for a . A repeated key never throws. A later + /// binding of a key replaces the value of an earlier one, whose key stays (ADR 0001, D14). + /// + [] + member ofSpan : span : Span> -> FrozenMap<'Key, 'Value> + + /// Creates a frozen map of one binding with the key comparer. + /// The key. + /// The value. + /// The frozen map of the binding. + /// O(1). + [] + member singleton : key : 'Key -> value : 'Value -> FrozenMap<'Key, 'Value> + + // ---- Properties ---- + + /// + /// The equality comparer of the keys. + /// + /// + /// O(1). The default value of the struct holds no comparer and returns + /// , the comparer that the BCL takes for a + /// comparer (ADR 0001, D14). + /// + member KeyComparer : IEqualityComparer<'Key> diff --git a/src/FSharp.Collections.Immutable/FrozenMap/FrozenMap.Extensions.fs b/src/FSharp.Collections.Immutable/FrozenMap/FrozenMap.Extensions.fs new file mode 100644 index 00000000..5d6307aa --- /dev/null +++ b/src/FSharp.Collections.Immutable/FrozenMap/FrozenMap.Extensions.fs @@ -0,0 +1,42 @@ +[] +module FSharp.Collections.Immutable.FrozenMapExtensions + +open System.Collections.Frozen +open System.Collections.Generic + +// The members are the members of CustomComparersFrozenMap with the default comparer, so that every shape of the bindings +// has one implementation (ADR 0001, D13). + +/// The comparer struct with the default comparer of the keys (D5). +let private defaultComparers<'Key, 'Value when 'Key : not null> () = + CustomComparersFrozenMap<'Key, 'Value> EqualityComparer<'Key>.Default + +// The extended type repeats the constraint of the BCL type (FS0957) +type FrozenDictionary<'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/FrozenMap/FrozenMap.Extensions.fsi b/src/FSharp.Collections.Immutable/FrozenMap/FrozenMap.Extensions.fsi new file mode 100644 index 00000000..e23eb434 --- /dev/null +++ b/src/FSharp.Collections.Immutable/FrozenMap/FrozenMap.Extensions.fsi @@ -0,0 +1,151 @@ +/// +/// 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 , FrozenMap.ofSeq pairs reaches +/// these members, because the module has no functions with their +/// names, and overload resolution picks the shape of the bindings; pairs |> FrozenMap.ofSeq works too. Inside generic +/// code, the bindings need a type annotation (FS0041). +/// +/// +/// The members create frozen maps with the default comparer, +/// : they are the members of +/// with that comparer. A later binding of a key +/// replaces the value of an earlier one, as in +/// , +/// and the key of the earlier binding stays, as in +/// +/// (ADR 0001, D14). +/// +/// +/// 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, FrozenMapExtensions.OfArray, OfList and OfSeq, +/// with one overload per shape. C# can call those, or the members of +/// , or the BCL. +/// +/// +[] +module FSharp.Collections.Immutable.FrozenMapExtensions + +open System.Collections.Frozen +open System.Collections.Generic + +// The extended type repeats the constraint of the BCL type (FS0957) +type FrozenDictionary<'Key, 'Value when 'Key : not null> with + + /// + /// Creates a frozen map from an array of key/value pairs. + /// + /// The input array of key/value pairs. + /// The frozen map of the bindings, with the default comparer. + /// Thrown when is . + /// + /// O(n). A later binding of a key replaces the value of an earlier one, whose key stays (ADR 0001, D14). + /// + [] + static member ofArray : elements : KeyValuePair<'Key, 'Value> array -> FrozenMap<'Key, 'Value> + + /// + /// Creates a frozen map from an array of reference tuples. + /// + /// The input array of key/value tuples. + /// The frozen map of the bindings, with the default comparer. + /// Thrown when is . + /// + /// O(n). A later binding of a key replaces the value of an earlier one, whose key stays (ADR 0001, D14). + /// + [] + static member ofArray : elements : ('Key * 'Value) array -> FrozenMap<'Key, 'Value> + + /// + /// Creates a frozen map from an array of struct tuples. + /// + /// The input array of key/value tuples. + /// The frozen map of the bindings, with the default comparer. + /// Thrown when is . + /// + /// O(n). A later binding of a key replaces the value of an earlier one, whose key stays (ADR 0001, D14). + /// + [] + static member ofArray : elements : struct ('Key * 'Value) array -> FrozenMap<'Key, 'Value> + + /// + /// Creates a frozen map from a list of key/value pairs. + /// + /// The input list of key/value pairs. + /// The frozen map of the bindings, with the default comparer. + /// + /// O(n). A later binding of a key replaces the value of an earlier one, whose key stays (ADR 0001, D14). + /// + [] + static member ofList : elements : KeyValuePair<'Key, 'Value> list -> FrozenMap<'Key, 'Value> + + /// + /// Creates a frozen map from a list of reference tuples. + /// + /// The input list of key/value tuples. + /// The frozen map of the bindings, with the default comparer. + /// + /// O(n). A later binding of a key replaces the value of an earlier one, whose key stays (ADR 0001, D14). + /// + [] + static member ofList : elements : ('Key * 'Value) list -> FrozenMap<'Key, 'Value> + + /// + /// Creates a frozen map from a list of struct tuples. + /// + /// The input list of key/value tuples. + /// The frozen map of the bindings, with the default comparer. + /// + /// O(n). A later binding of a key replaces the value of an earlier one, whose key stays (ADR 0001, D14). + /// + [] + static member ofList : elements : struct ('Key * 'Value) list -> FrozenMap<'Key, 'Value> + + /// + /// Creates a frozen map from a sequence of key/value pairs, such as a dictionary or another map. + /// + /// The input sequence of key/value pairs. + /// + /// The frozen map of the bindings, with the default comparer. A frozen map whose comparer equals the default comparer + /// comes back itself, as in + /// + /// (ADR 0001, D14). + /// + /// Thrown when is or a default flat list. + /// + /// O(n); O(1) for a frozen map whose comparer equals the default comparer. A later binding of a key replaces the value + /// of an earlier one, whose key stays (ADR 0001, D14): of the keys that only the default comparer considers equal, the + /// map keeps the first one in the order of enumeration with the value of the last one. + /// + [] + static member ofSeq : elements : seq> -> FrozenMap<'Key, 'Value> + + /// + /// Creates a frozen map from a sequence of reference tuples. + /// + /// The input sequence of key/value tuples. + /// The frozen map of the bindings, with the default comparer. + /// Thrown when is or a default flat list. + /// + /// O(n). A later binding of a key replaces the value of an earlier one, whose key stays (ADR 0001, D14). + /// + [] + static member ofSeq : elements : seq<'Key * 'Value> -> FrozenMap<'Key, 'Value> + + /// + /// Creates a frozen map from a sequence of struct tuples. + /// + /// The input sequence of key/value tuples. + /// The frozen map of the bindings, with the default comparer. + /// Thrown when is or a default flat list. + /// + /// O(n). A later binding of a key replaces the value of an earlier one, whose key stays (ADR 0001, D14). + /// + [] + static member ofSeq : elements : seq -> FrozenMap<'Key, 'Value> diff --git a/src/FSharp.Collections.Immutable/FrozenMap/FrozenMap.fs b/src/FSharp.Collections.Immutable/FrozenMap/FrozenMap.fs new file mode 100644 index 00000000..80a82ac4 --- /dev/null +++ b/src/FSharp.Collections.Immutable/FrozenMap/FrozenMap.fs @@ -0,0 +1,277 @@ +[] +module FSharp.Collections.Immutable.FrozenMap + +open System +open System.Collections.Frozen +open System.Collections.Generic + +// The functions call the members of FrozenDictionary where it has them (ADR 0001, D5) and visit the bindings with the +// algorithms that the map modules share (`Dictionaries` in Helper.fs). A frozen map has no builder, so the transformations +// collect the bindings in a Dictionary with the key comparer of the input and freeze it once. The functions that create a +// map from bindings are the members of CustomComparersFrozenMap (D12) and the static members of FrozenMapExtensions (D13); +// the module has none of their names, so that `FrozenMap.ofSeq` reaches the overloads of the extensions. + +/// Freezes the bindings of a dictionary. ToFrozenDictionary reads a dictionary whose comparer equals the given one +/// directly, without building another one, and the frozen map keeps that comparer. +let inline private freeze (comparer : IEqualityComparer<'Key>) (bindings : Dictionary<'Key, 'Value>) = + FrozenDictionary.ToFrozenDictionary (bindings, comparer) + +// ---- Creation ---- + +[] +[] +let empty<'Key, 'Value when 'Key : not null> : FrozenMap<'Key, 'Value> = + FrozenDictionary<'Key, 'Value>.Empty + +[] +let forKeyComparer (keyComparer : IEqualityComparer<'Key>) = CustomComparersFrozenMap<'Key, 'Value> keyComparer + +// A span has one shape, so there are no extension overloads (D13); the member of the struct with the default comparer wraps +// FrozenDictionary.Create (D12) +[] +let ofReadOnlySpan (span : ReadOnlySpan>) : FrozenMap<'Key, 'Value> = + (CustomComparersFrozenMap<'Key, 'Value> EqualityComparer<'Key>.Default).ofReadOnlySpan span + +[] +let ofSeqBy (projection : 'Value -> 'Key) (source : seq<'Value>) : FrozenMap<'Key, 'Value> = + checkNotNullOrDefaultSeq (nameof source) source + FrozenDictionary.ToFrozenDictionary (source, Func<'Value, 'Key> projection) + +[] +let ofSpan (span : Span>) : FrozenMap<'Key, 'Value> = + (CustomComparersFrozenMap<'Key, 'Value> EqualityComparer<'Key>.Default).ofSpan span + +[] +let singleton (key : 'Key) (value : 'Value) : FrozenMap<'Key, 'Value> = + FrozenDictionary.ToFrozenDictionary [| KeyValuePair (key, value) |] + +// ---- Properties ---- + +[] +let count (table : FrozenMap<'Key, 'Value>) = + checkNotNull (nameof table) table + table.Count + +[] +let isEmpty (table : FrozenMap<'Key, 'Value>) = + checkNotNull (nameof table) table + table.Count = 0 + +[] +let keyComparer (table : FrozenMap<'Key, 'Value>) = + checkNotNull (nameof table) table + table.Comparer + +[] +let keys (table : FrozenMap<'Key, 'Value>) : FlatList<'Key> = + checkNotNull (nameof table) table + // An immutable array over the keys that the map holds, in the order of its enumeration + table.Keys + +[] +let values (table : FrozenMap<'Key, 'Value>) : FlatList<'Value> = + checkNotNull (nameof table) table + table.Values + +// ---- Access ---- + +[] +let find (key : 'Key) (table : FrozenMap<'Key, 'Value>) = + checkNotNull (nameof table) table + + // The indexer of a frozen dictionary names the key in its message, which Map.find does not + match table.TryGetValue key with + | true, value -> value + | false, _ -> Raise.keyNotFound () + +[] +let tryFind (key : 'Key) (table : FrozenMap<'Key, 'Value>) = + checkNotNull (nameof table) table + + match table.TryGetValue key with + | true, value -> ValueSome value + | false, _ -> ValueNone + +// ---- Search ---- + +[] +let containsBinding (key : 'Key) (value : 'Value) (table : FrozenMap<'Key, 'Value>) = + checkNotNull (nameof table) table + // The member is an explicit implementation of ICollection. It compares the value with + // EqualityComparer.Default, as containsValue does, which tryFind followed by (=) does not reproduce for every type + (table :> ICollection>).Contains(KeyValuePair (key, value)) + +[] +let containsKey (key : 'Key) (table : FrozenMap<'Key, 'Value>) = + checkNotNull (nameof table) table + table.ContainsKey key + +[] +let containsValue (value : 'Value) (table : FrozenMap<'Key, 'Value>) = + checkNotNull (nameof table) table + // ImmutableArray.Contains compares with EqualityComparer.Default, as FrozenDictionary does for its values + table.Values.Contains value + +[] +let exists predicate (table : FrozenMap<'Key, 'Value>) = + checkNotNull (nameof table) table + Dictionaries.exists predicate table + +[] +let findKey predicate (table : FrozenMap<'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 : FrozenMap<'Key, 'Value>) = + checkNotNull (nameof table) table + Dictionaries.forall predicate table + +[] +let pick (chooser : 'Key -> 'Value -> 'U voption) (table : FrozenMap<'Key, 'Value>) = + checkNotNull (nameof table) table + + match Dictionaries.tryPick chooser table with + | ValueSome value -> value + | ValueNone -> Raise.keyNotFound () + +[] +let tryFindKey predicate (table : FrozenMap<'Key, 'Value>) = + checkNotNull (nameof table) table + Dictionaries.tryFindKey predicate table + +[] +let tryPick (chooser : 'Key -> 'Value -> 'U voption) (table : FrozenMap<'Key, 'Value>) = + checkNotNull (nameof table) table + Dictionaries.tryPick chooser table + +// ---- Transformation ---- + +// The keys of the input are distinct in its comparer, so the transformations size the dictionary for every binding + +[] +let choose (chooser : 'Key -> 'Value -> 'U voption) (table : FrozenMap<'Key, 'Value>) = + checkNotNull (nameof table) table + let bindings = Dictionary<'Key, 'U>(table.Count, table.Comparer) + Dictionaries.choose chooser bindings table + freeze table.Comparer bindings + +[] +let filter predicate (table : FrozenMap<'Key, 'Value>) = + checkNotNull (nameof table) table + let bindings = Dictionary<'Key, 'Value>(table.Count, table.Comparer) + + Dictionaries.choose (fun key value -> if predicate key value then ValueSome value else ValueNone) bindings table + + // Freezing is the expensive part, and the input already holds every binding + if bindings.Count = table.Count then + table + else + freeze table.Comparer bindings + +[] +let map (mapping : 'Key -> 'Value -> 'U) (table : FrozenMap<'Key, 'Value>) = + checkNotNull (nameof table) table + let bindings = Dictionary<'Key, 'U>(table.Count, table.Comparer) + Dictionaries.map mapping bindings table + freeze table.Comparer bindings + +// ---- Folding ---- + +[] +let fold (folder : 'State -> 'Key -> 'Value -> 'State) (state : 'State) (table : FrozenMap<'Key, 'Value>) = + checkNotNull (nameof table) table + Dictionaries.fold folder state table + +[] +let foldBack (folder : 'Key -> 'Value -> 'State -> 'State) (table : FrozenMap<'Key, 'Value>) (state : 'State) = + checkNotNull (nameof table) table + // The keys and the values are arrays in the order of enumeration, so they are read backwards without a copy + let keys = table.Keys + let values = table.Values + let mutable state = state + + for index = keys.Length - 1 downto 0 do + state <- folder keys[index] values[index] state + + state + +[] +let iter action (table : FrozenMap<'Key, 'Value>) = + checkNotNull (nameof table) table + Dictionaries.iter action table + +// ---- Partitioning ---- + +[] +let partition predicate (table : FrozenMap<'Key, 'Value>) = + checkNotNull (nameof table) table + // Without a builder over the input, the bindings are split into two new dictionaries in one pass + let accepted = Dictionary<'Key, 'Value> table.Comparer + let rejected = Dictionary<'Key, 'Value> table.Comparer + + for pair in table do + if predicate pair.Key pair.Value then + accepted.Add (pair.Key, pair.Value) + else + rejected.Add (pair.Key, pair.Value) + + // A part with every binding is the input itself, which saves freezing them again + let freezePart (part : Dictionary<'Key, 'Value>) = + if part.Count = table.Count then + table + else + freeze table.Comparer part + + struct (freezePart accepted, freezePart rejected) + +// ---- Conversion ---- + +[] +let copyTo (destination : KeyValuePair<'Key, 'Value> array) (destinationIndex : int) (table : FrozenMap<'Key, 'Value>) = + checkNotNull (nameof table) table + // The member checks the destination and the index, and throws for them as it does for its own arguments + table.CopyTo (destination, destinationIndex) + +[] +let toArray (table : FrozenMap<'Key, 'Value>) = + checkNotNull (nameof table) table + Dictionaries.toArray structPair table + +[] +let toList (table : FrozenMap<'Key, 'Value>) = + checkNotNull (nameof table) table + Dictionaries.toList structPair table + +[] +let toSeq (table : FrozenMap<'Key, 'Value>) = + checkNotNull (nameof table) table + Dictionaries.toSeq structPair table + +#if NET10_0_OR_GREATER +// ---- Type-specific ---- + +// The type of the alternate keys comes first, so that `alternateLookup` names it alone. +// An F# type parameter cannot declare `allows ref struct`, so these functions cannot take a span alternate (ADR 0001, D11). + +[] +let alternateLookup<'Alternate, 'Key, 'Value when 'Key : not null and 'Alternate : not null> + (table : FrozenMap<'Key, 'Value>) + : FrozenMap<'Key, 'Value>.AlternateLookup<'Alternate> = + checkNotNull (nameof table) table + table.GetAlternateLookup<'Alternate>() + +[] +let tryAlternateLookup<'Alternate, 'Key, 'Value when 'Key : not null and 'Alternate : not null> + (table : FrozenMap<'Key, 'Value>) + : FrozenMap<'Key, 'Value>.AlternateLookup<'Alternate> voption = + checkNotNull (nameof table) table + + match table.TryGetAlternateLookup<'Alternate>() with + | true, lookup -> ValueSome lookup + | false, _ -> ValueNone +#endif diff --git a/src/FSharp.Collections.Immutable/FrozenMap/FrozenMap.fsi b/src/FSharp.Collections.Immutable/FrozenMap/FrozenMap.fsi new file mode 100644 index 00000000..89f02541 --- /dev/null +++ b/src/FSharp.Collections.Immutable/FrozenMap/FrozenMap.fsi @@ -0,0 +1,659 @@ +/// +/// Functions for , the frozen map. +/// +/// +/// +/// The module mirrors : each function with a name of that module has +/// its semantics, its argument checks and its exceptions (ADR 0001). The other functions wrap a member of +/// or of +/// and keep its semantics and its exceptions. +/// +/// +/// A frozen map is built once and read many times, so the module has no function that adds or removes a binding. Each +/// transformation builds a new frozen map in O(n), which costs as much as the creation of a frozen map. +/// +/// +/// A frozen 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 frozen map compares its keys with the equality comparer that it was created with. The functions of the module create +/// frozen maps with the default comparer, ; +/// +/// returns a , which creates frozen maps with another +/// comparer (ADR 0001, D12). Every transformation keeps the key comparer. A frozen map has no comparer of its values, and +/// has no creation overload with a flag that optimizes the map +/// for reading, so no creation function takes either. +/// +/// +/// A binding comes as a key/value pair, a reference tuple or a struct tuple (ADR 0001, D13). FrozenMap.ofArray, +/// FrozenMap.ofList and FrozenMap.ofSeq take every shape: they are the static members of +/// , which the module leaves visible by having no functions +/// with their names. +/// +/// +/// Where the FSharp.Core Map module and differ in an edge +/// case, the module follows (ADR 0001, D14): a later binding of a +/// key replaces the value of an earlier one and keeps its key, ofSeqBy rejects every repeated key, and an existing +/// frozen map whose comparer equals the requested one comes back itself. +/// +/// +/// The single-key members of the BCL are functions of the module under the names of FSharp.Core: tryFind is +/// TryGetValue, and tryFind followed by ValueOption.defaultValue is GetValueOrDefault. A frozen +/// map has no function that adds a binding, so it has no counterpart of TryAdd. The members that return a reference, +/// GetValueRefOrNullRef and the indexer, stay calls on the frozen map, because a function cannot return a reference. +/// +/// +/// A map, sequence 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.FrozenMap + +open System +open System.Collections.Generic + +// ---- Creation ---- + +/// +/// The empty frozen map, with the default comparer. +/// +/// +/// O(1). The value is . +/// +[] +[] +val empty<'Key, 'Value when 'Key : not null> : FrozenMap<'Key, 'Value> + +/// +/// Holds an equality comparer of the keys, to create frozen maps with it. +/// +/// The equality comparer of the keys. +/// The struct whose members create frozen maps with the key comparer. +/// 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 struct also gives an existing frozen map another key comparer, through ofSeq. A frozen map whose comparer +/// equals the key comparer of the struct comes back itself, in O(1), as +/// +/// returns it. Any other frozen map is rebuilt in O(n), and the keys that the new comparer makes equal merge: the first of +/// them stays, with the value of the last one (ADR 0001, D14). +/// +/// +/// +/// +/// let table = (FrozenMap.forKeyComparer StringComparer.OrdinalIgnoreCase).ofSeq [ "one", 1; "ONE", 2 ] +/// +/// Evaluates to a frozen map whose keys ignore the case, with the key "one" bound to 2. +/// +/// +/// +/// let exact = FrozenMap.ofSeq [ "one", 1; "two", 2 ] +/// let ignoreCase = (FrozenMap.forKeyComparer StringComparer.OrdinalIgnoreCase).ofSeq exact +/// +/// Evaluates ignoreCase to a frozen map of the same bindings whose keys ignore the case. +/// +[] +val forKeyComparer : keyComparer : IEqualityComparer<'Key> -> CustomComparersFrozenMap<'Key, 'Value> + +/// +/// Creates a frozen map from a read-only span of key/value pairs. +/// +/// The input span of key/value pairs. +/// The frozen map of the bindings, with the default comparer. +/// +/// +/// O(n). The function wraps +/// +/// and reads the span itself, where ofArray and ofSeq need an array or a sequence, which copies the span first. +/// +/// +/// A repeated key never throws. A later binding of a key replaces the value of an earlier one and leaves its key (ADR 0001, +/// D14). The empty span gives the empty frozen map. +/// +/// +/// A or an array converts to the parameter only when the types of the call are known, and F# +/// warns about the conversion (FS3391). Where the type arguments are still to be inferred, call ofSpan or +/// ofArray. +/// +/// +[] +val ofReadOnlySpan : span : ReadOnlySpan> -> FrozenMap<'Key, 'Value> + +/// +/// Creates a frozen 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 frozen map of the elements under their keys. +/// Thrown when is or a default flat list. +/// Thrown when two elements have the same key, even when they are equal. +/// +/// +/// O(n). The function wraps +/// , +/// which rejects every repeated key (ADR 0001, D14). +/// +/// +/// The overload of ToFrozenDictionary with an element selector has no function. Mapping the source to pairs and +/// calling ofSeq builds the same map in O(n), but keeps the last binding of a repeated key, where the BCL member +/// rejects every repeated key. A frozen map has no addRange, so no function rejects them. +/// +/// +[] +val ofSeqBy : projection : ('Value -> 'Key) -> source : seq<'Value> -> FrozenMap<'Key, 'Value> + +/// +/// Creates a frozen map from a span of key/value pairs. +/// +/// The input span of key/value pairs. +/// The frozen map of the bindings, with the default comparer. +/// +/// +/// O(n). The function is ofReadOnlySpan for a . F# converts a span to a read-only span +/// only where the type of the parameter is known, so a call that leaves the type arguments to inference fails with FS0001 +/// for ofReadOnlySpan and succeeds here. +/// +/// +/// A repeated key never throws. A later binding of a key replaces the value of an earlier one and leaves its key (ADR 0001, +/// D14). +/// +/// +[] +val ofSpan : span : Span> -> FrozenMap<'Key, 'Value> + +/// Creates a frozen map of one binding. +/// The key. +/// The value. +/// The frozen map of the binding, with the default comparer. +/// O(1). +[] +val singleton : key : 'Key -> value : 'Value -> FrozenMap<'Key, 'Value> + +// ---- Properties ---- + +/// Returns the number of bindings of the frozen map. +/// The input frozen map. +/// The number of bindings. +/// Thrown when is . +/// O(1). +[] +val count : table : FrozenMap<'Key, 'Value> -> int + +/// Returns whether the frozen map has no bindings. +/// The input frozen map. +/// when the frozen map is empty. +/// Thrown when is . +/// O(1). +[] +val isEmpty : table : FrozenMap<'Key, 'Value> -> bool + +/// +/// Returns the equality comparer of the keys. +/// +/// The input frozen map. +/// The key comparer. +/// Thrown when is . +/// +/// O(1). The function wraps . +/// +[] +val keyComparer : table : FrozenMap<'Key, 'Value> -> IEqualityComparer<'Key> + +/// +/// Returns the keys of the frozen map. +/// +/// The input frozen map. +/// The flat list of the keys, in the order of enumeration of the frozen map. +/// Thrown when is . +/// +/// O(1). The function wraps : the flat list shares the +/// array of the keys of the frozen map instead of copying it. FlatList.asSpan views the array as a span, also +/// without a copy. +/// +[] +val keys : table : FrozenMap<'Key, 'Value> -> FlatList<'Key> + +/// +/// Returns the values of the frozen map, including the duplicates. +/// +/// The input frozen map. +/// The flat list of the values, in the order of enumeration of the frozen map. +/// Thrown when is . +/// +/// O(1). The function wraps : the flat list shares the +/// array of the values of the frozen map instead of copying it. FlatList.asSpan views the array as a span, also +/// without a copy. +/// +[] +val values : table : FrozenMap<'Key, 'Value> -> FlatList<'Value> + +// ---- Access ---- + +/// Returns the value of a key. +/// The key to find. +/// The input frozen map. +/// The value of the key. +/// Thrown when is . +/// Thrown when the frozen map has no binding of the key. +/// O(1) on average. +[] +val find : key : 'Key -> table : FrozenMap<'Key, 'Value> -> 'Value + +/// +/// Returns the value of a key, or ValueNone when the frozen map has no binding of the key. +/// +/// The key to find. +/// The input frozen map. +/// The value of the key, or ValueNone. +/// Thrown when is . +/// +/// O(1) on average. +/// +[] +val tryFind : key : 'Key -> table : FrozenMap<'Key, 'Value> -> 'Value voption + +// ---- Search ---- + +/// +/// Returns whether the frozen map binds a key to a value. +/// +/// The key to find. +/// The value to compare with the value of the key. +/// The input frozen map. +/// when the frozen map has a binding of the key whose value equals the given one. +/// +/// Thrown when or is . +/// +/// +/// +/// O(1) on average. The function wraps the explicit implementation of +/// for a key/value pair. +/// +/// +/// A frozen map has no comparer of its values, so the function compares them with +/// , as containsValue does. tryFind +/// followed by = compares with F# equality, which differs from it for some types, such as floats: a map that binds a +/// key to nan contains that binding, and nan = nan is . +/// +/// +[] +val containsBinding : key : 'Key -> value : 'Value -> table : FrozenMap<'Key, 'Value> -> bool + +/// Returns whether the frozen map has a binding of a key. +/// The key to find. +/// The input frozen map. +/// when the frozen map has a binding of the key. +/// Thrown when is . +/// O(1) on average. +[] +val containsKey : key : 'Key -> table : FrozenMap<'Key, 'Value> -> bool + +/// +/// Returns whether the frozen map has a binding of a value. +/// +/// The value to find. +/// The input frozen map. +/// when a value of the frozen map equals the given one. +/// Thrown when is . +/// +/// O(n). A frozen map has no comparer of its values, so the function compares them with +/// , as the frozen map itself does when it looks a +/// key/value pair up. +/// +[] +val containsValue : value : 'Value -> table : FrozenMap<'Key, 'Value> -> bool + +/// Returns whether any binding satisfies the predicate. +/// The function to test the bindings. +/// The input frozen map. +/// when a binding satisfies the predicate. +/// Thrown when is . +/// O(n). +[] +val exists : predicate : ('Key -> 'Value -> bool) -> table : FrozenMap<'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 frozen 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 : FrozenMap<'Key, 'Value> -> 'Key + +/// Returns whether all bindings satisfy the predicate. +/// The function to test the bindings. +/// The input frozen map. +/// when every binding satisfies the predicate. +/// Thrown when is . +/// O(n). +[] +val forall : predicate : ('Key -> 'Value -> bool) -> table : FrozenMap<'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 frozen 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 : FrozenMap<'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 frozen 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 : FrozenMap<'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 frozen map. +/// The first chosen value, or ValueNone. +/// Thrown when is . +/// +/// O(n). +/// +[] +val tryPick : chooser : ('Key -> 'Value -> 'U voption) -> table : FrozenMap<'Key, 'Value> -> 'U voption + +// ---- Transformation ---- + +/// +/// Returns a frozen 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 frozen map. +/// The frozen map of the chosen values, with the key comparer of the input. +/// Thrown when is . +/// +/// O(n). The function rebuilds the map: it freezes the chosen bindings into a new frozen map with the key comparer of the +/// input. +/// +[] +val choose : chooser : ('Key -> 'Value -> 'U voption) -> table : FrozenMap<'Key, 'Value> -> FrozenMap<'Key, 'U> + +/// +/// Returns the bindings that satisfy the predicate. +/// +/// The function to test the bindings. +/// The input frozen map. +/// +/// The frozen map of the bindings that satisfy the predicate, with the key comparer of the input; the input itself when +/// every binding does. +/// +/// Thrown when is . +/// +/// O(n). The function rebuilds the map: it freezes the bindings that satisfy the predicate into a new frozen map with the +/// key comparer of the input. +/// +[] +val filter : predicate : ('Key -> 'Value -> bool) -> table : FrozenMap<'Key, 'Value> -> FrozenMap<'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 frozen map. +/// The frozen map of the results, with the key comparer of the input. +/// Thrown when is . +/// +/// O(n). The function rebuilds the map: it freezes the results into a new frozen map with the key comparer of the input. +/// +[] +val map : mapping : ('Key -> 'Value -> 'U) -> table : FrozenMap<'Key, 'Value> -> FrozenMap<'Key, 'U> + +// ---- 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 frozen map. +/// The final state. +/// Thrown when is . +/// +/// O(n). +/// +[] +val fold : folder : ('State -> 'Key -> 'Value -> 'State) -> state : 'State -> table : FrozenMap<'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 frozen map. +/// The initial state. +/// The final state. +/// Thrown when is . +/// +/// O(n). The function visits the bindings from the last one that the frozen map enumerates to the first one, so it sees +/// them in the reverse order of +/// , +/// not in the order of the keys. It reads the arrays of the keys and the values of the frozen map backwards, without a +/// copy. +/// +[] +val foldBack : folder : ('Key -> 'Value -> 'State -> 'State) -> table : FrozenMap<'Key, 'Value> -> state : 'State -> 'State + +/// +/// Applies an action to each binding, in the order of enumeration. +/// +/// The action. +/// The input frozen map. +/// Thrown when is . +/// +/// O(n). +/// +[] +val iter : action : ('Key -> 'Value -> unit) -> table : FrozenMap<'Key, 'Value> -> unit + +// ---- Partitioning ---- + +/// +/// Splits the bindings by a predicate. +/// +/// The function to test the bindings. +/// The input frozen map. +/// +/// The frozen map of the bindings that satisfy the predicate and the frozen map of the other ones, both with the key +/// comparer of the input; the input itself in place of a part that has every binding. +/// +/// Thrown when is . +/// +/// O(n). The function rebuilds the map: it freezes each part into a new frozen map with the key comparer of the input. +/// +[] +val partition : + predicate : ('Key -> 'Value -> bool) -> + table : FrozenMap<'Key, 'Value> -> + struct (FrozenMap<'Key, 'Value> * FrozenMap<'Key, 'Value>) + +// ---- Conversion ---- + +/// +/// Copies the bindings of the frozen map to an array, starting at an index. +/// +/// The array that receives the bindings. +/// The index in of the first binding. +/// The input frozen map. +/// +/// Thrown when or is . +/// +/// +/// Thrown when is negative or greater than the length of . +/// +/// +/// Thrown when has no room for every binding from on. +/// +/// +/// +/// O(n). The function wraps +/// +/// and fills an array that the caller owns, in the order of enumeration of the frozen map, where toArray allocates a +/// new array of struct tuples. The elements before the index and after the last binding stay as they are. +/// +/// +/// The BCL gives no parameter name. The overload of the method that takes +/// a stays a call on the frozen map: a module function cannot be overloaded, and a parameter +/// of a byref-like type cannot be partially applied. +/// +/// +[] +val copyTo : destination : KeyValuePair<'Key, 'Value> array -> destinationIndex : int -> table : FrozenMap<'Key, 'Value> -> unit + +/// +/// Returns a new array of the bindings, in the order of enumeration. +/// +/// The input frozen map. +/// The array of key/value pairs. +/// Thrown when is . +/// +/// O(n). +/// +[] +val toArray : table : FrozenMap<'Key, 'Value> -> struct ('Key * 'Value) array + +/// Returns a list of the bindings, in the order of enumeration. +/// The input frozen map. +/// The list of key/value pairs. +/// Thrown when is . +/// O(n). +[] +val toList : table : FrozenMap<'Key, 'Value> -> struct ('Key * 'Value) list + +/// +/// Returns the bindings as a sequence, in the order of enumeration. +/// +/// The input frozen map. +/// The sequence of key/value pairs, computed on demand. +/// Thrown when is . +/// +/// O(1); O(n) to enumerate. +/// +[] +val toSeq : table : FrozenMap<'Key, 'Value> -> seq + +#if NET10_0_OR_GREATER +// ---- Type-specific ---- + +/// +/// Returns a lookup of the bindings of the frozen map by keys of another type. +/// +/// The input frozen map. +/// +/// The lookup, whose +/// , +/// and +/// find a binding by an alternate +/// key. +/// +/// Thrown when is . +/// +/// Thrown when the key comparer of the frozen map does not implement +/// of the alternate key type and the key type, or +/// when the keys are strings, which a frozen map looks up by spans of characters only. +/// +/// +/// +/// O(1). The function wraps . The type of +/// the alternate keys is the first type argument, so that a call names it and leaves the others to inference. +/// +/// +/// The function is on net10.0 only: the BCL member comes with .NET 9, and the library has no net9.0 target +/// (ADR 0001, D11). +/// +/// +/// An F# generic function cannot be instantiated with a byref-like type, so the function cannot look the keys up by a +/// span, such as the of characters that string keys take with +/// . For a span, call +/// on the frozen map itself. +/// +/// +/// +/// +/// // The comparer implements IEqualityComparer<int64> and IAlternateEqualityComparer<int, int64> +/// let table = (FrozenMap.forKeyComparer comparer).ofSeq [ 1L, "one"; 2L, "two" ] +/// let lookup = FrozenMap.alternateLookup<int, _, _> table +/// let two = lookup[2] +/// +/// // A span needs the method of the frozen map +/// let names = (FrozenMap.forKeyComparer StringComparer.Ordinal).ofSeq [ "one", 1; "two", 2 ] +/// let one = names.GetAlternateLookup<ReadOnlySpan<char>>().ContainsKey ("one".AsSpan ()) +/// +/// Evaluates two to "two" and one to . +/// +[] +val alternateLookup<'Alternate, 'Key, 'Value when 'Key : not null and 'Alternate : not null> : + table : FrozenMap<'Key, 'Value> -> FrozenMap<'Key, 'Value>.AlternateLookup<'Alternate> + +/// +/// Returns a lookup of the bindings of the frozen map by keys of another type, or ValueNone when the frozen map +/// cannot look its keys up by that type. +/// +/// The input frozen map. +/// +/// The lookup, whose +/// , +/// and +/// find a binding by an alternate +/// key; ValueNone when the key comparer of the frozen map does not implement +/// of the alternate key type and the key type, or +/// when the keys are strings, which a frozen map looks up by spans of characters only. +/// +/// Thrown when is . +/// +/// +/// O(1). The function wraps +/// . +/// The type of the alternate keys is the first type argument, so that a call names it and leaves the others to inference: +/// FrozenMap.tryAlternateLookup<int, _, _> table. +/// +/// +/// The function is on net10.0 only: the BCL member comes with .NET 9, and the library has no net9.0 target +/// (ADR 0001, D11). +/// +/// +/// An F# generic function cannot be instantiated with a byref-like type, so the function cannot look the keys up by a +/// span, such as the of characters that string keys take with +/// . For a span, call +/// +/// on the frozen map itself. +/// +/// +[] +val tryAlternateLookup<'Alternate, 'Key, 'Value when 'Key : not null and 'Alternate : not null> : + table : FrozenMap<'Key, 'Value> -> FrozenMap<'Key, 'Value>.AlternateLookup<'Alternate> voption +#endif diff --git a/src/FSharp.Collections.Immutable/FrozenMap/FrozenMapCE.fs b/src/FSharp.Collections.Immutable/FrozenMap/FrozenMapCE.fs new file mode 100644 index 00000000..edbeb978 --- /dev/null +++ b/src/FSharp.Collections.Immutable/FrozenMap/FrozenMapCE.fs @@ -0,0 +1,124 @@ +namespace FSharp.Collections.Immutable + +open System +open System.Collections.Frozen +open System.Collections.Generic + +type FrozenMapBuffer<'Key, 'Value when 'Key : not null> = Dictionary<'Key, 'Value> -> unit + +[] +type FrozenMapCE<'Key when 'Key : not null> (keyComparer : IEqualityComparer<'Key>) = + + do checkNotNull (nameof keyComparer) keyComparer + + new () = FrozenMapCE<'Key>(EqualityComparer<'Key>.Default) + + member inline _.Zero () : FrozenMapBuffer<'Key, 'Value> = ignore + + // Every binding goes through the indexer of the dictionary, which replaces the value of an earlier binding of a key and + // keeps its key, as ToFrozenDictionary does (ADR 0001, D14) + member inline _.Yield (pair : struct ('Key * 'Value)) : FrozenMapBuffer<'Key, 'Value> = + fun bindings -> + let struct (key, value) = pair + bindings[key] <- value + + member inline _.Yield (pair : KeyValuePair<'Key, 'Value>) : FrozenMapBuffer<'Key, 'Value> = + fun bindings -> bindings[pair.Key] <- pair.Value + + member inline _.YieldFrom (pairs : seq) : FrozenMapBuffer<'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 bindings -> + for struct (key, value) in pairs do + bindings[key] <- value + + member inline _.YieldFrom (pairs : struct ('Key * 'Value) array) : FrozenMapBuffer<'Key, 'Value> = + // A null array fails with NullReferenceException when the buffer enumerates it; the expression rejects it when + // yield! runs, as it does a null sequence (ADR 0001, D6) + checkNotNull (nameof pairs) pairs + + fun bindings -> + for struct (key, value) in pairs do + bindings[key] <- value + + member inline _.YieldFrom (pairs : seq>) : FrozenMapBuffer<'Key, 'Value> = + checkNotNullOrDefaultSeq (nameof pairs) pairs + + fun bindings -> + for pair in pairs do + bindings[pair.Key] <- pair.Value + + member inline _.YieldFrom (table : FrozenMap<'Key, 'Value>) : FrozenMapBuffer<'Key, 'Value> = + checkNotNull (nameof table) table + + fun bindings -> + for pair in table do + bindings[pair.Key] <- pair.Value + + member inline _.YieldFrom (buffer : FrozenMapBuffer<'Key, 'Value>) : FrozenMapBuffer<'Key, 'Value> = buffer + + member inline _.Combine + (first : FrozenMapBuffer<'Key, 'Value>, second : FrozenMapBuffer<'Key, 'Value>) + : FrozenMapBuffer<'Key, 'Value> + = + fun bindings -> + first bindings + second bindings + + member inline _.Delay ([] delayed : unit -> FrozenMapBuffer<'Key, 'Value>) : FrozenMapBuffer<'Key, 'Value> = + fun bindings -> delayed () bindings + + member inline _.For + (source : seq<'T>, [] body : 'T -> FrozenMapBuffer<'Key, 'Value>) + : FrozenMapBuffer<'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 bindings -> + for item in source do + body item bindings + + member inline _.While + ([] guard : unit -> bool, body : FrozenMapBuffer<'Key, 'Value>) + : FrozenMapBuffer<'Key, 'Value> + = + fun bindings -> + while guard () do + body bindings + + member inline _.TryWith + (body : FrozenMapBuffer<'Key, 'Value>, [] handler : exn -> FrozenMapBuffer<'Key, 'Value>) + : FrozenMapBuffer<'Key, 'Value> + = + fun bindings -> + try + body bindings + with ex -> + handler ex bindings + + member inline _.TryFinally + (body : FrozenMapBuffer<'Key, 'Value>, [] compensation : unit -> unit) + : FrozenMapBuffer<'Key, 'Value> + = + fun bindings -> + try + body bindings + finally + compensation () + + member inline _.Using + (resource : 'Resource, [] body : 'Resource -> FrozenMapBuffer<'Key, 'Value>) + : FrozenMapBuffer<'Key, 'Value> + = + fun bindings -> + use resource = resource + body resource bindings + + member _.Run (buffer : FrozenMapBuffer<'Key, 'Value>) : FrozenMap<'Key, 'Value> = + let bindings = Dictionary<'Key, 'Value> keyComparer + buffer bindings + // ToFrozenDictionary reads a dictionary whose comparer equals the given one without building another one + FrozenDictionary.ToFrozenDictionary (bindings, keyComparer) diff --git a/src/FSharp.Collections.Immutable/FrozenMap/FrozenMapCE.fsi b/src/FSharp.Collections.Immutable/FrozenMap/FrozenMapCE.fsi new file mode 100644 index 00000000..07d11dca --- /dev/null +++ b/src/FSharp.Collections.Immutable/FrozenMap/FrozenMapCE.fsi @@ -0,0 +1,139 @@ +namespace FSharp.Collections.Immutable + +open System +open System.Collections.Generic + +/// +/// The body of a frozenMap { } expression: a function that sets the bindings in a dictionary. +/// +type FrozenMapBuffer<'Key, 'Value when 'Key : not null> = Dictionary<'Key, 'Value> -> unit + +/// +/// The builder of the frozenMap { } computation expression. +/// +/// +/// +/// A frozen map has no builder, so the expression collects its bindings in a +/// with the key comparer (ADR 0001, D7) and freezes them once, when +/// it runs. It yields a binding as a struct pair, yield struct (key, value), or as a +/// . A later binding of a key replaces the value of an earlier one, +/// as in +/// , +/// and the key of the earlier binding stays, as the indexer of the dictionary keeps it (ADR 0001, D14). +/// +/// +/// The keys compare with the key comparer that the constructor takes, or with +/// when it is omitted. The builder is generic in the +/// type of the keys, so that it can hold a comparer of them, and each frozenMap { } expression infers the type of its +/// keys. FrozenMapCE StringComparer.OrdinalIgnoreCase { yield struct ("a", 1) } builds a map whose keys ignore the +/// case. +/// +/// +[] +type FrozenMapCE<'Key when 'Key : not null> = + + /// + /// Creates the builder of frozen maps whose keys compare with a key comparer. + /// + /// The equality comparer of the keys of the frozen map. + /// Thrown when is . + new : keyComparer : IEqualityComparer<'Key> -> FrozenMapCE<'Key> + + /// + /// Creates the builder of frozen maps whose keys compare with . + /// + new : unit -> FrozenMapCE<'Key> + + /// An expression without bindings. + member inline Zero : unit -> FrozenMapBuffer<'Key, 'Value> + + /// + /// Sets a binding given as a struct pair of a key and a value. + /// + /// + /// The value replaces the value of an earlier binding of the key, whose key stays (ADR 0001, D14). + /// + member inline Yield : pair : struct ('Key * 'Value) -> FrozenMapBuffer<'Key, 'Value> + + /// + /// Sets a binding given as a key/value pair. + /// + /// + /// The value replaces the value of an earlier binding of the key, whose key stays (ADR 0001, D14). + /// + member inline Yield : pair : KeyValuePair<'Key, 'Value> -> FrozenMapBuffer<'Key, 'Value> + + /// + /// Sets the bindings of a sequence of struct pairs. + /// + /// Thrown when the sequence is or a default flat list. + /// + /// A value replaces the value of an earlier binding of its key, whose key stays (ADR 0001, D14). + /// + member inline YieldFrom : pairs : seq -> FrozenMapBuffer<'Key, 'Value> + + /// + /// Sets the bindings of an array of struct pairs. + /// + /// Thrown when the array is . + /// + /// A value replaces the value of an earlier binding of its key, whose key stays (ADR 0001, D14). + /// + member inline YieldFrom : pairs : struct ('Key * 'Value) array -> FrozenMapBuffer<'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 replaces the value of an earlier binding of its key, whose key stays (ADR 0001, D14). + /// + member inline YieldFrom : pairs : seq> -> FrozenMapBuffer<'Key, 'Value> + + /// + /// Sets the bindings of a frozen map. + /// + /// Thrown when the frozen map is . + /// + /// A value replaces the value of an earlier binding of its key, whose key stays (ADR 0001, D14). + /// + member inline YieldFrom : table : FrozenMap<'Key, 'Value> -> FrozenMapBuffer<'Key, 'Value> + + /// Sets the bindings of another expression body. + member inline YieldFrom : buffer : FrozenMapBuffer<'Key, 'Value> -> FrozenMapBuffer<'Key, 'Value> + + /// Sets the bindings of two parts of the expression, in order. + member inline Combine : + first : FrozenMapBuffer<'Key, 'Value> * second : FrozenMapBuffer<'Key, 'Value> -> FrozenMapBuffer<'Key, 'Value> + + /// + /// Runs the body of the expression each time the bindings are collected. + /// + member inline Delay : [] delayed : (unit -> FrozenMapBuffer<'Key, 'Value>) -> FrozenMapBuffer<'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 -> FrozenMapBuffer<'Key, 'Value>) -> FrozenMapBuffer<'Key, 'Value> + + /// Sets the bindings of the body while the guard holds. + member inline While : + [] guard : (unit -> bool) * body : FrozenMapBuffer<'Key, 'Value> -> FrozenMapBuffer<'Key, 'Value> + + /// Handles the exceptions of the body. + member inline TryWith : + body : FrozenMapBuffer<'Key, 'Value> * [] handler : (exn -> FrozenMapBuffer<'Key, 'Value>) -> + FrozenMapBuffer<'Key, 'Value> + + /// Runs the compensation after the body. + member inline TryFinally : + body : FrozenMapBuffer<'Key, 'Value> * [] compensation : (unit -> unit) -> FrozenMapBuffer<'Key, 'Value> + + /// Disposes a resource after the body. + member inline Using : + resource : 'Resource * [] body : ('Resource -> FrozenMapBuffer<'Key, 'Value>) -> + FrozenMapBuffer<'Key, 'Value> + when 'Resource :> IDisposable | null + + /// Collects the bindings of the expression and freezes them. + member Run : buffer : FrozenMapBuffer<'Key, 'Value> -> FrozenMap<'Key, 'Value> diff --git a/src/FSharp.Collections.Immutable/FrozenMap/FrozenMapOption.fs b/src/FSharp.Collections.Immutable/FrozenMap/FrozenMapOption.fs new file mode 100644 index 00000000..b71115a0 --- /dev/null +++ b/src/FSharp.Collections.Immutable/FrozenMap/FrozenMapOption.fs @@ -0,0 +1,43 @@ +[] +module FSharp.Collections.Immutable.Option.FrozenMap + +open FSharp.Collections.Immutable + +// FSharp.Core has no function over a frozen 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 : FrozenMap<'Key, 'Value>) = FrozenMap.tryFind key table |> ValueOption.toOption + +// ---- Search ---- + +[] +let pick (chooser : 'Key -> 'Value -> 'U option) (table : FrozenMap<'Key, 'Value>) = + FrozenMap.pick (fun key value -> chooser key value |> ValueOption.ofOption) table + +[] +let tryFindKey predicate (table : FrozenMap<'Key, 'Value>) = FrozenMap.tryFindKey predicate table |> ValueOption.toOption + +[] +let tryPick (chooser : 'Key -> 'Value -> 'U option) (table : FrozenMap<'Key, 'Value>) = + FrozenMap.tryPick (fun key value -> chooser key value |> ValueOption.ofOption) table + |> ValueOption.toOption + +// ---- Transformation ---- + +[] +let choose (chooser : 'Key -> 'Value -> 'U option) (table : FrozenMap<'Key, 'Value>) = + FrozenMap.choose (fun key value -> chooser key value |> ValueOption.ofOption) table + +#if NET10_0_OR_GREATER +// ---- Type-specific ---- + +[] +let tryAlternateLookup<'Alternate, 'Key, 'Value when 'Key : not null and 'Alternate : not null> + (table : FrozenMap<'Key, 'Value>) + = + FrozenMap.tryAlternateLookup<'Alternate, 'Key, 'Value> table + |> ValueOption.toOption +#endif diff --git a/src/FSharp.Collections.Immutable/FrozenMap/FrozenMapOption.fsi b/src/FSharp.Collections.Immutable/FrozenMap/FrozenMapOption.fsi new file mode 100644 index 00000000..da44d625 --- /dev/null +++ b/src/FSharp.Collections.Immutable/FrozenMap/FrozenMapOption.fsi @@ -0,0 +1,111 @@ +/// +/// 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.FrozenMap + +open FSharp.Collections.Immutable + +// ---- Access ---- + +/// +/// Returns the value of a key, or None when the frozen map has no binding of the key. +/// +/// The key to find. +/// The input frozen map. +/// The value of the key, or None. +/// Thrown when is . +/// +/// O(1) on average. +/// +[] +val tryFind : key : 'Key -> table : FrozenMap<'Key, 'Value> -> 'Value 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 frozen 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 : FrozenMap<'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 frozen 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 : FrozenMap<'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 frozen map. +/// The first chosen value, or None. +/// Thrown when is . +/// +/// O(n). +/// +[] +val tryPick : chooser : ('Key -> 'Value -> 'U option) -> table : FrozenMap<'Key, 'Value> -> 'U option + +// ---- Transformation ---- + +/// +/// Returns a frozen 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 frozen map. +/// The frozen map of the chosen values, with the key comparer of the input. +/// Thrown when is . +/// +/// O(n). The function rebuilds the map: it freezes the chosen bindings into a new frozen map with the key comparer of the +/// input. +/// +[] +val choose : chooser : ('Key -> 'Value -> 'U option) -> table : FrozenMap<'Key, 'Value> -> FrozenMap<'Key, 'U> + +#if NET10_0_OR_GREATER +// ---- Type-specific ---- + +/// +/// Returns a lookup of the bindings of the frozen map by keys of another type, or None when the frozen map cannot +/// look its keys up by that type. +/// +/// The input frozen map. +/// +/// The lookup; None when the key comparer of the frozen map does not implement +/// of the alternate key type and the key type, or +/// when the keys are strings, which a frozen map looks up by spans of characters only. +/// +/// Thrown when is . +/// +/// O(1). The function wraps +/// . +/// An F# generic function cannot be instantiated with a byref-like type, so the function cannot look the keys up by a +/// span; for a span, call that method on the frozen map itself. +/// +[] +val tryAlternateLookup<'Alternate, 'Key, 'Value when 'Key : not null and 'Alternate : not null> : + table : FrozenMap<'Key, 'Value> -> FrozenMap<'Key, 'Value>.AlternateLookup<'Alternate> option +#endif diff --git a/src/FSharp.Collections.Immutable/FrozenMap/FrozenMapTuple.fs b/src/FSharp.Collections.Immutable/FrozenMap/FrozenMapTuple.fs new file mode 100644 index 00000000..69823ad5 --- /dev/null +++ b/src/FSharp.Collections.Immutable/FrozenMap/FrozenMapTuple.fs @@ -0,0 +1,31 @@ +[] +module FSharp.Collections.Immutable.Tuple.FrozenMap + +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 : FrozenMap<'Key, 'Value>) = + let struct (accepted, rejected) = FrozenMap.partition predicate table + accepted, rejected + +// ---- Conversion ---- + +[] +let toArray (table : FrozenMap<'Key, 'Value>) = + checkNotNull (nameof table) table + Dictionaries.toArray referencePair table + +[] +let toList (table : FrozenMap<'Key, 'Value>) = + checkNotNull (nameof table) table + Dictionaries.toList referencePair table + +[] +let toSeq (table : FrozenMap<'Key, 'Value>) = + checkNotNull (nameof table) table + Dictionaries.toSeq referencePair table diff --git a/src/FSharp.Collections.Immutable/FrozenMap/FrozenMapTuple.fsi b/src/FSharp.Collections.Immutable/FrozenMap/FrozenMapTuple.fsi new file mode 100644 index 00000000..f492e09a --- /dev/null +++ b/src/FSharp.Collections.Immutable/FrozenMap/FrozenMapTuple.fsi @@ -0,0 +1,72 @@ +/// +/// 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): FrozenMap.ofSeq is overloaded for +/// reference tuples. +/// +/// +[] +module FSharp.Collections.Immutable.Tuple.FrozenMap + +open FSharp.Collections.Immutable + +// ---- Partitioning ---- + +/// +/// Splits the bindings by a predicate. +/// +/// The function to test the bindings. +/// The input frozen map. +/// +/// The frozen map of the bindings that satisfy the predicate and the frozen map of the other ones, both with the key +/// comparer of the input; the input itself in place of a part that has every binding. +/// +/// Thrown when is . +/// +/// O(n). The function rebuilds the map: it freezes each part into a new frozen map with the key comparer of the input. +/// +[] +val partition : + predicate : ('Key -> 'Value -> bool) -> table : FrozenMap<'Key, 'Value> -> FrozenMap<'Key, 'Value> * FrozenMap<'Key, 'Value> + +// ---- Conversion ---- + +/// +/// Returns a new array of the bindings, in the order of enumeration. +/// +/// The input frozen map. +/// The array of key/value pairs. +/// Thrown when is . +/// +/// O(n). +/// +[] +val toArray : table : FrozenMap<'Key, 'Value> -> ('Key * 'Value) array + +/// Returns a list of the bindings, in the order of enumeration. +/// The input frozen map. +/// The list of key/value pairs. +/// Thrown when is . +/// O(n). +[] +val toList : table : FrozenMap<'Key, 'Value> -> ('Key * 'Value) list + +/// +/// Returns the bindings as a sequence, in the order of enumeration. +/// +/// The input frozen map. +/// The sequence of key/value pairs, computed on demand. +/// Thrown when is . +/// +/// O(1); O(n) to enumerate. +/// +[] +val toSeq : table : FrozenMap<'Key, 'Value> -> seq<'Key * 'Value> diff --git a/src/FSharp.Collections.Immutable/FrozenMap/FrozenMapTypes.fs b/src/FSharp.Collections.Immutable/FrozenMap/FrozenMapTypes.fs new file mode 100644 index 00000000..c8f2717f --- /dev/null +++ b/src/FSharp.Collections.Immutable/FrozenMap/FrozenMapTypes.fs @@ -0,0 +1,16 @@ +namespace FSharp.Collections.Immutable + +open System.Collections.Frozen + +// The abbreviation has its own file because the module of the folder is a root module, which cannot declare a type of +// the namespace; a file of abbreviations needs no signature file (ADR 0001, D2). + +/// +/// An immutable map that is built once and read many times, . +/// +/// +/// The creation of the map analyses its keys to make the lookups fast, so it costs more than the creation of a hash map. The +/// map enumerates its bindings in an unspecified order, which stays the same for the same map: the order of the arrays of +/// its keys and its values. +/// +type FrozenMap<'Key, 'Value when 'Key : not null> = FrozenDictionary<'Key, 'Value> 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 4669bd86..a18b2158 100644 --- a/tests/FSharp.Collections.Immutable.Tests/FSharp.Collections.Immutable.Tests.fsproj +++ b/tests/FSharp.Collections.Immutable.Tests/FSharp.Collections.Immutable.Tests.fsproj @@ -83,6 +83,14 @@ + + + + + + + + diff --git a/tests/FSharp.Collections.Immutable.Tests/FrozenMap/CustomComparersFrozenMapTests.fs b/tests/FSharp.Collections.Immutable.Tests/FrozenMap/CustomComparersFrozenMapTests.fs new file mode 100644 index 00000000..56eb5d3c --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/FrozenMap/CustomComparersFrozenMapTests.fs @@ -0,0 +1,412 @@ +namespace FSharp.Collections.Immutable.Tests + +open System +open System.Collections.Generic +open System.Globalization +open FSharp.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// The comparer struct of `FrozenMap`, `CustomComparersFrozenMap`, and the function of the module that creates it (ADR 0001, +/// D12). The creation contract runs its members with a key comparer; these tests cover the comparer itself. +[] +type CustomComparersFrozenMapTests () = + + static let nullComparer = Unchecked.defaultof> + + /// The members of the struct that create a frozen map, by name. + static let creations : Map -> FrozenMap> = + 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") + |] + + /// The members of the struct that create a frozen map from the bindings of "a" to 1, "b" to 2 and "A" to 3, by name: + /// keys that ignore the case make the first and the last binding equal. + static let equalKeyCreations : Map -> FrozenMap> = + Map.ofArray [| + "ofArray of key/value pairs", + (fun comparers -> comparers.ofArray [| KeyValuePair ("a", 1); KeyValuePair ("b", 2); KeyValuePair ("A", 3) |]) + "ofArray of reference tuples", (fun comparers -> comparers.ofArray [| "a", 1; "b", 2; "A", 3 |]) + "ofArray of struct tuples", + (fun comparers -> comparers.ofArray [| struct ("a", 1); struct ("b", 2); struct ("A", 3) |]) + "ofList of key/value pairs", + (fun comparers -> comparers.ofList [ KeyValuePair ("a", 1); KeyValuePair ("b", 2); KeyValuePair ("A", 3) ]) + "ofList of reference tuples", (fun comparers -> comparers.ofList [ "a", 1; "b", 2; "A", 3 ]) + "ofList of struct tuples", (fun comparers -> comparers.ofList [ struct ("a", 1); struct ("b", 2); struct ("A", 3) ]) + "ofReadOnlySpan", + (fun comparers -> + comparers.ofReadOnlySpan (ReadOnlySpan [| KeyValuePair ("a", 1); KeyValuePair ("b", 2); KeyValuePair ("A", 3) |]) + ) + "ofSeq of key/value pairs", + (fun comparers -> + comparers.ofSeq (Seq.ofArray [| KeyValuePair ("a", 1); KeyValuePair ("b", 2); KeyValuePair ("A", 3) |]) + ) + "ofSeq of reference tuples", (fun comparers -> comparers.ofSeq (Seq.ofArray [| "a", 1; "b", 2; "A", 3 |])) + "ofSeq of struct tuples", + (fun comparers -> comparers.ofSeq (Seq.ofArray [| struct ("a", 1); struct ("b", 2); struct ("A", 3) |])) + "ofSpan", + (fun comparers -> comparers.ofSpan (Span [| KeyValuePair ("a", 1); KeyValuePair ("b", 2); KeyValuePair ("A", 3) |])) + |] + + /// The members of the struct that take the span of an array, by name. A span cannot be captured, so each member makes + /// its own. + static let spanMembers + : Map -> KeyValuePair array -> FrozenMap> = + Map.ofArray [| + "ofReadOnlySpan", (fun comparers pairs -> comparers.ofReadOnlySpan (ReadOnlySpan pairs)) + "ofSpan", (fun comparers pairs -> comparers.ofSpan (Span pairs)) + |] + + /// 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. + static let nullSequenceMembers : Map unit))> = + let comparers : CustomComparersFrozenMap = + FrozenMap.forKeyComparer StringComparer.OrdinalIgnoreCase + + 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 : CustomComparersFrozenMap = + FrozenMap.forKeyComparer StringComparer.OrdinalIgnoreCase + + let objectComparers : CustomComparersFrozenMap = + FrozenMap.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 bindings of a frozen map as reference tuples, sorted by key. + static let sortedBindings (table : FrozenMap) = + FrozenMap.toArray table + |> Array.map (fun struct (key, value) -> key, value) + |> Array.sortBy fst + + /// The names of the members that create a frozen map, as the rows of the tests of their comparer. + static member Creations = creations.Keys + + /// The names of the members that create a frozen map from keys that the comparer makes equal, as the rows of the test of + /// the kept binding. + static member EqualKeyCreations = equalKeyCreations.Keys + + /// The names of the members that take a span, as the rows of the tests of the spans. + static member SpanMembers = spanMembers.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 rejects a null comparer`` () = + let ex = + Assert.ThrowsExactly( + Action (fun () -> + CustomComparersFrozenMap nullComparer + |> ignore + ), + "The constructor must reject a null key comparer" + ) + + Assert.AreEqual("keyComparer", ex.ParamName, "The constructor must name the key comparer") + + [] + member _.``forKeyComparer rejects a null comparer`` () = + let ex = + Assert.ThrowsExactly( + Action (fun () -> + (FrozenMap.forKeyComparer nullComparer : CustomComparersFrozenMap) + |> ignore + ), + "FrozenMap.forKeyComparer must reject a null key comparer" + ) + + Assert.AreEqual("keyComparer", ex.ParamName, "FrozenMap.forKeyComparer must name the key comparer") + + [] + member _.``members reject a null sequence`` (name : string) = + let struct (paramName, call) = nullSequenceMembers[name] + FlatListAssert.RejectsNull ($"CustomComparersFrozenMap.%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 ($"CustomComparersFrozenMap.%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 : CustomComparersFrozenMap = + FrozenMap.forKeyComparer StringComparer.OrdinalIgnoreCase + + let bindingsOf (table : FrozenMap) = + FrozenMap.toArray table + |> Array.sortBy (fun struct (key, _) -> key) + + CollectionAssert.AreEqual ( + [| struct ("a", "b") |], + bindingsOf (comparers.ofSeq (flatList [| KeyValuePair ("a", "b") |])), + "ofSeq must take the key/value pairs of a flat list" + ) + + CollectionAssert.AreEqual ( + [| struct ("a", "b") |], + bindingsOf (comparers.ofSeq (flatList [| ("a", "b") |])), + "ofSeq must take the reference tuples of a flat list" + ) + + CollectionAssert.AreEqual ( + [| struct ("a", "b") |], + bindingsOf (comparers.ofSeq (flatList [| struct ("a", "b") |])), + "ofSeq must take the struct tuples of a flat list" + ) + + CollectionAssert.AreEqual ( + [| struct ("ab", "ab") |], + bindingsOf (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 : CustomComparersFrozenMap = + FrozenMap.forKeyComparer EqualityComparer.Default + let widened = FlatListOracles.stringsAsObjects (FlatList.ofArray [| "ab" |]) + + CollectionAssert.AreEqual ( + [| "ab" :> obj |], + FrozenMap.keys (objectComparers.ofSeqBy id widened) + |> Seq.toArray, + "ofSeqBy must take the elements of a flat list of strings as objects" + ) + + [] + member _.``forKeyComparer holds the key comparer`` () = + let comparers : CustomComparersFrozenMap = + FrozenMap.forKeyComparer StringComparer.OrdinalIgnoreCase + + Assert.AreSame>( + StringComparer.OrdinalIgnoreCase, + comparers.KeyComparer, + "FrozenMap.forKeyComparer must hold the key comparer" + ) + + [] + member _.``forKeyComparer StringComparer.OrdinalIgnoreCase applies the comparer`` () = + let table = + (FrozenMap.forKeyComparer StringComparer.OrdinalIgnoreCase).ofArray [| "one", 1; "ONE", 2; "two", 3 |] + + Assert.AreSame>( + StringComparer.OrdinalIgnoreCase, + FrozenMap.keyComparer table, + "The frozen map of FrozenMap.forKeyComparer must have the key comparer" + ) + + Assert.AreEqual(2, FrozenMap.count table, "The comparer must merge the keys that differ in their case") + Assert.AreEqual(2, FrozenMap.find "One" table, "The frozen map must keep the last binding of keys that are equal") + Assert.AreEqual(3, FrozenMap.find "TWO" table, "The frozen map must look the keys up with the comparer") + + [] + member _.``every member creates frozen maps with the comparer of the struct`` (creation : string) = + let table = creations[creation](FrozenMap.forKeyComparer StringComparer.OrdinalIgnoreCase) + + Assert.AreSame>( + StringComparer.OrdinalIgnoreCase, + FrozenMap.keyComparer table, + "The member must create a frozen map with the key comparer of the struct" + ) + + [] + member _.``the default value of the struct stands for the default comparer`` () = + let comparers = CustomComparersFrozenMap() + + Assert.AreSame>( + EqualityComparer.Default, + comparers.KeyComparer, + "The default value of the struct must report the default key comparer" + ) + + [] + member _.``every member of the default value of the struct creates frozen maps with the default comparer`` + (creation : string) + = + let table = creations[creation](CustomComparersFrozenMap()) + + Assert.AreSame>( + EqualityComparer.Default, + FrozenMap.keyComparer table, + "The member of the default value of the struct must create a frozen map with the default key comparer" + ) + + [] + member _.``every shape keeps the first of the keys that the comparer makes equal with the value of the last one`` + (creation : string) + = + // ToFrozenDictionary keeps the key of the earlier binding and the value of the later one (ADR 0001, D14) + CollectionAssert.AreEqual ( + [| "a", 3; "b", 2 |], + sortedBindings (equalKeyCreations[creation](FrozenMap.forKeyComparer StringComparer.OrdinalIgnoreCase)), + "The frozen map must keep the first of the equal keys with the value of the last one" + ) + + [] + member _.``ofSeq returns a frozen map with the comparer of the struct itself`` () = + let table = (FrozenMap.forKeyComparer StringComparer.OrdinalIgnoreCase).ofArray [| "one", 1; "two", 2 |] + + Assert.AreSame ( + table, + (FrozenMap.forKeyComparer StringComparer.OrdinalIgnoreCase).ofSeq table, + "ofSeq must return a frozen map with the comparer of the struct itself" + ) + + // The first two rows hold equal comparers that are not the same instance, which ToFrozenDictionary accepts as well + // (ADR 0001, D14) + [] + member _.``ofSeq returns a frozen map itself only when its comparer equals the comparer of the struct`` + (mapIgnoresCase : bool, structIgnoresCase : bool, itself : bool) + = + // Two comparers of the same culture and options are equal without being the same instance + let mapComparer = StringComparer.Create (CultureInfo.InvariantCulture, mapIgnoresCase) + let structComparer = StringComparer.Create (CultureInfo.InvariantCulture, structIgnoresCase) + Assert.AreNotSame (mapComparer, structComparer, "The comparers must be two instances") + let table = (FrozenMap.forKeyComparer mapComparer).ofArray [| "one", 1; "two", 2 |] + let result = (FrozenMap.forKeyComparer structComparer).ofSeq table + + Assert.AreEqual( + itself, + obj.ReferenceEquals (table, result), + "ofSeq must return a frozen map itself exactly when its comparer equals the comparer of the struct" + ) + + CollectionAssert.AreEqual (sortedBindings table, sortedBindings result, "ofSeq must keep the bindings of a frozen map") + + [] + member _.``ofSeq gives a frozen map the key comparer of the struct and merges the keys that become equal`` () = + let table = FrozenMap.ofArray [| "a", 1; "A", 2; "b", 3 |] + + // A frozen map enumerates its bindings in an unspecified order, which decides the first key and the last value + let equalKeys = + FrozenMap.toArray table + |> Array.filter (fun struct (key, _) -> String.Equals (key, "a", StringComparison.OrdinalIgnoreCase)) + + let struct (firstKey, _) = Array.head equalKeys + let struct (_, lastValue) = Array.last equalKeys + let ignoreCase = (FrozenMap.forKeyComparer StringComparer.OrdinalIgnoreCase).ofSeq table + + Assert.AreSame>( + StringComparer.OrdinalIgnoreCase, + FrozenMap.keyComparer ignoreCase, + "ofSeq must give the frozen map the key comparer of the struct" + ) + + CollectionAssert.AreEqual ( + [| firstKey, lastValue; "b", 3 |], + sortedBindings ignoreCase, + "ofSeq must keep the first of the keys that become equal in the order of enumeration, with the last value" + ) + + [] + member _.``the span members copy the span`` (name : string) = + let pairs = [| KeyValuePair ("one", "uno") |] + let table = spanMembers[name] (FrozenMap.forKeyComparer StringComparer.OrdinalIgnoreCase) pairs + pairs[0] <- KeyValuePair ("one", "changed") + Assert.AreEqual("uno", FrozenMap.find "ONE" table, $"%s{name} must copy the span") + + [] + member _.``the span members create an empty map with the comparer for no bindings`` (name : string) = + let table = spanMembers[name] (FrozenMap.forKeyComparer StringComparer.OrdinalIgnoreCase) [||] + + Assert.IsTrue (FrozenMap.isEmpty table, $"%s{name} must create an empty map for no bindings") + + Assert.AreSame>( + StringComparer.OrdinalIgnoreCase, + FrozenMap.keyComparer table, + $"%s{name} must give the empty map the key comparer of the struct" + ) + + [] + member _.``the span members keep the first key and the last value of the keys that the comparer makes equal`` + (name : string) + = + // The copies tell the two bindings apart; the keys are equal in the comparer of the struct (ADR 0001, D14) + let firstKey = MapInputs.Copy "one" + let lastValue = MapInputs.Copy "dos" + + let table = + spanMembers[name] (FrozenMap.forKeyComparer StringComparer.OrdinalIgnoreCase) [| + KeyValuePair (firstKey, "uno") + KeyValuePair (MapInputs.Copy "ONE", lastValue) + |] + + let struct (key, value) = + Assert.ContainsSingle (FrozenMap.toArray table, $"%s{name} must merge the bindings of the equal keys") + + Assert.AreSame (firstKey, key, $"%s{name} must keep the first key") + Assert.AreSame (lastValue, value, $"%s{name} must keep the last value") + + [] + member _.``ofSeqBy rejects the keys that the comparer of the struct makes equal`` () = + let comparers = FrozenMap.forKeyComparer StringComparer.OrdinalIgnoreCase + + Assert.AreEqual( + 2, + FrozenMap.count (comparers.ofSeqBy id [| "a"; "b" |]), + "ofSeqBy must accept the keys that differ in the key comparer" + ) + + Assert.ThrowsExactly( + Action (fun () -> comparers.ofSeqBy id [| "a"; "A" |] |> ignore), + "ofSeqBy must reject the keys that the key comparer makes equal, as the BCL does" + ) + |> ignore diff --git a/tests/FSharp.Collections.Immutable.Tests/FrozenMap/FrozenMapComputationExpressionTests.fs b/tests/FSharp.Collections.Immutable.Tests/FrozenMap/FrozenMapComputationExpressionTests.fs new file mode 100644 index 00000000..ca97635f --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/FrozenMap/FrozenMapComputationExpressionTests.fs @@ -0,0 +1,416 @@ +namespace FSharp.Collections.Immutable.Tests + +open System +open System.Collections.Generic +open FSharp.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// The `frozenMap { }` computation expression. +[] +type FrozenMapComputationExpressionTests () = + + /// The bindings of a map of integer keys, sorted by key. + static let bindings (table : FrozenMap) = MapInputs.SortByKey (FrozenMap.toArray table) + + /// The expressions that bind a key to the first value and then a key that the default comparer finds equal to the second + /// value, by the member that sets the second binding. + static let equalKeyExpressions : Map string -> string -> FrozenMap> = + Map.ofArray [| + "yield of a struct pair", + (fun firstKey secondKey value -> frozenMap { + yield struct (firstKey, "uno") + yield struct (secondKey, value) + }) + "yield of a key/value pair", + (fun firstKey secondKey value -> frozenMap { + yield struct (firstKey, "uno") + yield KeyValuePair (secondKey, value) + }) + "yield! of a sequence of struct pairs", + (fun firstKey secondKey value -> frozenMap { + yield struct (firstKey, "uno") + yield! Seq.singleton (struct (secondKey, value)) + }) + "yield! of an array of struct pairs", + (fun firstKey secondKey value -> frozenMap { + yield struct (firstKey, "uno") + yield! [| struct (secondKey, value) |] + }) + "yield! of key/value pairs", + (fun firstKey secondKey value -> frozenMap { + yield struct (firstKey, "uno") + yield! Seq.singleton (KeyValuePair (secondKey, value)) + }) + "yield! of a frozen map", + (fun firstKey secondKey value -> frozenMap { + yield struct (firstKey, "uno") + yield! FrozenMap.singleton secondKey value + }) + |] + + /// 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 () -> + frozenMap { yield! Unchecked.defaultof> } + |> ignore) + "yield! of a sequence of key/value pairs", + struct ("pairs", + fun () -> + frozenMap { yield! Unchecked.defaultof>> } + |> ignore) + "for", + struct ("source", + fun () -> + frozenMap { + for item in Unchecked.defaultof> do + yield struct (item, "x") + } + |> ignore) + |] + + /// The expressions that take an array or a frozen map, by name, with the name of the parameter and the expression on a + /// null one. They are not sequences by type, so the contract of the sequences does not reach them. The expression + /// rejects the input when the member runs, and not when the buffer enumerates it. + static let nullCollectionExpressions : Map unit))> = + Map.ofArray [| + "yield! of an array of struct pairs", + struct ("pairs", + fun () -> + frozenMap { yield! Unchecked.defaultof } + |> ignore) + "yield! of a frozen map", + struct ("table", + fun () -> + frozenMap { yield! Unchecked.defaultof> } + |> 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 () -> frozenMap { yield! defaultStructPairs } |> ignore) + "yield! of a sequence of key/value pairs", struct ("pairs", fun () -> frozenMap { yield! defaultPairs } |> ignore) + "for", + struct ("source", + fun () -> + frozenMap { + for item in defaultKeys do + yield struct (item, "x") + } + |> ignore) + "for (default flat list of strings as objects)", + struct ("source", + fun () -> + frozenMap { + for item in FlatListOracles.defaultStringsAsObjects do + yield struct (item, "x") + } + |> ignore) + |] + + /// The names of the members that set a binding, as the rows of the test of the keys that the default comparer finds + /// equal. + static member EqualKeyExpressions = equalKeyExpressions.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 + + /// The names of the expressions that take an array or a frozen map and are given a null one, as the rows of their test. + static member NullCollectionExpressions = nullCollectionExpressions.Keys + + [] + member _.``an expression without bindings is empty`` () = + let table : FrozenMap = frozenMap { () } + Assert.IsTrue (FrozenMap.isEmpty table, "frozenMap { () } must be empty") + + [] + member _.``yield sets struct pairs and key value pairs`` () = + let table = frozenMap { + yield struct (1, "one") + yield KeyValuePair (2, "two") + } + + CollectionAssert.AreEqual ( + [| struct (1, "one"); struct (2, "two") |], + bindings table, + "frozenMap must set the yielded bindings" + ) + + [] + member _.``implicit yields set the bindings`` () = + let table = frozenMap { + struct (1, "one") + struct (2, "two") + } + + CollectionAssert.AreEqual ( + [| struct (1, "one"); struct (2, "two") |], + bindings table, + "frozenMap must set the implicitly yielded bindings" + ) + + [] + member _.``a later binding of a key replaces an earlier one`` () = + let table = frozenMap { + yield struct (1, "one") + yield struct (1, "uno") + yield KeyValuePair (1, "eins") + } + + CollectionAssert.AreEqual ([| struct (1, "eins") |], bindings table, "frozenMap must keep the last binding of a key") + + [] + member _.``a key that the default comparer finds equal keeps the earlier key and takes the later value`` + (expression : string) + = + // The indexer of the dictionary keeps the earlier key and replaces the value, and ToFrozenDictionary freezes both + // (ADR 0001, D14); the copies are equal in the default comparer and tell the two keys apart + let firstKey = MapInputs.Copy "one" + let value = MapInputs.Copy "dos" + let table = equalKeyExpressions[expression] firstKey (MapInputs.Copy "one") value + + let struct (key, kept) = + Assert.ContainsSingle (FrozenMap.toArray table, "frozenMap must merge the bindings of the equal keys") + + Assert.AreSame (firstKey, key, "frozenMap must keep the earlier key") + Assert.AreSame (value, kept, "frozenMap must take the later value") + + [] + member _.``yield! and for reject a null sequence`` (name : string) = + let struct (paramName, run) = nullSequenceExpressions[name] + FlatListAssert.RejectsNull ($"frozenMap %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 FrozenMap do, when the expression runs it and not when the buffer does + let struct (paramName, run) = defaultSequenceExpressions[name] + FlatListAssert.RejectsNull ($"frozenMap %s{name}", paramName, run) + + [] + member _.``yield! rejects a null array and a null frozen map`` (name : string) = + // A null array or frozen map fails with NullReferenceException when the buffer enumerates it; the member rejects it + // as it does a null sequence, when the expression runs it + let struct (paramName, run) = nullCollectionExpressions[name] + FlatListAssert.RejectsNull ($"frozenMap %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 = frozenMap { + 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, + "frozenMap 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 = frozenMap { + for item in widened do + yield struct (item, 1) + } + + CollectionAssert.AreEquivalent ( + [| "a" :> obj; "b" :> obj |], + FrozenMap.keys objectTable |> Seq.toArray, + "frozenMap must take the elements of a flat list of strings as objects in for" + ) + + [] + member _.``yield! sets the bindings of sequences, arrays, maps and other expressions`` () = + let inner = frozenMap { yield struct (7, "seven") } + let dictionary = Dictionary() + dictionary[6] <- "six" + + let table = frozenMap { + yield! [ struct (1, "one"); struct (2, "two") ] + yield! [| struct (3, "three") |] + yield! [ KeyValuePair (4, "four") ] + yield! FrozenMap.singleton 5 "five" + yield! dictionary + yield! inner + yield! HashMap.singleton 8 "eight" + } + + CollectionAssert.AreEqual ( + [| 1..8 |], + MapInputs.KeysOf (bindings table), + "frozenMap must set the bindings of every yield!" + ) + + [] + member _.``for sets the bindings of the body for each item`` () = + let table = frozenMap { + 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, + "frozenMap must run the body of for for each item" + ) + + [] + member _.``while sets the bindings while the guard holds`` () = + let mutable count = 0 + + let table = frozenMap { + while count < 3 do + count <- count + 1 + yield struct (count, string count) + } + + CollectionAssert.AreEqual ( + [| 1; 2; 3 |], + MapInputs.KeysOf (bindings table), + "frozenMap must run the body of while while the guard holds" + ) + + [] + member _.``if without else and following bindings combine`` () = + let table = frozenMap { + 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), + "frozenMap must combine conditional and plain bindings" + ) + + [] + member _.``try with sets the bindings of the handler after an exception`` () = + let table = frozenMap { + try + yield struct (1, "one") + failwith "fails" + with _ -> + yield struct (2, "two") + } + + CollectionAssert.AreEqual ( + [| 1; 2 |], + MapInputs.KeysOf (bindings table), + "frozenMap must keep the bindings before the exception and add the handler's" + ) + + [] + member _.``try finally runs the compensation`` () = + let mutable compensated = false + + let table = frozenMap { + try + yield struct (1, "one") + finally + compensated <- true + } + + Assert.IsTrue (compensated, "frozenMap must run the compensation of try finally") + + CollectionAssert.AreEqual ( + [| 1 |], + MapInputs.KeysOf (bindings table), + "frozenMap must keep the bindings of the body of try finally" + ) + + [] + member _.``use disposes the resource after the body`` () = + let mutable disposed = false + + let table = frozenMap { + use _resource = { + new IDisposable with + member _.Dispose () = disposed <- true + } + + yield struct (1, "one") + } + + Assert.IsTrue (disposed, "frozenMap must dispose the resource of use") + + CollectionAssert.AreEqual ( + [| 1 |], + MapInputs.KeysOf (bindings table), + "frozenMap must keep the bindings of the body of use" + ) + + [] + member _.``frozenMap compares the keys with the default comparer`` () = + let table = frozenMap { yield struct ("one", 1) } + + Assert.AreSame>( + EqualityComparer.Default, + FrozenMap.keyComparer table, + "frozenMap must compare the keys with the default comparer" + ) + + [] + member _.``FrozenMapCE compares the keys with the comparer that it takes`` () = + let ignoreCase = FrozenMapCE StringComparer.OrdinalIgnoreCase + + let table = ignoreCase { + yield struct ("one", 1) + yield struct ("ONE", 2) + yield struct ("two", 3) + } + + Assert.AreSame>( + StringComparer.OrdinalIgnoreCase, + FrozenMap.keyComparer table, + "FrozenMapCE must keep the key comparer" + ) + + Assert.AreEqual(2, FrozenMap.count table, "FrozenMapCE must merge the keys that the comparer considers equal") + Assert.AreEqual(2, FrozenMap.find "One" table, "FrozenMapCE must keep the last value of equal keys") + + CollectionAssert.AreEquivalent ( + [| "one"; "two" |], + FlatList.toArray (FrozenMap.keys table), + "FrozenMapCE must keep the first of equal keys" + ) + + [] + member _.``FrozenMapCE rejects a null key comparer`` () = + let ex = + Assert.ThrowsExactly( + Action (fun () -> + FrozenMapCE(Unchecked.defaultof>) + |> ignore + ), + "FrozenMapCE must reject a null key comparer" + ) + + Assert.AreEqual("keyComparer", ex.ParamName, "FrozenMapCE must name the key comparer") diff --git a/tests/FSharp.Collections.Immutable.Tests/FrozenMap/FrozenMapContractTests.fs b/tests/FSharp.Collections.Immutable.Tests/FrozenMap/FrozenMapContractTests.fs new file mode 100644 index 00000000..7c8eb622 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/FrozenMap/FrozenMapContractTests.fs @@ -0,0 +1,24 @@ +namespace FSharp.Collections.Immutable.Tests + +open FSharp.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// The reading contract for `FrozenMap`. +[] +type FrozenMapReadingTests () = + inherit MapContract> (FrozenMapOps.reading) + +/// The transformation contract for `FrozenMap`. +[] +type FrozenMapTransformTests () = + inherit MapTransformContract> (FrozenMapOps.transform) + +/// The creation contract for `FrozenMap` and its extension members, with the default comparer. +[] +type FrozenMapCreationTests () = + inherit MapCreationContract> (FrozenMapOps.creation) + +/// The creation contract for the members of `CustomComparersFrozenMap`, with a key comparer. +[] +type CustomComparersFrozenMapCreationTests () = + inherit MapCreationContract> (FrozenMapOps.customComparersCreation) diff --git a/tests/FSharp.Collections.Immutable.Tests/FrozenMap/FrozenMapExtensionsTests.fs b/tests/FSharp.Collections.Immutable.Tests/FrozenMap/FrozenMapExtensionsTests.fs new file mode 100644 index 00000000..5f3f207d --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/FrozenMap/FrozenMapExtensionsTests.fs @@ -0,0 +1,163 @@ +namespace FSharp.Collections.Immutable.Tests + +open System +open System.Collections.Generic +open FSharp.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// The static members of `FrozenMapExtensions`, which `FrozenMap.ofArray`, `FrozenMap.ofList` and `FrozenMap.ofSeq` reach +/// through the abbreviation, called as user code calls them (ADR 0001, D13). +[] +type FrozenMapExtensionsTests () = + + /// The creations of a frozen map from bindings that repeat the key "a", by the member and the shape that they call. + static let repeatedKeyCreations : Map FrozenMap> = + Map.ofArray [| + "ofArray of key/value pairs", + (fun () -> FrozenMap.ofArray [| KeyValuePair ("a", 1); KeyValuePair ("b", 2); KeyValuePair ("a", 3) |]) + "ofArray of reference tuples", (fun () -> FrozenMap.ofArray [| "a", 1; "b", 2; "a", 3 |]) + "ofArray of struct tuples", (fun () -> FrozenMap.ofArray [| struct ("a", 1); struct ("b", 2); struct ("a", 3) |]) + "ofList of key/value pairs", + (fun () -> FrozenMap.ofList [ KeyValuePair ("a", 1); KeyValuePair ("b", 2); KeyValuePair ("a", 3) ]) + "ofList of reference tuples", (fun () -> FrozenMap.ofList [ "a", 1; "b", 2; "a", 3 ]) + "ofList of struct tuples", (fun () -> FrozenMap.ofList [ struct ("a", 1); struct ("b", 2); struct ("a", 3) ]) + "ofSeq of key/value pairs", + (fun () -> FrozenMap.ofSeq (Seq.ofArray [| KeyValuePair ("a", 1); KeyValuePair ("b", 2); KeyValuePair ("a", 3) |])) + "ofSeq of reference tuples", (fun () -> FrozenMap.ofSeq (Seq.ofArray [| "a", 1; "b", 2; "a", 3 |])) + "ofSeq of struct tuples", + (fun () -> FrozenMap.ofSeq (Seq.ofArray [| struct ("a", 1); struct ("b", 2); struct ("a", 3) |])) + |] + + /// The creations of a frozen map of "one" and "two" in a pipeline, by the member and the source of the bindings. + static let pipelines : Map FrozenMap> = + Map.ofArray [| + "ofArray of an array of key/value pairs", + (fun () -> + [| KeyValuePair ("one", 1); KeyValuePair ("two", 2) |] + |> FrozenMap.ofArray + ) + "ofArray of an array of reference tuples", (fun () -> [| "one", 1; "two", 2 |] |> FrozenMap.ofArray) + "ofArray of an array of struct tuples", + (fun () -> + [| struct ("one", 1); struct ("two", 2) |] + |> FrozenMap.ofArray + ) + "ofList of a list of key/value pairs", + (fun () -> + [ KeyValuePair ("one", 1); KeyValuePair ("two", 2) ] + |> FrozenMap.ofList + ) + "ofList of a list of reference tuples", (fun () -> [ "one", 1; "two", 2 ] |> FrozenMap.ofList) + "ofList of a list of struct tuples", (fun () -> [ struct ("one", 1); struct ("two", 2) ] |> FrozenMap.ofList) + "ofSeq of a sequence of key/value pairs", + (fun () -> + Seq.ofArray [| KeyValuePair ("one", 1); KeyValuePair ("two", 2) |] + |> FrozenMap.ofSeq + ) + "ofSeq of a sequence of reference tuples", (fun () -> Seq.ofArray [| "one", 1; "two", 2 |] |> FrozenMap.ofSeq) + "ofSeq of a sequence of struct tuples", + (fun () -> + Seq.ofArray [| struct ("one", 1); struct ("two", 2) |] + |> FrozenMap.ofSeq + ) + "ofSeq of a dictionary", (fun () -> dict [| "one", 1; "two", 2 |] |> FrozenMap.ofSeq) + "ofSeq of an F# map", (fun () -> Map.ofArray [| "one", 1; "two", 2 |] |> FrozenMap.ofSeq) + "ofSeq of a frozen map", + (fun () -> + FrozenMap.ofArray [| "one", 1; "two", 2 |] + |> FrozenMap.ofSeq + ) + |] + + /// The creations of a frozen map from the given bindings, by the member and the shape that they call. + static let shapedCreations : Map FrozenMap> = + Map.ofArray [| + "ofArray of key/value pairs", (fun bindings -> FrozenMap.ofArray (Array.map MapInputs.ToKeyValuePair bindings)) + "ofArray of reference tuples", (fun bindings -> FrozenMap.ofArray (Array.map MapInputs.ToTuple bindings)) + "ofArray of struct tuples", (fun bindings -> FrozenMap.ofArray bindings) + "ofList of key/value pairs", + (fun bindings -> + FrozenMap.ofList ( + bindings + |> Array.map MapInputs.ToKeyValuePair + |> List.ofArray + ) + ) + "ofList of reference tuples", + (fun bindings -> FrozenMap.ofList (bindings |> Array.map MapInputs.ToTuple |> List.ofArray)) + "ofList of struct tuples", (fun bindings -> FrozenMap.ofList (List.ofArray bindings)) + "ofSeq of key/value pairs", (fun bindings -> FrozenMap.ofSeq (Seq.map MapInputs.ToKeyValuePair bindings)) + "ofSeq of reference tuples", (fun bindings -> FrozenMap.ofSeq (Seq.map MapInputs.ToTuple bindings)) + "ofSeq of struct tuples", (fun bindings -> FrozenMap.ofSeq (Seq.ofArray bindings)) + |] + + /// The bindings of a frozen map as reference tuples, sorted by key, to compare them with those of an F# map. + static let sortedBindings (table : FrozenMap) = + FrozenMap.toArray table + |> Array.map (fun struct (key, value) -> key, value) + |> Array.sortBy fst + + /// 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 pipelines, as 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 keys 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 frozen map must keep the last binding of a key, as Map does" + ) + + [] + member _.``every shape keeps the earlier key and the later value of keys that the default comparer finds equal`` + (creation : string) + = + // The indexer of the dictionary keeps the earlier key and replaces the value, and ToFrozenDictionary freezes both + // (ADR 0001, D14); the copies are equal in the default comparer and tell the two keys apart + let firstKey = MapInputs.Copy "one" + let secondValue = MapInputs.Copy "dos" + + let table = + shapedCreations[creation] [| struct (firstKey, "uno"); struct (MapInputs.Copy "one", secondValue) |] + + let struct (key, value) = + Assert.ContainsSingle (FrozenMap.toArray table, "The frozen map must merge the bindings of the equal keys") + + Assert.AreSame (firstKey, key, "The frozen map must keep the earlier key") + Assert.AreSame (secondValue, value, "The frozen map must keep the later value") + + [] + member _.``the creation members resolve the shape of the bindings in a pipeline`` (pipeline : string) = + CollectionAssert.AreEqual ( + [| "one", 1; "two", 2 |], + sortedBindings (pipelines[pipeline]()), + "The pipeline must create the bindings of its source" + ) + + [] + member _.``ofSeq returns a frozen map with the default comparer itself`` () = + let table = FrozenMap.ofArray [| "one", 1; "two", 2 |] + Assert.AreSame (table, FrozenMap.ofSeq table, "FrozenMap.ofSeq must return a frozen map with the default comparer itself") + + [] + member _.``ofSeq gives a frozen map with another comparer the default comparer`` () = + let ignoreCase = + (FrozenMap.forKeyComparer StringComparer.OrdinalIgnoreCase).ofArray [| "one", 1; "two", 2 |] + + let table = FrozenMap.ofSeq ignoreCase + + Assert.AreSame>( + EqualityComparer.Default, + FrozenMap.keyComparer table, + "FrozenMap.ofSeq must give a frozen map the default key comparer" + ) + + CollectionAssert.AreEqual (sortedBindings ignoreCase, sortedBindings table, "FrozenMap.ofSeq must keep the bindings") + Assert.IsFalse (FrozenMap.containsKey "ONE" table, "FrozenMap.ofSeq must look the keys up with the default comparer") diff --git a/tests/FSharp.Collections.Immutable.Tests/FrozenMap/FrozenMapOps.fs b/tests/FSharp.Collections.Immutable.Tests/FrozenMap/FrozenMapOps.fs new file mode 100644 index 00000000..cc413c3d --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/FrozenMap/FrozenMapOps.fs @@ -0,0 +1,132 @@ +/// The functions of the `FrozenMap` module for the contract tests. +/// A frozen map has no incremental changes, so the module has no mutation facet (ADR 0001, D1), and it has no value +/// comparer and no builder, so the creation facet holds neither. +[] +module FSharp.Collections.Immutable.Tests.FrozenMapOps + +open System.Collections.Generic +open FSharp.Collections.Immutable + +#if NET10_0_OR_GREATER +/// An equality comparer of `int64` keys that looks them up by `int` alternates, for the alternate lookups (ADR 0001, D11). +let int64ByInt32 = { + new IEqualityComparer with + member _.Equals (left, right) = left = right + member _.GetHashCode key = key.GetHashCode () + interface IAlternateEqualityComparer with + member _.Equals (alternate : int, key : int64) = int64 alternate = key + member _.GetHashCode (alternate : int) = (int64 alternate).GetHashCode() + member _.Create (alternate : int) = int64 alternate +} +#endif + +/// The functions of the reading facet. `FrozenMap` names its map parameter `table`, as the `Map` module does. +let reading : MapOps> = { + Name = nameof FrozenMap + ParamName = id + OfSeq = FrozenMap.ofSeq + ToArray = FrozenMap.toArray + + Count = FrozenMap.count + IsEmpty = FrozenMap.isEmpty + // The flat lists of the keys and the values are structs, which the record takes as sequences + Keys = fun table -> FrozenMap.keys table :> seq<_> + Values = fun table -> FrozenMap.values table :> seq<_> + + Find = FrozenMap.find + TryFind = FrozenMap.tryFind + + ContainsBinding = FrozenMap.containsBinding + ContainsKey = FrozenMap.containsKey + Exists = FrozenMap.exists + FindKey = FrozenMap.findKey + Forall = FrozenMap.forall + Pick = FrozenMap.pick + TryFindKey = FrozenMap.tryFindKey + TryPick = FrozenMap.tryPick + + Fold = FrozenMap.fold + FoldBack = FrozenMap.foldBack + Iter = FrozenMap.iter + + ToList = FrozenMap.toList + ToSeq = FrozenMap.toSeq +} + +/// The functions of the transformation facet. +let transform : MapTransformOps> = { + Name = nameof FrozenMap + ParamName = id + OfSeq = FrozenMap.ofSeq + ToArray = FrozenMap.toArray + + Choose = fun chooser table -> FrozenMap.choose chooser table |> FrozenMap.toArray + Filter = FrozenMap.filter + Map = fun mapping table -> FrozenMap.map mapping table |> FrozenMap.toArray + Partition = FrozenMap.partition +} + +/// The functions of the creation facet with the default comparer: the functions of the module and the overloads of the +/// extension members, one shape per field (ADR 0001, D13). +let creation : MapCreationOps> = { + Name = nameof FrozenMap + ParamName = id + OfSeq = FrozenMap.ofSeq + ToArray = FrozenMap.toArray + ContainsKey = FrozenMap.containsKey + EquivalentKey = id + + Empty = fun () -> FrozenMap.empty + KeyValuePairs = { + OfArray = FrozenMap.ofArray + OfList = FrozenMap.ofList + OfSeq = FrozenMap.ofSeq + } + Tuples = { + OfArray = FrozenMap.ofArray + OfList = FrozenMap.ofList + OfSeq = FrozenMap.ofSeq + } + StructTuples = { + OfArray = FrozenMap.ofArray + OfList = FrozenMap.ofList + OfSeq = FrozenMap.ofSeq + } + OfSeqBy = FrozenMap.ofSeqBy + Singleton = FrozenMap.singleton +} + +/// The comparer struct whose key comparer compares the absolute values of the keys. A frozen map takes the same type of +/// key comparer as a hash map. +let private absoluteKeys : CustomComparersFrozenMap = + FrozenMap.forKeyComparer HashMapOps.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 CustomComparersFrozenMap + ParamName = id + OfSeq = FrozenMap.ofSeq + ToArray = FrozenMap.toArray + ContainsKey = FrozenMap.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/FrozenMap/FrozenMapOptionTests.fs b/tests/FSharp.Collections.Immutable.Tests/FrozenMap/FrozenMapOptionTests.fs new file mode 100644 index 00000000..66e78d47 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/FrozenMap/FrozenMapOptionTests.fs @@ -0,0 +1,127 @@ +namespace FSharp.Collections.Immutable.Tests + +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 `FrozenMap` module of the `Option` namespace. +[] +type FrozenMapOptionTests () = + + static let table = FSharp.Collections.Immutable.FrozenMap.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 = FrozenMap.tryFind key table + + OverrideAssert.OptionMatches ( + FSharp.Collections.Immutable.FrozenMap.tryFind key table, + found, + "Option.FrozenMap.tryFind must match the default" + ) + + [] + member _.``pick takes a chooser of options`` () = + let picked : int = FrozenMap.pick negatedIfNegative table + + Assert.AreEqual( + FSharp.Collections.Immutable.FrozenMap.pick + (fun key value -> negatedIfNegative key value |> ValueOption.ofOption) + table, + picked, + "Option.FrozenMap.pick must pick as the default does" + ) + + [] + member _.``tryFindKey returns an option`` (predicate : BindingPredicate) = + let found : int option = FrozenMap.tryFindKey predicate.Matches table + + OverrideAssert.OptionMatches ( + FSharp.Collections.Immutable.FrozenMap.tryFindKey predicate.Matches table, + found, + "Option.FrozenMap.tryFindKey must match the default" + ) + + [] + member _.``tryPick takes a chooser of options and returns an option`` () = + let found : int option = FrozenMap.tryPick negatedIfNegative table + + OverrideAssert.OptionMatches ( + FSharp.Collections.Immutable.FrozenMap.tryPick + (fun key value -> negatedIfNegative key value |> ValueOption.ofOption) + table, + found, + "Option.FrozenMap.tryPick must match the default" + ) + + let nothing : int option = FrozenMap.tryPick (fun _ _ -> None) table + Assert.AreEqual(None, nothing, "Option.FrozenMap.tryPick must return None when the chooser returns nothing") + + [] + member _.``choose takes a chooser of options`` () = + let chosen : FrozenMap = FrozenMap.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.FrozenMap.toArray chosen), + "Option.FrozenMap.choose must keep the chosen values" + ) + + [] + member _.``the functions without an option keep resolving to the other modules`` () = + let mapped : FrozenMap = FrozenMap.map (fun _ (value : string) -> value.Length) table + let pairs : (int * string) list = FrozenMap.toList table + + Assert.AreEqual( + MapInputs.Several.Length, + FSharp.Collections.Immutable.FrozenMap.count mapped, + "FrozenMap.map must resolve to the default module" + ) + + CollectionAssert.AreEqual ( + Map.toArray oracle, + List.toArray pairs |> Array.sortBy fst, + "FrozenMap.toList must resolve to the Tuple module" + ) + +#if NET10_0_OR_GREATER + [] + member _.``tryAlternateLookup returns Some for a comparer with the alternate key type`` () = + let longs = (FrozenMap.forKeyComparer FrozenMapOps.int64ByInt32).ofArray [| struct (1L, "one") |] + + let found : FrozenMap.AlternateLookup option = + FrozenMap.tryAlternateLookup longs + + match found with + | Some lookup -> Assert.IsTrue (lookup.ContainsKey 1, "Option.FrozenMap.tryAlternateLookup must return a working lookup") + | None -> Assert.Fail "Option.FrozenMap.tryAlternateLookup must return Some for a comparer with the alternate key type" + + [] + member _.``tryAlternateLookup returns None for a comparer without the alternate key type`` () = + let structural = (FrozenMap.forKeyComparer HashIdentity.Structural).ofArray [| struct (1L, "one") |] + + let none : FrozenMap.AlternateLookup option = + FrozenMap.tryAlternateLookup structural + + Assert.IsTrue (none.IsNone, "Option.FrozenMap.tryAlternateLookup must return None for a comparer without the type") +#endif diff --git a/tests/FSharp.Collections.Immutable.Tests/FrozenMap/FrozenMapTests.fs b/tests/FSharp.Collections.Immutable.Tests/FrozenMap/FrozenMapTests.fs new file mode 100644 index 00000000..2ba35dbf --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/FrozenMap/FrozenMapTests.fs @@ -0,0 +1,824 @@ +namespace FSharp.Collections.Immutable.Tests + +open System +open System.Collections.Generic +open System.Runtime.InteropServices +open FSharp.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// The behaviour of `FrozenMap` that the shared contracts do not cover: null inputs, the key comparer, the arrays of the +/// keys and the values, and the repeated keys of the functions that wrap the BCL. +[] +type FrozenMapTests () = + + static let nullTable = Unchecked.defaultof> + + /// A map whose keys ignore the case. + static let ignoreCaseTable () = + (FrozenMap.forKeyComparer StringComparer.OrdinalIgnoreCase).ofArray [| "one", "uno"; "Two", "dos"; "three", "tres" |] + + /// A map with the default comparer. + static let defaultTable () = FrozenMap.ofArray [| struct ("one", "uno"); struct ("two", "dos") |] + + /// The array under a flat list, to tell whether two flat lists share their storage. + static let underlying (array : FlatList<'T>) = ImmutableCollectionsMarshal.AsArray array + + /// 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 FrozenMap.choose, + (fun () -> + FrozenMap.choose (fun _ value -> ValueSome value) nullTable + |> ignore + ) + nameof FrozenMap.containsBinding, (fun () -> FrozenMap.containsBinding "a" "b" nullTable |> ignore) + nameof FrozenMap.containsKey, (fun () -> FrozenMap.containsKey "a" nullTable |> ignore) + nameof FrozenMap.containsValue, (fun () -> FrozenMap.containsValue "a" nullTable |> ignore) + nameof FrozenMap.copyTo, (fun () -> FrozenMap.copyTo [||] 0 nullTable) + nameof FrozenMap.count, (fun () -> FrozenMap.count nullTable |> ignore) + nameof FrozenMap.exists, (fun () -> FrozenMap.exists (fun _ _ -> true) nullTable |> ignore) + nameof FrozenMap.filter, (fun () -> FrozenMap.filter (fun _ _ -> true) nullTable |> ignore) + nameof FrozenMap.find, (fun () -> FrozenMap.find "a" nullTable |> ignore) + nameof FrozenMap.findKey, (fun () -> FrozenMap.findKey (fun _ _ -> true) nullTable |> ignore) + nameof FrozenMap.fold, + (fun () -> + FrozenMap.fold (fun state _ _ -> state) 0 nullTable + |> ignore + ) + nameof FrozenMap.foldBack, + (fun () -> + FrozenMap.foldBack (fun _ _ state -> state) nullTable 0 + |> ignore + ) + nameof FrozenMap.forall, (fun () -> FrozenMap.forall (fun _ _ -> true) nullTable |> ignore) + nameof FrozenMap.isEmpty, (fun () -> FrozenMap.isEmpty nullTable |> ignore) + nameof FrozenMap.iter, (fun () -> FrozenMap.iter (fun _ _ -> ()) nullTable) + nameof FrozenMap.keyComparer, (fun () -> FrozenMap.keyComparer nullTable |> ignore) + nameof FrozenMap.keys, (fun () -> FrozenMap.keys nullTable |> ignore) + nameof FrozenMap.map, (fun () -> FrozenMap.map (fun _ value -> value) nullTable |> ignore) + nameof FrozenMap.partition, (fun () -> FrozenMap.partition (fun _ _ -> true) nullTable |> ignore) + nameof FrozenMap.pick, + (fun () -> + FrozenMap.pick (fun _ value -> ValueSome value) nullTable + |> ignore + ) + nameof FrozenMap.toArray, (fun () -> FrozenMap.toArray nullTable |> ignore) + nameof FrozenMap.toList, (fun () -> FrozenMap.toList nullTable |> ignore) + nameof FrozenMap.toSeq, (fun () -> FrozenMap.toSeq nullTable |> ignore) + nameof FrozenMap.tryFind, (fun () -> FrozenMap.tryFind "a" nullTable |> ignore) + nameof FrozenMap.tryFindKey, (fun () -> FrozenMap.tryFindKey (fun _ _ -> true) nullTable |> ignore) + nameof FrozenMap.tryPick, + (fun () -> + FrozenMap.tryPick (fun _ value -> ValueSome value) nullTable + |> ignore + ) + nameof FrozenMap.values, (fun () -> FrozenMap.values 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))> = + Map.ofArray [| + "ofArray of key/value pairs", + struct ("elements", + fun () -> + FrozenMap.ofArray (Unchecked.defaultof array>) + |> ignore) + "ofArray of reference tuples", + struct ("elements", + fun () -> + FrozenMap.ofArray (Unchecked.defaultof<(string * string) array>) + |> ignore) + "ofArray of struct tuples", + struct ("elements", + fun () -> + FrozenMap.ofArray (Unchecked.defaultof) + |> ignore) + "ofSeq of key/value pairs", + struct ("elements", + fun () -> + FrozenMap.ofSeq (Unchecked.defaultof>>) + |> ignore) + "ofSeq of reference tuples", + struct ("elements", + fun () -> + FrozenMap.ofSeq (Unchecked.defaultof>) + |> ignore) + "ofSeq of struct tuples", + struct ("elements", + fun () -> + FrozenMap.ofSeq (Unchecked.defaultof>) + |> ignore) + nameof FrozenMap.ofSeqBy, + struct ("source", + fun () -> + FrozenMap.ofSeqBy id (Unchecked.defaultof>) + |> 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 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. The extension overloads of ofSeq run through the struct. + static let defaultSequenceCalls : Map unit))> = + let defaultPairs = Unchecked.defaultof>> :> seq<_> + let defaultTuples = Unchecked.defaultof> :> seq<_> + let defaultStructTuples = Unchecked.defaultof> :> seq<_> + let defaultKeys = Unchecked.defaultof> :> seq<_> + + Map.ofArray [| + "ofSeq of key/value pairs", struct ("elements", fun () -> FrozenMap.ofSeq defaultPairs |> ignore) + "ofSeq of reference tuples", struct ("elements", fun () -> FrozenMap.ofSeq defaultTuples |> ignore) + "ofSeq of struct tuples", struct ("elements", fun () -> FrozenMap.ofSeq defaultStructTuples |> ignore) + nameof FrozenMap.ofSeqBy, struct ("source", fun () -> FrozenMap.ofSeqBy id defaultKeys |> ignore) + $"%s{nameof FrozenMap.ofSeqBy} (default flat list of strings as objects)", + struct ("source", + fun () -> + FrozenMap.ofSeqBy id FlatListOracles.defaultStringsAsObjects + |> ignore) + |] + + /// The creation functions without a comparer, by name and shape of the bindings. + static let defaultCreations : Map FrozenMap> = + Map.ofArray [| + nameof FrozenMap.empty, (fun () -> FrozenMap.empty) + "ofArray of key/value pairs", (fun () -> FrozenMap.ofArray [| KeyValuePair ("a", "b") |]) + "ofArray of reference tuples", (fun () -> FrozenMap.ofArray [| ("a", "b") |]) + "ofArray of struct tuples", (fun () -> FrozenMap.ofArray [| struct ("a", "b") |]) + "ofList of key/value pairs", (fun () -> FrozenMap.ofList [ KeyValuePair ("a", "b") ]) + "ofList of reference tuples", (fun () -> FrozenMap.ofList [ ("a", "b") ]) + "ofList of struct tuples", (fun () -> FrozenMap.ofList [ struct ("a", "b") ]) + "ofSeq of key/value pairs", (fun () -> FrozenMap.ofSeq (dict [| "a", "b" |])) + "ofSeq of reference tuples", (fun () -> FrozenMap.ofSeq (Seq.singleton ("a", "b"))) + "ofSeq of struct tuples", (fun () -> FrozenMap.ofSeq (Seq.singleton (struct ("a", "b")))) + nameof FrozenMap.ofSeqBy, (fun () -> FrozenMap.ofSeqBy id [| "a" |]) + nameof FrozenMap.singleton, (fun () -> FrozenMap.singleton "a" "b") + |] + + /// The key comparer of the result of every transformation of the map whose keys ignore the case, by name. The + /// transformations drop some, all or none of the bindings, and change the values or keep them. + static let resultKeyComparers : Map -> IEqualityComparer> = + Map.ofArray [| + $"%s{nameof FrozenMap.choose} (some values)", + fun table -> + FrozenMap.choose (fun _ (value : string) -> ValueSome value.Length) table + |> FrozenMap.keyComparer + $"%s{nameof FrozenMap.choose} (no value)", + fun table -> + FrozenMap.choose (fun _ _ -> ValueNone) table + |> FrozenMap.keyComparer + $"%s{nameof FrozenMap.filter} (some bindings)", + fun table -> + FrozenMap.filter (fun (key : string) _ -> key.Length > 3) table + |> FrozenMap.keyComparer + $"%s{nameof FrozenMap.filter} (no binding)", + fun table -> + FrozenMap.filter (fun _ _ -> false) table + |> FrozenMap.keyComparer + nameof FrozenMap.map, + fun table -> + FrozenMap.map (fun _ (value : string) -> value.Length) table + |> FrozenMap.keyComparer + $"%s{nameof FrozenMap.partition} (accepted)", + fun table -> + let struct (accepted, _) = FrozenMap.partition (fun (key : string) _ -> key.Length > 3) table + FrozenMap.keyComparer accepted + $"%s{nameof FrozenMap.partition} (rejected)", + fun table -> + let struct (_, rejected) = FrozenMap.partition (fun (key : string) _ -> key.Length > 3) table + FrozenMap.keyComparer rejected + $"%s{nameof FrozenMap.partition} (rejected, nothing)", + fun table -> + let struct (_, nothing) = FrozenMap.partition (fun _ _ -> true) table + FrozenMap.keyComparer nothing + |] + + /// The transformations of a map of integer keys, by name; the key comparer of the map compares the absolute values of + /// the keys. + static let absoluteKeyTransformations : Map -> FrozenMap> = + Map.ofArray [| + nameof FrozenMap.choose, FrozenMap.choose (fun _ value -> ValueSome value) + nameof FrozenMap.filter, FrozenMap.filter (fun key _ -> key > 1) + nameof FrozenMap.map, FrozenMap.map (fun _ value -> value) + $"%s{nameof FrozenMap.partition} (accepted)", + fun table -> + let struct (accepted, _) = FrozenMap.partition (fun key _ -> key % 2 = 0) table + accepted + $"%s{nameof FrozenMap.partition} (rejected)", + fun table -> + let struct (_, rejected) = FrozenMap.partition (fun key _ -> key % 2 = 0) table + rejected + |] + + /// Maps of every kind that the frozen dictionary tells apart, by name: it picks its implementation by the type of the + /// keys, the comparer and the size. + static let tablesOfEveryKind : Map FrozenMap> = + Map.ofArray [| + "an empty map", (fun () -> FrozenMap.empty) + "a small map", (fun () -> FrozenMap.ofArray [| struct ("one", 1); struct ("two", 2) |]) + "a large map", (fun () -> FrozenMap.ofArray [| for index in 1..50 -> struct (string index, index) |]) + "a map with a custom comparer", + (fun () -> + (FrozenMap.forKeyComparer HashIdentity.Structural).ofArray [| + for index in 1..50 -> struct (string index, index) + |] + ) + |] + + /// The creations of a frozen map from the key/value pairs of an array, by the function that takes the span of the array. + /// A span cannot be captured, so each function makes its span itself. + static let spanCreations : Map array -> FrozenMap> = + Map.ofArray [| + nameof FrozenMap.ofReadOnlySpan, (fun pairs -> FrozenMap.ofReadOnlySpan (ReadOnlySpan pairs)) + nameof FrozenMap.ofSpan, (fun pairs -> FrozenMap.ofSpan (Span pairs)) + |] + + /// The creations of a frozen map of string keys from the key/value pairs of an array, by the function that takes the span + /// of the array. + static let stringSpanCreations : Map array -> FrozenMap> = + Map.ofArray [| + nameof FrozenMap.ofReadOnlySpan, (fun pairs -> FrozenMap.ofReadOnlySpan (ReadOnlySpan pairs)) + nameof FrozenMap.ofSpan, (fun pairs -> FrozenMap.ofSpan (Span pairs)) + |] + + /// 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 a comparer, as the rows of their test. + static member DefaultCreations = defaultCreations.Keys + + /// The names of every transformation, as the rows of the test of the key comparer. + static member Transformations = resultKeyComparers.Keys + + /// Every size of a map of a custom comparer with every transformation: the rows of the test of the key comparer of the + /// transformations. The frozen dictionaries of a custom comparer search a few keys linearly and hash more keys. + static member SizesAndAbsoluteKeyTransformations = seq { + for size in [| 3; 30 |] do + for name in absoluteKeyTransformations.Keys do + struct (size, name) + } + + /// The names of the maps of every kind, as the rows of the tests that run on each kind. + static member TablesOfEveryKind = tablesOfEveryKind.Keys + + /// The names of the functions that create a frozen map from a span, as the rows of the tests of the span creations. + static member SpanCreations = spanCreations.Keys + + /// The names of the functions that create a frozen map of string keys from a span, as the rows of the tests of the + /// copies of strings. + static member StringSpanCreations = stringSpanCreations.Keys + + /// Every function that creates a frozen map from a span with every input of the creation tests, the bindings that repeat + /// keys included: the rows of the test of the bindings. The rows of every property are built on each read. + static member SpanCreationsAndInputs = seq { + for name in spanCreations.Keys do + for input in + seq { + yield! MapInputs.All + MapInputs.RepeatedKeys + } do + struct (name, input) + } + + /// Every input and every index of the first binding in the destination of copyTo, with a free slot after the last + /// binding: the rows of the test of the copy. + static member CopyToInputsAndIndexes = seq { + for input in MapInputs.All do + for index in [| 0; 2 |] do + struct (input, index) + } + + [] + 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], $"FrozenMap.%s{name} must reject a null map") + + Assert.AreEqual("table", ex.ParamName, $"FrozenMap.%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, $"FrozenMap.%s{name} must reject a null sequence") + + Assert.AreEqual(paramName, ex.ParamName, $"FrozenMap.%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 ($"FrozenMap.%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 bindingsOf (table : FrozenMap) = + FrozenMap.toArray table + |> Array.sortBy (fun struct (key, _) -> key) + + CollectionAssert.AreEqual ( + [| struct ("a", "b") |], + bindingsOf (FrozenMap.ofSeq (flatList [| KeyValuePair ("a", "b") |])), + "FrozenMap.ofSeq must take the key/value pairs of a flat list" + ) + + CollectionAssert.AreEqual ( + [| struct ("a", "b") |], + bindingsOf (FrozenMap.ofSeq (flatList [| ("a", "b") |])), + "FrozenMap.ofSeq must take the reference tuples of a flat list" + ) + + CollectionAssert.AreEqual ( + [| struct ("a", "b") |], + bindingsOf (FrozenMap.ofSeq (flatList [| struct ("a", "b") |])), + "FrozenMap.ofSeq must take the struct tuples of a flat list" + ) + + CollectionAssert.AreEqual ( + [| struct ("ab", "ab") |], + bindingsOf (FrozenMap.ofSeqBy id (flatList [| "ab" |])), + "FrozenMap.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 [| "ab" |]) + + CollectionAssert.AreEqual ( + [| "ab" :> obj |], + FrozenMap.keys (FrozenMap.ofSeqBy id widened) |> Seq.toArray, + "FrozenMap.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 () -> FrozenMap.find "three" table |> ignore), + "FrozenMap.find must reject a key without a binding" + ) + + Assert.AreEqual( + KeyNotFoundException().Message, + ex.Message, + "FrozenMap.find must throw with the message of Map.find, which does not name the key" + ) + + [] + member _.``the creation functions without a comparer use the default comparer`` (name : string) = + Assert.AreSame>( + EqualityComparer.Default, + FrozenMap.keyComparer (defaultCreations[name]()), + $"FrozenMap.%s{name} must compare the keys with the default comparer" + ) + + // ToFrozenDictionary with a key selector rejects every repeated key, an equal element included (ADR 0001, D14) + [] + member _.``ofSeqBy rejects a repeated key, even of an equal element`` (values : int[]) = + Assert.ThrowsExactly( + Action (fun () -> + FrozenMap.ofSeqBy (fun (value : int) -> string (value % 2)) values + |> ignore + ), + "FrozenMap.ofSeqBy must reject a repeated key, as ToFrozenDictionary with a key selector does" + ) + |> ignore + + [] + member _.``the transformations keep the key comparer`` (name : string) = + Assert.AreSame>( + StringComparer.OrdinalIgnoreCase, + resultKeyComparers[name](ignoreCaseTable ()), + $"FrozenMap.%s{name} must keep the key comparer" + ) + + [] + member _.``the transformations look the keys up with the key comparer of the input`` () = + let table = ignoreCaseTable () + let struct (accepted, rejected) = FrozenMap.partition (fun (key : string) _ -> key.Length > 3) table + + Assert.AreEqual( + ValueSome 3, + FrozenMap.tryFind "ONE" (FrozenMap.map (fun _ (value : string) -> value.Length) table), + "FrozenMap.map must look the keys up with the comparer of the input" + ) + + Assert.AreEqual( + ValueSome 3, + FrozenMap.tryFind "TWO" (FrozenMap.choose (fun _ (value : string) -> ValueSome value.Length) table), + "FrozenMap.choose must look the keys up with the comparer of the input" + ) + + Assert.IsTrue ( + FrozenMap.containsKey "THREE" (FrozenMap.filter (fun (key : string) _ -> key.Length > 3) table), + "FrozenMap.filter must look the keys up with the comparer of the input" + ) + + Assert.IsTrue (FrozenMap.containsKey "THREE" accepted, "FrozenMap.partition must keep the comparer in the accepted part") + Assert.IsTrue (FrozenMap.containsKey "TWO" rejected, "FrozenMap.partition must keep the comparer in the rejected part") + + [] + member _.``the transformations keep a custom key comparer of a small and a large map`` (size : int, name : string) = + let table = + (FrozenMap.forKeyComparer HashMapOps.absoluteKeyEquality).ofArray [| for key in 1..size -> struct (key, string key) |] + + let result = absoluteKeyTransformations[name] table + + Assert.AreSame ( + HashMapOps.absoluteKeyEquality, + FrozenMap.keyComparer result, + $"FrozenMap.%s{name} must keep the key comparer" + ) + + let key = FrozenMap.findKey (fun _ _ -> true) result + + Assert.IsTrue (FrozenMap.containsKey -key result, $"FrozenMap.%s{name} must look the keys up with the key comparer") + + [] + member _.``filter and partition return the input itself for a part with every binding`` () = + let table = ignoreCaseTable () + let struct (accepted, _) = FrozenMap.partition (fun _ _ -> true) table + let struct (_, rejected) = FrozenMap.partition (fun _ _ -> false) table + + Assert.AreSame ( + table, + FrozenMap.filter (fun _ _ -> true) table, + "FrozenMap.filter must return the input itself when it keeps every binding" + ) + + Assert.AreSame (table, accepted, "FrozenMap.partition must return the input itself when every binding is accepted") + Assert.AreSame (table, rejected, "FrozenMap.partition must return the input itself when every binding is rejected") + + Assert.AreNotSame ( + table, + FrozenMap.filter (fun (key : string) _ -> key.Length > 3) table, + "FrozenMap.filter must build a new map when it drops a binding" + ) + + [] + member _.``keys and values share the arrays of the frozen map`` (name : string) = + let table = tablesOfEveryKind[name]() + + Assert.AreSame ( + underlying table.Keys, + underlying (FrozenMap.keys table), + "FrozenMap.keys must share the array of the keys" + ) + + Assert.AreSame ( + underlying table.Values, + underlying (FrozenMap.values table), + "FrozenMap.values must share the array of the values" + ) + + CollectionAssert.AreEqual ( + FrozenMap.toArray table + |> Array.map (fun struct (key, _) -> key), + FlatList.toArray (FrozenMap.keys table), + "FrozenMap.keys must follow the enumeration" + ) + + CollectionAssert.AreEqual ( + FrozenMap.toArray table + |> Array.map (fun struct (_, value) -> value), + FlatList.toArray (FrozenMap.values table), + "FrozenMap.values must follow the enumeration" + ) + + [] + member _.``containsValue compares the values with their default comparer`` () = + let table = ignoreCaseTable () + Assert.IsTrue (FrozenMap.containsValue "dos" table, "FrozenMap.containsValue must find an equal value") + + Assert.IsFalse ( + FrozenMap.containsValue "DOS" table, + "FrozenMap.containsValue must compare the values with their default comparer, not with the key comparer" + ) + + Assert.IsFalse (FrozenMap.containsValue "cuatro" table, "FrozenMap.containsValue must not find a missing value") + + // The keys ignore the case, and the values compare with the default comparer: "one" is bound to "uno" + [] + member _.``containsBinding finds the key in the key comparer and the value in the default comparer`` + (key : string, value : string, expected : bool) + = + Assert.AreEqual( + expected, + FrozenMap.containsBinding key value (ignoreCaseTable ()), + "FrozenMap.containsBinding must compare the key with the key comparer and the value with the default comparer" + ) + + [] + member _.``containsBinding compares the values as EqualityComparer.Default does where tryFind and equality do not`` () = + let table = FrozenMap.singleton "nan" nan + + Assert.IsTrue ( + FrozenMap.containsBinding "nan" nan table, + "FrozenMap.containsBinding must find NaN, which EqualityComparer.Default finds equal to itself" + ) + + Assert.IsFalse ( + FrozenMap.tryFind "nan" table + |> ValueOption.exists (fun stored -> stored = nan), + "F# equality finds NaN different from itself, which is why the function asks EqualityComparer.Default" + ) + + [] + member _.``containsBinding rejects a null key`` (name : string) = + let table = tablesOfEveryKind[name]() + + let ex = + Assert.ThrowsExactly( + Action (fun () -> + FrozenMap.containsBinding (Unchecked.defaultof) 1 table + |> ignore + ), + "FrozenMap.containsBinding must reject a null key" + ) + + Assert.AreEqual("key", ex.ParamName, "FrozenMap.containsBinding must name the key parameter") + + [] + member _.``the span creations create the bindings of a span as Map.ofSeq does`` + (name : string, input : struct (int * string) array) + = + let table = spanCreations[name](Array.map MapInputs.ToKeyValuePair input) + + CollectionAssert.AreEqual ( + MapInputs.OfMap (MapInputs.ToMap input), + MapInputs.SortByKey (FrozenMap.toArray table), + $"FrozenMap.%s{name} must keep the last binding of each key, as Map.ofSeq does" + ) + + [] + member _.``the span creations copy the span`` (name : string) = + let pairs = [| KeyValuePair (1, "one") |] + let table = spanCreations[name] pairs + pairs[0] <- KeyValuePair (1, "changed") + Assert.AreEqual("one", FrozenMap.find 1 table, $"FrozenMap.%s{name} must copy the span") + + [] + member _.``the span creations keep the first key and the last value of keys that the default comparer finds equal`` + (name : string) + = + // The copies are equal in the default comparer and tell the two bindings apart (ADR 0001, D14) + let firstKey = MapInputs.Copy "one" + let lastValue = MapInputs.Copy "dos" + + let table = + stringSpanCreations[name] [| KeyValuePair (firstKey, "uno"); KeyValuePair (MapInputs.Copy "one", lastValue) |] + + let struct (key, value) = + Assert.ContainsSingle (FrozenMap.toArray table, $"FrozenMap.%s{name} must merge the bindings of equal keys") + + Assert.AreSame (firstKey, key, $"FrozenMap.%s{name} must keep the first key") + Assert.AreSame (lastValue, value, $"FrozenMap.%s{name} must keep the last value") + + [] + member _.``ofReadOnlySpan takes an array or a Span when the types of the call are known`` () = + let pairs = [| KeyValuePair ("one", "uno") |] + + // F# reports the conversions with FS3391, which is what the test is about + #nowarn "3391" + let fromArray : FrozenMap = FrozenMap.ofReadOnlySpan pairs + let fromSpan : FrozenMap = FrozenMap.ofReadOnlySpan (Span pairs) + #warnon "3391" + + CollectionAssert.AreEqual ( + [| struct ("one", "uno") |], + FrozenMap.toArray fromArray, + "FrozenMap.ofReadOnlySpan must take an array when the types of the call are known" + ) + + CollectionAssert.AreEqual ( + [| struct ("one", "uno") |], + FrozenMap.toArray fromSpan, + "FrozenMap.ofReadOnlySpan must take a Span when the types of the call are known" + ) + + [] + member _.``ofSpan takes a Span whose type arguments the call infers`` () = + // ofReadOnlySpan converts a Span only where the type of the parameter is known, so ofSpan takes the Span itself + let table = FrozenMap.ofSpan (Span [| KeyValuePair ("one", "uno") |]) + + CollectionAssert.AreEqual ( + [| struct ("one", "uno") |], + FrozenMap.toArray table, + "FrozenMap.ofSpan must create the bindings of a Span without a type annotation" + ) + + // The destination has a free slot after the last binding, and the first binding goes to the index + [] + member _.``copyTo copies the bindings in the order of enumeration from the index`` + (input : struct (int * string) array, index : int) + = + let table = FrozenMap.ofSeq input + let marker = KeyValuePair (-1, "marker") + let destination = Array.create (input.Length + index + 1) marker + FrozenMap.copyTo destination index table + + CollectionAssert.AreEqual ( + Array.concat [| + Array.create index marker + FrozenMap.toArray table + |> Array.map (fun struct (key, value) -> KeyValuePair (key, value)) + [| marker |] + |], + destination, + "FrozenMap.copyTo must fill the destination from the index in the order of enumeration, and leave the other elements" + ) + + [] + member _.``copyTo of an empty map at the end of the destination changes nothing`` () = + let destination = [| KeyValuePair (1, "one") |] + FrozenMap.copyTo destination 1 FrozenMap.empty + + CollectionAssert.AreEqual ( + [| KeyValuePair (1, "one") |], + destination, + "FrozenMap.copyTo must leave the destination as it is for a map without bindings" + ) + + // The destination of three elements has no index -1 and no index 4 + [] + member _.``copyTo rejects an index outside the destination`` (length : int, index : int) = + let table = FrozenMap.ofArray [| struct (1, "one"); struct (2, "two") |] + let destination = Array.zeroCreate> length + + Assert.ThrowsExactly( + Action (fun () -> FrozenMap.copyTo destination index table), + "FrozenMap.copyTo must reject an index outside the destination" + ) + |> ignore + + // Two bindings do not fit after index 2 of three elements, at index 3, in one element or in none + [] + member _.``copyTo rejects a destination without room for the bindings`` (length : int, index : int) = + let table = FrozenMap.ofArray [| struct (1, "one"); struct (2, "two") |] + let destination = Array.zeroCreate> length + + let ex = + Assert.ThrowsExactly( + Action (fun () -> FrozenMap.copyTo destination index table), + "FrozenMap.copyTo must reject a destination without room for the bindings" + ) + + Assert.AreEqual("destination", ex.ParamName, "FrozenMap.copyTo must name the destination parameter") + + [] + member _.``copyTo rejects a null destination`` () = + let table = FrozenMap.ofArray [| struct (1, "one") |] + + let ex = + Assert.ThrowsExactly( + Action (fun () -> FrozenMap.copyTo (Unchecked.defaultof array>) 0 table), + "FrozenMap.copyTo must reject a null destination" + ) + + Assert.AreEqual("destination", ex.ParamName, "FrozenMap.copyTo must name the destination parameter") + + [] + member _.``copyTo throws what the BCL method throws`` (length : int, index : int) = + let table = FrozenMap.ofArray [| struct (1, "one"); struct (2, "two") |] + let destination = Array.zeroCreate> length + + ExceptionAssert.Same ( + id, + (fun () -> table.CopyTo (destination, index)), + (fun () -> FrozenMap.copyTo destination index table), + "FrozenMap.copyTo must throw what FrozenDictionary.CopyTo throws" + ) + +#if NET10_0_OR_GREATER +/// The alternate lookups of `FrozenMap`, which exist on `net10.0` only (ADR 0001, D11). +[] +type FrozenMapAlternateLookupTests () = + + /// Compares string keys and looks them up by arrays of characters, which a frozen map of strings does not support. + static let stringByChars = { + new IEqualityComparer with + member _.Equals (left, right) = String.Equals (left, right, StringComparison.Ordinal) + member _.GetHashCode key = StringComparer.Ordinal.GetHashCode key + interface IAlternateEqualityComparer with + member _.Equals (alternate : char array, key : string) = + String.Equals (String alternate, key, StringComparison.Ordinal) + + member _.GetHashCode (alternate : char array) = StringComparer.Ordinal.GetHashCode (String alternate) + member _.Create (alternate : char array) = String alternate + } + + /// A map of `int64` keys, from 1 to the size, whose comparer looks them up by `int` alternates. + static let alternateTable (size : int) = + (FrozenMap.forKeyComparer FrozenMapOps.int64ByInt32).ofArray [| for key in 1L .. int64 size -> struct (key, string key) |] + + /// The alternate lookups on a null map, by name. + static let nullTableCalls : Map unit> = + let nullTable = Unchecked.defaultof> + + Map.ofArray [| + nameof FrozenMap.alternateLookup, (fun () -> FrozenMap.alternateLookup nullTable |> ignore) + nameof FrozenMap.tryAlternateLookup, (fun () -> FrozenMap.tryAlternateLookup nullTable |> ignore) + |] + + /// The names of the alternate lookups, as the rows of the test of a null map. + static member NullTableCalls = nullTableCalls.Keys + + // The maps are empty, small and large + [] + member _.``alternateLookup finds the bindings by an alternate key`` (size : int) = + let table = alternateTable size + let lookup = FrozenMap.alternateLookup table + Assert.AreSame (table, lookup.Dictionary, "FrozenMap.alternateLookup must look the bindings of the map up") + + for key in 1..size do + Assert.IsTrue (lookup.ContainsKey key, $"FrozenMap.alternateLookup must find %d{key}") + Assert.AreEqual(string key, lookup[key], $"FrozenMap.alternateLookup must return the value of %d{key}") + + Assert.IsFalse (lookup.ContainsKey (size + 1), $"FrozenMap.alternateLookup must not find %d{size + 1}") + + [] + member _.``alternateLookup throws when the comparer has no alternate key type`` () = + let table = + (FrozenMap.forKeyComparer HashIdentity.Structural).ofArray [| struct (1L, "one"); struct (2L, "two") |] + + Assert.ThrowsExactly( + Action (fun () -> FrozenMap.alternateLookup table |> ignore), + "FrozenMap.alternateLookup must reject a comparer without the alternate key type, as GetAlternateLookup does" + ) + |> ignore + + // The maps are empty, small and large + [] + member _.``tryAlternateLookup returns the lookup`` (size : int) = + let table = alternateTable size + + match FrozenMap.tryAlternateLookup table with + | ValueSome lookup -> + Assert.AreSame (table, lookup.Dictionary, "FrozenMap.tryAlternateLookup must look the bindings of the map up") + + Assert.AreEqual( + size > 0, + lookup.ContainsKey 1, + "FrozenMap.tryAlternateLookup must find 1 exactly when the map has a binding" + ) + | ValueNone -> Assert.Fail "FrozenMap.tryAlternateLookup must return a lookup for a comparer with the alternate key type" + + [] + member _.``tryAlternateLookup returns nothing for a comparer without the alternate key type`` () = + let structural = + (FrozenMap.forKeyComparer HashIdentity.Structural).ofArray [| struct (1L, "one"); struct (2L, "two") |] + + Assert.IsTrue ( + (FrozenMap.tryAlternateLookup structural).IsNone, + "FrozenMap.tryAlternateLookup must return nothing for HashIdentity.Structural, which has no alternate key type" + ) + + [] + member _.``the alternate lookups of string keys take spans only`` () = + let table = (FrozenMap.forKeyComparer stringByChars).ofArray [| struct ("one", 1); struct ("two", 2) |] + + Assert.IsTrue ( + (FrozenMap.tryAlternateLookup table).IsNone, + "FrozenMap.tryAlternateLookup must return nothing for string keys and an alternate key that is not a span" + ) + + Assert.ThrowsExactly( + Action (fun () -> FrozenMap.alternateLookup table |> ignore), + "FrozenMap.alternateLookup must reject string keys and an alternate key that is not a span" + ) + |> ignore + + [] + member _.``a span alternate key takes the method of the frozen map`` () = + // The functions of the module cannot take a span, so their documentation points to the method of the map (D11) + let table = + (FrozenMap.forKeyComparer StringComparer.Ordinal).ofArray [| struct ("one", 1); struct ("two", 2) |] + + let lookup = table.GetAlternateLookup>() + Assert.IsTrue (lookup.ContainsKey ("two".AsSpan()), "GetAlternateLookup must find a key by a span of its characters") + Assert.AreEqual(2, lookup["two".AsSpan()], "GetAlternateLookup must return the value of a span key") + Assert.IsFalse (lookup.ContainsKey ("three".AsSpan()), "GetAlternateLookup must not find a missing span key") + + [] + member _.``the alternate lookups reject a null map`` (name : string) = + let ex = + Assert.ThrowsExactly(Action nullTableCalls[name], $"FrozenMap.%s{name} must reject a null map") + + Assert.AreEqual("table", ex.ParamName, $"FrozenMap.%s{name} must name the map parameter") +#endif diff --git a/tests/FSharp.Collections.Immutable.Tests/FrozenMap/FrozenMapTupleTests.fs b/tests/FSharp.Collections.Immutable.Tests/FrozenMap/FrozenMapTupleTests.fs new file mode 100644 index 00000000..ab7634a6 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/FrozenMap/FrozenMapTupleTests.fs @@ -0,0 +1,88 @@ +namespace FSharp.Collections.Immutable.Tests + +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 `FrozenMap` module of the `Tuple` namespace. +[] +type FrozenMapTupleTests () = + + /// 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.FrozenMap.ofSeq MapInputs.Several + + /// The reference pairs of a map, sorted by key. + static let sortedPairs (table : FrozenMap) = FrozenMap.toArray table |> Array.sortBy fst + + [] + member _.``partition returns a reference tuple`` () = + let (accepted : FrozenMap), (rejected : FrozenMap) = + FrozenMap.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.FrozenMap.partition must return the accepted bindings" + ) + + CollectionAssert.AreEqual ( + Map.toArray expectedRejected, + sortedPairs rejected, + "Tuple.FrozenMap.partition must return the rejected bindings" + ) + + [] + member _.``toArray returns reference tuples in the order of enumeration`` () = + let array : (int * string) array = FrozenMap.toArray table + + CollectionAssert.AreEqual ( + pairs (FSharp.Collections.Immutable.FrozenMap.toArray table), + array, + "Tuple.FrozenMap.toArray must follow the enumeration" + ) + + [] + member _.``toList returns reference tuples in the order of enumeration`` () = + let list : (int * string) list = FrozenMap.toList table + + CollectionAssert.AreEqual ( + pairs (FSharp.Collections.Immutable.FrozenMap.toArray table), + List.toArray list, + "Tuple.FrozenMap.toList must follow the enumeration" + ) + + [] + member _.``toSeq returns reference tuples in the order of enumeration`` () = + let sequence : seq = FrozenMap.toSeq table + + CollectionAssert.AreEqual ( + pairs (FSharp.Collections.Immutable.FrozenMap.toArray table), + Seq.toArray sequence, + "Tuple.FrozenMap.toSeq must follow the enumeration" + ) + + [] + member _.``the functions without a tuple keep resolving to the default module`` () = + let found : string voption = FrozenMap.tryFind 5 table + let keys : FlatList = FrozenMap.keys table + Assert.AreEqual(ValueSome "five", found, "FrozenMap.tryFind must resolve to the default module") + Assert.HasCount (MapInputs.Several.Length, keys, "FrozenMap.keys 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 FrozenMapExtensions + let bindings = pairs MapInputs.RepeatedKeys + let expected = Map.toArray (Map.ofArray bindings) + let ofArray : FrozenMap = FrozenMap.ofArray bindings + let ofList : FrozenMap = FrozenMap.ofList (List.ofArray bindings) + let ofSeq : FrozenMap = bindings |> FrozenMap.ofSeq + CollectionAssert.AreEqual (expected, sortedPairs ofArray, "FrozenMap.ofArray must take reference tuples") + CollectionAssert.AreEqual (expected, sortedPairs ofList, "FrozenMap.ofList must take reference tuples") + CollectionAssert.AreEqual (expected, sortedPairs ofSeq, "FrozenMap.ofSeq must take reference tuples")