From 34fa7038c7f22108eefacd562c4646b4b9bf0f3a Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Sun, 18 Oct 2026 13:48:52 +0200 Subject: [PATCH 1/4] feat: `FrozenMap` module with the vocabulary of the FSharp.Core `Map` module Adds `FrozenMap` over `FrozenDictionary<'Key, 'Value>` after ADR 0001 in `FrozenMap/FrozenMap.fsi` and `FrozenMap/FrozenMap.fs`, each function documented in the signature file. - The module reads and transforms a map that is built once: it has no `add`, `remove`, `change`, `clear`, builder or `with*Comparer*`. `choose`, `filter`, `map` and `partition` rebuild the map in O(n) with the key comparer of the input; `filter` and `partition` return the input itself for a part that keeps every binding. - The functions with a name of `Map` have its semantics and exceptions: `ofArray`, `ofList` and `ofSeq` keep the last binding of a key, `find` and `pick` throw the `KeyNotFoundException` of `Map`, and `findKey` the one of `Seq.pick`. The wrappers keep the semantics of the BCL: `ofKeyValuePairs` and the comparer variants keep the first key and the last value of equal keys, as `ToFrozenDictionary` does, and `ofSeqBy` rejects every repeated key. - `keys` and `values` return flat lists over the arrays of the map without a copy, and `foldBack` reads those arrays backwards. `containsValue` compares with the default comparer of the values, because a frozen map has no value comparer. - On `net10.0`, `alternateLookup` and `tryAlternateLookup` wrap `GetAlternateLookup` and `TryGetAlternateLookup`. The type of the alternate keys is their first type argument, and their documentation points span alternates to the methods of the map. - `try*` functions return `voption`, tuples are struct tuples, and the `Option` and `Tuple` namespaces repeat those functions with options and reference tuples. - `frozenMap { }` collects `yield struct (key, value)` and `yield KeyValuePair` in a dictionary with the key comparer and freezes it when it runs; `FrozenMapCE` takes a key comparer. - The package description mentions System.Collections.Frozen. Co-Authored-By: Claude Opus 5.5 --- .../ComputationExpressions.fs | 3 + .../ComputationExpressions.fsi | 23 + .../FSharp.Collections.Immutable.fsproj | 23 +- .../FrozenMap/FrozenMap.CustomComparers.fs | 109 +++ .../FrozenMap/FrozenMap.CustomComparers.fsi | 247 +++++++ .../FrozenMap/FrozenMap.Extensions.fs | 42 ++ .../FrozenMap/FrozenMap.Extensions.fsi | 151 ++++ .../FrozenMap/FrozenMap.fs | 277 ++++++++ .../FrozenMap/FrozenMap.fsi | 659 ++++++++++++++++++ .../FrozenMap/FrozenMapCE.fs | 124 ++++ .../FrozenMap/FrozenMapCE.fsi | 139 ++++ .../FrozenMap/FrozenMapOption.fs | 47 ++ .../FrozenMap/FrozenMapOption.fsi | 111 +++ .../FrozenMap/FrozenMapTuple.fs | 31 + .../FrozenMap/FrozenMapTuple.fsi | 72 ++ .../FrozenMap/FrozenMapTypes.fs | 16 + 16 files changed, 2073 insertions(+), 1 deletion(-) create mode 100644 src/FSharp.Collections.Immutable/FrozenMap/FrozenMap.CustomComparers.fs create mode 100644 src/FSharp.Collections.Immutable/FrozenMap/FrozenMap.CustomComparers.fsi create mode 100644 src/FSharp.Collections.Immutable/FrozenMap/FrozenMap.Extensions.fs create mode 100644 src/FSharp.Collections.Immutable/FrozenMap/FrozenMap.Extensions.fsi create mode 100644 src/FSharp.Collections.Immutable/FrozenMap/FrozenMap.fs create mode 100644 src/FSharp.Collections.Immutable/FrozenMap/FrozenMap.fsi create mode 100644 src/FSharp.Collections.Immutable/FrozenMap/FrozenMapCE.fs create mode 100644 src/FSharp.Collections.Immutable/FrozenMap/FrozenMapCE.fsi create mode 100644 src/FSharp.Collections.Immutable/FrozenMap/FrozenMapOption.fs create mode 100644 src/FSharp.Collections.Immutable/FrozenMap/FrozenMapOption.fsi create mode 100644 src/FSharp.Collections.Immutable/FrozenMap/FrozenMapTuple.fs create mode 100644 src/FSharp.Collections.Immutable/FrozenMap/FrozenMapTuple.fsi create mode 100644 src/FSharp.Collections.Immutable/FrozenMap/FrozenMapTypes.fs 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..f50c59f5 --- /dev/null +++ b/src/FSharp.Collections.Immutable/FrozenMap/FrozenMapOption.fs @@ -0,0 +1,47 @@ +[] +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>) = + FSharp.Collections.Immutable.FrozenMap.tryFind key table + |> ValueOption.toOption + +// ---- Search ---- + +[] +let pick (chooser : 'Key -> 'Value -> 'U option) (table : FrozenMap<'Key, 'Value>) = + FSharp.Collections.Immutable.FrozenMap.pick (fun key value -> chooser key value |> ValueOption.ofOption) table + +[] +let tryFindKey predicate (table : FrozenMap<'Key, 'Value>) = + FSharp.Collections.Immutable.FrozenMap.tryFindKey predicate table + |> ValueOption.toOption + +[] +let tryPick (chooser : 'Key -> 'Value -> 'U option) (table : FrozenMap<'Key, 'Value>) = + FSharp.Collections.Immutable.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>) = + FSharp.Collections.Immutable.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>) + = + FSharp.Collections.Immutable.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..e8425be4 --- /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) = FSharp.Collections.Immutable.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> From 232e96f3cf44a32ab72c7df24cac142d04a37007 Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Sun, 18 Oct 2026 13:49:02 +0200 Subject: [PATCH 2/4] test: `FrozenMap` against the map contracts - `FrozenMapOps` records the functions of the module, and `FrozenMap` derives the reading, transformation and creation facets; a frozen map has no mutation facet. - The tests of `FrozenMap` cover null inputs, the key comparer of the creation functions and of the transformations, the results that are the input itself, the arrays of the keys and the values, and the repeated keys of the functions that wrap `ToFrozenDictionary`. - On `net10.0`, the alternate lookups find `int64` keys by `int` alternates through a comparer that implements `IAlternateEqualityComparer`, find nothing for `HashIdentity.Structural` and for string keys without a span, and a span goes through `GetAlternateLookup` of the map. - The computation expression and the `Option` and `Tuple` namespaces have their own tests. Co-Authored-By: Claude Opus 5.5 --- .../FSharp.Collections.Immutable.Tests.fsproj | 8 + .../CustomComparersFrozenMapTests.fs | 412 +++++++++ .../FrozenMapComputationExpressionTests.fs | 416 +++++++++ .../FrozenMap/FrozenMapContractTests.fs | 24 + .../FrozenMap/FrozenMapExtensionsTests.fs | 163 ++++ .../FrozenMap/FrozenMapOps.fs | 132 +++ .../FrozenMap/FrozenMapOptionTests.fs | 127 +++ .../FrozenMap/FrozenMapTests.fs | 824 ++++++++++++++++++ .../FrozenMap/FrozenMapTupleTests.fs | 88 ++ 9 files changed, 2194 insertions(+) create mode 100644 tests/FSharp.Collections.Immutable.Tests/FrozenMap/CustomComparersFrozenMapTests.fs create mode 100644 tests/FSharp.Collections.Immutable.Tests/FrozenMap/FrozenMapComputationExpressionTests.fs create mode 100644 tests/FSharp.Collections.Immutable.Tests/FrozenMap/FrozenMapContractTests.fs create mode 100644 tests/FSharp.Collections.Immutable.Tests/FrozenMap/FrozenMapExtensionsTests.fs create mode 100644 tests/FSharp.Collections.Immutable.Tests/FrozenMap/FrozenMapOps.fs create mode 100644 tests/FSharp.Collections.Immutable.Tests/FrozenMap/FrozenMapOptionTests.fs create mode 100644 tests/FSharp.Collections.Immutable.Tests/FrozenMap/FrozenMapTests.fs create mode 100644 tests/FSharp.Collections.Immutable.Tests/FrozenMap/FrozenMapTupleTests.fs 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") From 91792736a611e655e2e26184a80e468a17cd7df5 Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Sun, 18 Oct 2026 13:49:02 +0200 Subject: [PATCH 3/4] docs: `FrozenMap` in the changelog and the collection table The README, the index page and the background page list `FrozenMap` with `frozenMap { }`. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 50 ++++++++++++++++++++++++++++++ README.md | 1 + docsSrc/Explanations/Background.md | 1 + docsSrc/index.md | 1 + 4 files changed, 53 insertions(+) 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>` | – | From 1b3d334287e5f84498e0625e1e8101fad4b1c062 Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Wed, 21 Oct 2026 04:02:41 +0200 Subject: [PATCH 4/4] refactor: the `Tuple` and `Option` modules of `FrozenMap` call the default modules by their short names Each of these files opens `FSharp.Collections.Immutable`, and inside `FSharp.Collections.Immutable.Tuple.FrozenMap` the name `FrozenMap` reaches the default module, also for the functions that both modules define, so the full namespace in front of it added nothing. The compiled code is the same: the opcodes and the called methods of every method of both builds match one to one. Co-Authored-By: Claude Sonnet 5.5 --- .../FrozenMap/FrozenMapOption.fs | 16 ++++++---------- .../FrozenMap/FrozenMapTuple.fs | 2 +- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/FSharp.Collections.Immutable/FrozenMap/FrozenMapOption.fs b/src/FSharp.Collections.Immutable/FrozenMap/FrozenMapOption.fs index f50c59f5..b71115a0 100644 --- a/src/FSharp.Collections.Immutable/FrozenMap/FrozenMapOption.fs +++ b/src/FSharp.Collections.Immutable/FrozenMap/FrozenMapOption.fs @@ -9,31 +9,27 @@ open FSharp.Collections.Immutable // ---- Access ---- [] -let tryFind (key : 'Key) (table : FrozenMap<'Key, 'Value>) = - FSharp.Collections.Immutable.FrozenMap.tryFind key table - |> ValueOption.toOption +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>) = - FSharp.Collections.Immutable.FrozenMap.pick (fun key value -> chooser key value |> ValueOption.ofOption) table + FrozenMap.pick (fun key value -> chooser key value |> ValueOption.ofOption) table [] -let tryFindKey predicate (table : FrozenMap<'Key, 'Value>) = - FSharp.Collections.Immutable.FrozenMap.tryFindKey predicate table - |> ValueOption.toOption +let tryFindKey predicate (table : FrozenMap<'Key, 'Value>) = FrozenMap.tryFindKey predicate table |> ValueOption.toOption [] let tryPick (chooser : 'Key -> 'Value -> 'U option) (table : FrozenMap<'Key, 'Value>) = - FSharp.Collections.Immutable.FrozenMap.tryPick (fun key value -> chooser key value |> ValueOption.ofOption) table + 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>) = - FSharp.Collections.Immutable.FrozenMap.choose (fun key value -> chooser key value |> ValueOption.ofOption) table + FrozenMap.choose (fun key value -> chooser key value |> ValueOption.ofOption) table #if NET10_0_OR_GREATER // ---- Type-specific ---- @@ -42,6 +38,6 @@ let choose (chooser : 'Key -> 'Value -> 'U option) (table : FrozenMap<'Key, 'Val let tryAlternateLookup<'Alternate, 'Key, 'Value when 'Key : not null and 'Alternate : not null> (table : FrozenMap<'Key, 'Value>) = - FSharp.Collections.Immutable.FrozenMap.tryAlternateLookup<'Alternate, 'Key, 'Value> table + FrozenMap.tryAlternateLookup<'Alternate, 'Key, 'Value> table |> ValueOption.toOption #endif diff --git a/src/FSharp.Collections.Immutable/FrozenMap/FrozenMapTuple.fs b/src/FSharp.Collections.Immutable/FrozenMap/FrozenMapTuple.fs index e8425be4..69823ad5 100644 --- a/src/FSharp.Collections.Immutable/FrozenMap/FrozenMapTuple.fs +++ b/src/FSharp.Collections.Immutable/FrozenMap/FrozenMapTuple.fs @@ -10,7 +10,7 @@ open FSharp.Collections.Immutable [] let partition predicate (table : FrozenMap<'Key, 'Value>) = - let struct (accepted, rejected) = FSharp.Collections.Immutable.FrozenMap.partition predicate table + let struct (accepted, rejected) = FrozenMap.partition predicate table accepted, rejected // ---- Conversion ----