diff --git a/CHANGELOG.md b/CHANGELOG.md index e156a682..5f75103c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -84,6 +84,52 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `tryGetValue` with options, and `partition` and `partitionWith` with reference tuples. * `hashSet { }` builds a hash set, and `HashSetCE` created with an equality comparer builds hash sets that compare with it; a `null` comparer throws `ArgumentNullException`. +* `FrozenSet` works on `System.Collections.Frozen.FrozenSet<'T>` with the functions of the FSharp.Core `Set` module that + read a set or transform a whole set, with their semantics, argument order and exceptions, plus `choose`, `comparer`, + `forComparer`, `ofReadOnlySpan`, `ofSpan`, `overlaps`, `setEquals`, `symmetricDifference`, `toFlatList`, which returns + the elements without a copy, and `tryGetValue`. A frozen set is built once: the module has no function that adds or + removes an element, and every transformation builds a new frozen set. `FrozenSet.union set (FrozenSet.singleton value)` + and `FrozenSet.difference set (FrozenSet.singleton value)` do what `Set.add` and `Set.remove` do, each as an O(n) + rebuild that compares in the comparer of `set`. Every function throws `ArgumentNullException` for a `null` frozen set, + sequence, list or array, and for a default flat list that is given as a sequence; `yield!` and `for` of + `frozenSet { }` do the same. +* `FrozenSet.ofReadOnlySpan` and `ofSpan` create a frozen set from a span. They wrap `FrozenSet.Create`, and of the + elements that the comparer finds equal the first stays. +* `FrozenSet.forComparer comparer` creates the `CustomComparerFrozenSet` struct, whose members `empty`, `ofArray`, + `ofList`, `ofReadOnlySpan`, `ofSeq`, `ofSpan` and `singleton` create frozen sets that compare with the comparer, as in + `(FrozenSet.forComparer StringComparer.OrdinalIgnoreCase).ofSeq names`. They replace the BCL methods + `FrozenSet.Create(comparer, ...)` and `ToFrozenSet(source, comparer)`; `ToFrozenSet` has no `optimizeForReading` + parameter in the package that the library references, so nothing is lost. `ofSeq` also gives an existing frozen set the + comparer, as `ToFrozenSet` does: the frozen set itself comes back when its comparer equals the comparer of the struct. + A `null` comparer throws `ArgumentNullException`, where the BCL methods take it as the default comparer: + `FrozenSet.ofSeq set` gives a frozen set the default comparer, and the default value of the struct has the BCL meaning. +* The members of `FrozenSet<'T>` map to the module as follows. `TryGetValue(x, out v)` is `tryGetValue x set`, which + returns a `voption` (`Option.FrozenSet.tryGetValue` returns an `option`). `Items` is `toFlatList`, and `CopyTo` is + `toFlatList` followed by `FlatList.blit` or `FlatList.asSpan`. `IsSubsetOf`, `IsProperSubsetOf`, `IsSupersetOf`, + `IsProperSupersetOf`, `Overlaps` and `SetEquals` take frozen sets in the module, as the `Set` functions do. To pass a + sequence, convert it with `(FrozenSet.forComparer (FrozenSet.comparer set)).ofSeq elements`: the elements get the + comparer of the first frozen set, as they do in the members. `FrozenSet.ofSeq elements` converts them with the default + comparer instead, which merges the elements that it finds equal before the relation sees them. The answer is that of + the member when the comparer of `set` finds equal every pair of elements that the default comparer finds equal, as the + default comparer and `StringComparer.OrdinalIgnoreCase` do. It can differ when the comparer of `set` tells such + elements apart, as `HashIdentity.Reference` does for two strings with the same text. `Overlaps` and `IsSupersetOf` + with a sequence are `Seq.exists` and `Seq.forall` with `FrozenSet.contains`, for every comparer. The mutating members + of the interfaces, such as `ISet.UnionWith` and `ICollection.Add`, throw `NotSupportedException` on the BCL type + and have no function. +* `FrozenSet`: the element that stays when the comparer finds two elements equal follows the BCL. `union` keeps the + element of the first frozen set, `intersect` keeps the elements of the second one, as `ImmutableHashSet.Intersect` and + `ImmutableSortedSet.Intersect` do, because `FrozenSet<'T>` has no such member, and the creation functions keep the + first element of their input. `isProperSuperset` counts the elements of the second frozen set that the comparer of the + first one finds equal once, as `FrozenSet<'T>.IsProperSupersetOf` does. +* On `net10.0`, `FrozenSet.alternateLookup` and `FrozenSet.tryAlternateLookup` return the alternate lookup of a frozen set + whose comparer compares the elements with values of another type. They are `GetAlternateLookup` and + `TryGetAlternateLookup`. The alternate type comes first, as in `FrozenSet.alternateLookup set`, and a lookup by a + `ReadOnlySpan` calls the BCL method directly, because an F# generic function cannot be instantiated with a + byref-like type. +* The `FSharp.Collections.Immutable.Option.FrozenSet` and `FSharp.Collections.Immutable.Tuple.FrozenSet` modules repeat + `choose`, `tryGetValue` and `tryAlternateLookup` with options, and `partition` and `partitionWith` with reference tuples. +* `frozenSet { }` builds a frozen set, and `FrozenSetCE` created with an equality comparer builds frozen sets that compare + with it; a `null` comparer throws `ArgumentNullException`. ### Changed diff --git a/README.md b/README.md index 555b4c97..4fab023c 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ Please also join the [F# Open Source Group](http://fsharp.github.com) | `HashSet` | `ImmutableHashSet<'T>` | `hashSet { }` | | `SortedSet` | `ImmutableSortedSet<'T>` | – | | `ISet` | `IImmutableSet<'T>` | – | +| `FrozenSet` | `FrozenSet<'T>` | `frozenSet { }` | | `IIndexedSeq` | `IReadOnlyList<'T>` | – | [Documentation](https://fsprojects.github.io/FSharp.Collections.Immutable/) diff --git a/docsSrc/Explanations/Background.md b/docsSrc/Explanations/Background.md index 88b0f21a..82c7b43f 100644 --- a/docsSrc/Explanations/Background.md +++ b/docsSrc/Explanations/Background.md @@ -35,6 +35,7 @@ While F# has its own built-in immutable collections, System.Collections.Immutabl | `HashSet` | `ImmutableHashSet<'T>` | `hashSet { }` | | `SortedSet` | `ImmutableSortedSet<'T>` | – | | `ISet` | `IImmutableSet<'T>` | – | +| `FrozenSet` | `FrozenSet<'T>` | `frozenSet { }` | | `IIndexedSeq` | `IReadOnlyList<'T>` | – | --- diff --git a/docsSrc/index.md b/docsSrc/index.md index 7541de20..7c5af5c0 100644 --- a/docsSrc/index.md +++ b/docsSrc/index.md @@ -17,6 +17,7 @@ F# bindings for [System.Collections.Immutable](https://learn.microsoft.com/en-us | `HashSet` | `ImmutableHashSet<'T>` | `hashSet { }` | | `SortedSet` | `ImmutableSortedSet<'T>` | – | | `ISet` | `IImmutableSet<'T>` | – | +| `FrozenSet` | `FrozenSet<'T>` | `frozenSet { }` | | `IIndexedSeq` | `IReadOnlyList<'T>` | – | --- diff --git a/src/FSharp.Collections.Immutable/ComputationExpressions.fs b/src/FSharp.Collections.Immutable/ComputationExpressions.fs index 156d4eb5..db7a683f 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 frozenSet<'T> = FrozenSetCE<'T>() + [] let hashSet<'T> = HashSetCE<'T>() diff --git a/src/FSharp.Collections.Immutable/ComputationExpressions.fsi b/src/FSharp.Collections.Immutable/ComputationExpressions.fsi index 78396635..584501e1 100644 --- a/src/FSharp.Collections.Immutable/ComputationExpressions.fsi +++ b/src/FSharp.Collections.Immutable/ComputationExpressions.fsi @@ -20,6 +20,33 @@ module FSharp.Collections.Immutable.ComputationExpressions /// val flatList : FlatListCE +/// +/// Builds a frozen set that compares with . +/// +/// +/// The value is a type function, so that one value builds frozen sets of every element type. A +/// created with an equality comparer builds frozen sets that +/// compare with it. The expression freezes its elements once, after it has collected all of them. +/// +/// +/// +/// let squares = frozenSet { +/// for i in -2..2 do +/// yield i * i +/// yield! [ 4; 9 ] +/// } +/// +/// Evaluates to a frozen set of 0, 1, 4 and 9. +/// +/// +/// +/// let names = FrozenSetCE StringComparer.OrdinalIgnoreCase { "Ann"; "ANN"; "Bob" } +/// +/// Evaluates to a frozen set of "Ann" and "Bob" that ignores case. +/// +[] +val frozenSet<'T> : FrozenSetCE<'T> + /// /// Builds a hash set that compares with . /// diff --git a/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj b/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj index ee70cb54..3eb47673 100644 --- a/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj +++ b/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj @@ -67,6 +67,20 @@ + + FrozenSet.fsi + + + FrozenSet.fs + + + + + + + + + diff --git a/src/FSharp.Collections.Immutable/FrozenSet/FrozenSet.CustomComparer.fs b/src/FSharp.Collections.Immutable/FrozenSet/FrozenSet.CustomComparer.fs new file mode 100644 index 00000000..5e410650 --- /dev/null +++ b/src/FSharp.Collections.Immutable/FrozenSet/FrozenSet.CustomComparer.fs @@ -0,0 +1,60 @@ +namespace FSharp.Collections.Immutable + +open System +open System.Collections.Frozen +open System.Collections.Generic + +// A struct with a primary constructor cannot check its argument, so the field and the constructor are explicit (ADR 0001, +// D12). The field is null only in the default value of the struct, which no constructor creates; the BCL treats a null +// comparer as the default comparer, and the Comparer property says so. +[] +type CustomComparerFrozenSet<'T> = + + val private comparer : IEqualityComparer<'T> | null + + new (comparer : IEqualityComparer<'T>) + = + checkNotNull (nameof comparer) comparer + { comparer = comparer } + + // ---- Creation ---- + + // Create returns the shared empty frozen set for the default comparer and, unlike ToFrozenSet, collects nothing first + [] + member this.empty : FrozenSet<'T> = FrozenSet.Create<'T>(this.Comparer, ReadOnlySpan<'T>.Empty) + + [] + member this.ofArray (array : 'T array) : FrozenSet<'T> = + checkNotNull (nameof array) array + array.ToFrozenSet this.Comparer + + [] + member this.ofList (elements : 'T list) : FrozenSet<'T> = + // Set.ofList dereferences a null list without a check; a null input throws ArgumentNullException here (D6) + checkNotNull (nameof elements) elements + elements.ToFrozenSet this.Comparer + + // The BCL keeps the first of the elements that the comparer finds equal, for every size of the span (the tests pin it) + [] + member this.ofReadOnlySpan (span : ReadOnlySpan<'T>) : FrozenSet<'T> = FrozenSet.Create<'T>(this.Comparer, span) + + [] + member this.ofSeq (elements : seq<'T>) : FrozenSet<'T> = + // A default flat list given as a sequence would fail with InvalidOperationException in the BCL call (ADR 0001, D6) + checkNotNullOrDefaultSeq (nameof elements) elements + // ToFrozenSet returns a frozen set whose comparer equals the comparer, and copies any other sequence + elements.ToFrozenSet this.Comparer + + // Create takes a read-only span; F# converts the span of the argument + [] + member this.ofSpan (span : Span<'T>) : FrozenSet<'T> = FrozenSet.Create<'T>(this.Comparer, span) + + [] + member this.singleton (value : 'T) : FrozenSet<'T> = [| value |].ToFrozenSet this.Comparer + + // ---- Properties ---- + + member this.Comparer : IEqualityComparer<'T> = + match this.comparer with + | null -> EqualityComparer<'T>.Default :> IEqualityComparer<'T> + | comparer -> comparer diff --git a/src/FSharp.Collections.Immutable/FrozenSet/FrozenSet.CustomComparer.fsi b/src/FSharp.Collections.Immutable/FrozenSet/FrozenSet.CustomComparer.fsi new file mode 100644 index 00000000..eb562a96 --- /dev/null +++ b/src/FSharp.Collections.Immutable/FrozenSet/FrozenSet.CustomComparer.fsi @@ -0,0 +1,156 @@ +namespace FSharp.Collections.Immutable + +open System +open System.Collections.Frozen +open System.Collections.Generic + +/// +/// Creates frozen sets that compare their elements with an equality comparer (ADR 0001, D12). +/// +/// +/// +/// +/// creates the struct. Its members are the creation functions of +/// under the same names, with the comparer applied, so that a call reads as one phrase. A frozen set has no builder, so +/// the struct has no builder member. +/// +/// +/// The struct holds only the comparer, so creating it allocates nothing. Its default value, which no constructor +/// creates, compares with . +/// +/// +/// +/// +/// let names = (FrozenSet.forComparer StringComparer.OrdinalIgnoreCase).ofList [ "Ann"; "ANN"; "Bob" ] +/// +/// Evaluates to a frozen set of "Ann" and "Bob" that compares without case. +/// +[] +type CustomComparerFrozenSet<'T> = + + // A signature reveals the fields of a struct, private ones included (FS0314); null only in the default value + val private comparer : IEqualityComparer<'T> | null + + /// Creates the struct for an equality comparer. + /// The equality comparer of the elements of the frozen sets that the struct creates. + /// Thrown when is . + new : comparer : IEqualityComparer<'T> -> CustomComparerFrozenSet<'T> + + // ---- Creation ---- + + /// + /// The empty frozen set that compares with the comparer. + /// + /// + /// O(1). The member wraps + /// , + /// which returns for + /// . + /// + [] + member empty : FrozenSet<'T> + + /// + /// Creates a frozen set from the elements of an array, compared with the comparer. + /// + /// The input array. + /// + /// The frozen set of the elements that are distinct in the comparer; of the elements that the comparer considers equal, + /// it holds the first one. + /// + /// Thrown when is . + /// + /// O(n). Later changes to the array do not change the frozen set. The member wraps + /// . + /// + [] + member ofArray : array : 'T array -> FrozenSet<'T> + + /// + /// Creates a frozen set from the elements of a list, compared with the comparer. + /// + /// The input list. + /// + /// The frozen set of the elements that are distinct in the comparer; of the elements that the comparer considers equal, + /// it holds the first one. + /// + /// Thrown when is . + /// + /// O(n). + /// + [] + member ofList : elements : 'T list -> FrozenSet<'T> + + /// + /// Creates a frozen set from the elements of a read-only span, compared with the comparer. + /// + /// The input span. + /// + /// The frozen set of the elements that are distinct in the comparer; of the elements that the comparer considers equal, + /// it holds the first one. An empty span gives an empty frozen set. + /// + /// + /// O(n). Later changes to the memory under the span do not change the frozen set. The member wraps + /// . + /// + [] + member ofReadOnlySpan : span : ReadOnlySpan<'T> -> FrozenSet<'T> + + /// + /// Creates a frozen set from the elements of a sequence, compared with the comparer. + /// + /// The input sequence, which can be a frozen set with another comparer. + /// + /// The frozen set of the elements that are distinct in the comparer; of the elements that the comparer considers equal, + /// it holds the first one. + /// + /// Thrown when is or a default flat list. + /// + /// + /// O(n). The member wraps + /// . + /// + /// + /// A frozen set gets the comparer: the member returns the frozen set itself in O(1) when its comparer equals the + /// comparer, and otherwise a new frozen set of its elements in the comparer. + /// + /// + [] + member ofSeq : elements : seq<'T> -> FrozenSet<'T> + + /// + /// Creates a frozen set from the elements of a span, compared with the comparer. + /// + /// The input span. + /// + /// The frozen set of the elements that are distinct in the comparer; of the elements that the comparer considers equal, + /// it holds the first one. An empty span gives an empty frozen set. + /// + /// + /// O(n). Later changes to the memory under the span do not change the frozen set. The member converts the span to a + /// read-only span and wraps + /// . + /// + [] + member ofSpan : span : Span<'T> -> FrozenSet<'T> + + /// + /// Creates a frozen set of one element that compares with the comparer. + /// + /// The element. + /// The frozen set of one element. + /// + /// O(1). + /// + [] + member singleton : value : 'T -> FrozenSet<'T> + + // ---- Properties ---- + + /// + /// The equality comparer of the elements of the frozen sets that the struct creates. + /// + /// + /// O(1). The default value of the struct returns . + /// + member Comparer : IEqualityComparer<'T> diff --git a/src/FSharp.Collections.Immutable/FrozenSet/FrozenSet.fs b/src/FSharp.Collections.Immutable/FrozenSet/FrozenSet.fs new file mode 100644 index 00000000..8e3d56a6 --- /dev/null +++ b/src/FSharp.Collections.Immutable/FrozenSet/FrozenSet.fs @@ -0,0 +1,322 @@ +[] +module FSharp.Collections.Immutable.FrozenSet + +open System +open System.Collections.Frozen +open System.Collections.Generic + +// The functions call the members of FrozenSet: Contains, TryGetValue, the set predicates, Comparer and Items, whose array +// the reading functions pass to the FSharp.Core Array functions without a copy (ADR 0001, D5). A frozen set has no +// builder, so a transformation collects its result in a System.Collections.Generic.HashSet with the comparer of the +// result: ToFrozenSet takes a hash set with the requested comparer over without copying it into another one. In this file +// HashSet is that mutable hash set, because System.Collections.Generic shadows the HashSet abbreviation of this namespace. +// FSharp.Core sets cannot be null, so the null checks name the parameters of this module (D6). + +/// The struct with the default comparer: the creation functions are its members, so that each has one implementation (D12). +let private withDefaultComparer<'T> () = CustomComparerFrozenSet<'T>(EqualityComparer<'T>.Default) + +// ---- Creation ---- + +[] +[] +let empty<'T> : FrozenSet<'T> = FrozenSet<'T>.Empty + +[] +let forComparer (comparer : IEqualityComparer<'T>) = CustomComparerFrozenSet<'T> comparer + +[] +let ofArray (array : 'T array) = (withDefaultComparer ()).ofArray array + +[] +let ofList (elements : 'T list) = (withDefaultComparer ()).ofList elements + +[] +let ofReadOnlySpan (span : ReadOnlySpan<'T>) = (withDefaultComparer ()).ofReadOnlySpan span + +[] +let ofSeq (elements : seq<'T>) = (withDefaultComparer ()).ofSeq elements + +[] +let ofSpan (span : Span<'T>) = (withDefaultComparer ()).ofSpan span + +[] +let singleton (value : 'T) = (withDefaultComparer ()).singleton value + +// ---- Properties ---- + +[] +let comparer (set : FrozenSet<'T>) = + checkNotNull (nameof set) set + set.Comparer + +[] +let count (set : FrozenSet<'T>) = + checkNotNull (nameof set) set + set.Count + +[] +let isEmpty (set : FrozenSet<'T>) = + checkNotNull (nameof set) set + set.Count = 0 + +// ---- Access ---- + +[] +let tryGetValue (value : 'T) (set : FrozenSet<'T>) = + checkNotNull (nameof set) set + + match set.TryGetValue value with + | true, actual -> ValueSome actual + | false, _ -> ValueNone + +// ---- Search ---- + +[] +let contains (element : 'T) (set : FrozenSet<'T>) = + checkNotNull (nameof set) set + set.Contains element + +[] +let exists (predicate : 'T -> bool) (set : FrozenSet<'T>) = + checkNotNull (nameof set) set + Array.exists predicate (unwrapArray (nameof set) set.Items) + +[] +let forall (predicate : 'T -> bool) (set : FrozenSet<'T>) = + checkNotNull (nameof set) set + Array.forall predicate (unwrapArray (nameof set) set.Items) + +[] +let isProperSubset (set1 : FrozenSet<'T>) (set2 : FrozenSet<'T>) = + checkNotNull (nameof set1) set1 + checkNotNull (nameof set2) set2 + set1.IsProperSubsetOf set2 + +[] +let isProperSuperset (set1 : FrozenSet<'T>) (set2 : FrozenSet<'T>) = + checkNotNull (nameof set1) set1 + checkNotNull (nameof set2) set2 + set1.IsProperSupersetOf set2 + +[] +let isSubset (set1 : FrozenSet<'T>) (set2 : FrozenSet<'T>) = + checkNotNull (nameof set1) set1 + checkNotNull (nameof set2) set2 + set1.IsSubsetOf set2 + +[] +let isSuperset (set1 : FrozenSet<'T>) (set2 : FrozenSet<'T>) = + checkNotNull (nameof set1) set1 + checkNotNull (nameof set2) set2 + set1.IsSupersetOf set2 + +[] +let overlaps (set1 : FrozenSet<'T>) (set2 : FrozenSet<'T>) = + checkNotNull (nameof set1) set1 + checkNotNull (nameof set2) set2 + set1.Overlaps set2 + +[] +let setEquals (set1 : FrozenSet<'T>) (set2 : FrozenSet<'T>) = + checkNotNull (nameof set1) set1 + checkNotNull (nameof set2) set2 + set1.SetEquals set2 + +// ---- Transformation ---- + +[] +let choose (chooser : 'T -> 'U voption) (set : FrozenSet<'T>) = + checkNotNull (nameof set) set + let chosen = HashSet<'U>() + + for item in set.Items do + match chooser item with + | ValueSome value -> chosen.Add value |> ignore + | ValueNone -> () + + chosen.ToFrozenSet () + +[] +let difference (set1 : FrozenSet<'T>) (set2 : FrozenSet<'T>) = + checkNotNull (nameof set1) set1 + checkNotNull (nameof set2) set2 + let difference = HashSet<'T>(set1, set1.Comparer) + difference.ExceptWith set2 + difference.ToFrozenSet set1.Comparer + +[] +let filter (predicate : 'T -> bool) (set : FrozenSet<'T>) = + checkNotNull (nameof set) set + let accepted = HashSet<'T> set.Comparer + + for item in set.Items do + if predicate item then + accepted.Add item |> ignore + + accepted.ToFrozenSet set.Comparer + +[] +let intersect (set1 : FrozenSet<'T>) (set2 : FrozenSet<'T>) = + checkNotNull (nameof set1) set1 + checkNotNull (nameof set2) set2 + // The elements of the second set are looked up in the first one, in the comparer of the first set. The intersection + // keeps the elements of the second set, as ImmutableHashSet.Intersect and ImmutableSortedSet.Intersect do; FrozenSet + // has no such member, so it follows them (ADR 0001, D14) + let intersection = HashSet<'T> set1.Comparer + + for item in set2.Items do + if set1.Contains item then + intersection.Add item |> ignore + + intersection.ToFrozenSet set1.Comparer + +[] +let intersectMany (sets : seq>) = + // Set.intersectMany is Seq.reduce over Set.intersect, so its checks are those of Seq.reduce. A default flat list given as + // a sequence would fail with InvalidOperationException when the reduction enumerates it (ADR 0001, D6) + checkNotNullOrDefaultSeq (nameof sets) sets + + // The type of a set is annotated, because the sequence that fixes it comes last + let checkSet (set : FrozenSet<'T>) = checkNotNull (nameof sets) set + + // Each step builds a frozen set, but an intersection never grows: it is at most as large as the set that the step + // looks up, so the steps together take time in proportion to the elements of the sequence + match Sets.reduce checkSet intersect sets with + | ValueSome result -> result + | ValueNone -> invalidArg (nameof sets) ErrorStrings.InputSequenceEmpty + +[] +let map (mapping : 'T -> 'U) (set : FrozenSet<'T>) = + checkNotNull (nameof set) set + let mapped = HashSet<'U>() + + for item in set.Items do + mapped.Add (mapping item) |> ignore + + mapped.ToFrozenSet () + +[] +let symmetricDifference (set1 : FrozenSet<'T>) (set2 : FrozenSet<'T>) = + checkNotNull (nameof set1) set1 + checkNotNull (nameof set2) set2 + let difference = HashSet<'T>(set1, set1.Comparer) + difference.SymmetricExceptWith set2 + difference.ToFrozenSet set1.Comparer + +[] +let union (set1 : FrozenSet<'T>) (set2 : FrozenSet<'T>) = + checkNotNull (nameof set1) set1 + checkNotNull (nameof set2) set2 + let union = HashSet<'T>(set1, set1.Comparer) + union.UnionWith set2 + union.ToFrozenSet set1.Comparer + +[] +let unionMany (sets : seq>) = + // Set.unionMany is Seq.fold over Set.union, so its checks are those of Seq.fold. A default flat list given as a + // sequence would fail with InvalidOperationException when the fold enumerates it (ADR 0001, D6) + checkNotNullOrDefaultSeq (nameof sets) sets + + // The fold collects the elements of every set in one hash set with the comparer of the first set and freezes them + // once. Uniting the frozen sets pairwise, as intersectMany intersects them, would rebuild a growing frozen set for + // every set of the sequence: quadratic time for many sets. + let union = + (null, sets) + ||> Seq.fold (fun (union : HashSet<'T> | null) set -> + checkNotNull (nameof sets) set + + match union with + | null -> HashSet<'T>(set, set.Comparer) + | union -> + union.UnionWith set + union + ) + + match union with + | null -> empty + | union -> union.ToFrozenSet union.Comparer + +// ---- Folding ---- + +[] +let fold (folder : 'State -> 'T -> 'State) (state : 'State) (set : FrozenSet<'T>) = + checkNotNull (nameof set) set + Array.fold folder state (unwrapArray (nameof set) set.Items) + +[] +let foldBack (folder : 'T -> 'State -> 'State) (set : FrozenSet<'T>) (state : 'State) = + checkNotNull (nameof set) set + Array.foldBack folder (unwrapArray (nameof set) set.Items) state + +[] +let iter (action : 'T -> unit) (set : FrozenSet<'T>) = + checkNotNull (nameof set) set + Array.iter action (unwrapArray (nameof set) set.Items) + +// ---- Partitioning ---- + +[] +let partition (predicate : 'T -> bool) (set : FrozenSet<'T>) = + checkNotNull (nameof set) set + let accepted = HashSet<'T> set.Comparer + let rejected = HashSet<'T> set.Comparer + + for item in set.Items do + if predicate item then + accepted.Add item |> ignore + else + rejected.Add item |> ignore + + struct (accepted.ToFrozenSet set.Comparer, rejected.ToFrozenSet set.Comparer) + +[] +let partitionWith (partitioner : 'T -> Choice<'T1, 'T2>) (set : FrozenSet<'T>) = + checkNotNull (nameof set) set + let first = HashSet<'T1>() + let second = HashSet<'T2>() + + for item in set.Items do + match partitioner item with + | Choice1Of2 value -> first.Add value |> ignore + | Choice2Of2 value -> second.Add value |> ignore + + struct (first.ToFrozenSet (), second.ToFrozenSet ()) + +// ---- Conversion ---- + +[] +let toArray (set : FrozenSet<'T>) = + checkNotNull (nameof set) set + Array.copy (unwrapArray (nameof set) set.Items) + +[] +let toFlatList (set : FrozenSet<'T>) : FlatList<'T> = + checkNotNull (nameof set) set + set.Items + +[] +let toList (set : FrozenSet<'T>) = + checkNotNull (nameof set) set + Array.toList (unwrapArray (nameof set) set.Items) + +[] +let toSeq (set : FrozenSet<'T>) = + checkNotNull (nameof set) set + set :> seq<'T> + +#if NET10_0_OR_GREATER +// ---- Type-specific ---- + +[] +let alternateLookup<'Alternate, 'T> (set : FrozenSet<'T>) = + checkNotNull (nameof set) set + set.GetAlternateLookup<'Alternate>() + +[] +let tryAlternateLookup<'Alternate, 'T> (set : FrozenSet<'T>) = + checkNotNull (nameof set) set + + match set.TryGetAlternateLookup<'Alternate>() with + | true, lookup -> ValueSome lookup + | false, _ -> ValueNone +#endif diff --git a/src/FSharp.Collections.Immutable/FrozenSet/FrozenSet.fsi b/src/FSharp.Collections.Immutable/FrozenSet/FrozenSet.fsi new file mode 100644 index 00000000..0253c2ef --- /dev/null +++ b/src/FSharp.Collections.Immutable/FrozenSet/FrozenSet.fsi @@ -0,0 +1,671 @@ +/// +/// Functions for , the frozen set. +/// +/// +/// +/// A frozen set is built once and read many times: it costs more to create than a hash set and finds its elements faster. +/// The module therefore offers creation, lookup, iteration, folds, conversions and the transformations of a whole set, +/// but no function that adds or removes a single element (ADR 0001, D1). Every transformation is an O(n) rebuild: it +/// collects its result and creates a new frozen set from it. +/// +/// +/// The module mirrors : each function has the semantics, the +/// argument order and the exceptions of the function with the same name in that module (ADR 0001). Functions that +/// FSharp.Core lacks wrap a member of and throw what that member +/// throws. Every function throws for a frozen set. +/// +/// +/// A frozen set finds an element in O(1) on average. The order in which a frozen set enumerates its elements is +/// unspecified; the functions that visit the elements visit them in that order. +/// +/// +/// The creation functions create frozen sets that compare with +/// . +/// +/// creates a , whose members create frozen sets with +/// another comparer (ADR 0001, D12). +/// +/// +/// The transformations that keep the element type keep the comparer of their input; the set algebra keeps the comparer of +/// the first set and compares with it. The transformations that change the element type compare the new elements with +/// the default comparer of their type. +/// +/// +/// The module has the name of the class and coexists with it: after +/// open System.Collections.Frozen, FrozenSet.ofSeq calls this module and FrozenSet.ToFrozenSet calls +/// the class, whichever namespace is opened last (ADR 0001, D3). +/// +/// +/// 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.FrozenSet + +open System +open System.Collections.Frozen +open System.Collections.Generic + +// ---- Creation ---- + +/// +/// The empty frozen set, which compares with . +/// +/// +/// O(1). The value is . +/// +[] +[] +val empty<'T> : FrozenSet<'T> + +/// +/// Creates the struct that creates frozen sets that compare their elements with an equality comparer. +/// +/// The equality comparer of the elements. +/// +/// The whose members are the creation functions of +/// this module with the comparer: they create frozen sets that compare with it. +/// +/// Thrown when is . +/// +/// +/// O(1). Creating the struct allocates nothing. +/// +/// also gives an existing frozen set the comparer (ADR 0001, D12). +/// +/// +/// A comparer throws, where the BCL methods Create and ToFrozenSet take it as the +/// default comparer. For that meaning use +/// , which gives +/// a frozen set the default comparer, or the default value of the struct, which compares with +/// . +/// +/// +/// +/// +/// let names = (FrozenSet.forComparer StringComparer.OrdinalIgnoreCase).ofSeq [ "Ann"; "ANN"; "Bob" ] +/// +/// Evaluates to a frozen set of "Ann" and "Bob" that compares without case. +/// +[] +val forComparer : comparer : IEqualityComparer<'T> -> CustomComparerFrozenSet<'T> + +/// +/// Creates a frozen set from the elements of an array. +/// +/// The input array. +/// The frozen set of the distinct elements of the array. +/// Thrown when is . +/// +/// O(n). Later changes to the array do not change the frozen set. +/// +[] +val ofArray : array : 'T array -> FrozenSet<'T> + +/// Creates a frozen set from the elements of a list. +/// The input list. +/// The frozen set of the distinct elements of the list. +/// Thrown when is . +/// O(n). +[] +val ofList : elements : 'T list -> FrozenSet<'T> + +/// +/// Creates a frozen set from the elements of a read-only span. +/// +/// The input span. +/// +/// The frozen set of the distinct elements of the span; an empty span gives an empty frozen set. +/// +/// +/// O(n). Later changes to the memory under the span do not change the frozen set. The function wraps +/// . +/// +[] +val ofReadOnlySpan : span : ReadOnlySpan<'T> -> FrozenSet<'T> + +/// +/// Creates a frozen set from the elements of a sequence. +/// +/// The input sequence. +/// The frozen set of the distinct elements of the sequence. +/// Thrown when is or a default flat list. +/// +/// O(n), or O(1) for a frozen set that compares with the default comparer: the function returns such a frozen set +/// without a copy, and gives a frozen set with another comparer the default one. The function wraps +/// . +/// +[] +val ofSeq : elements : seq<'T> -> FrozenSet<'T> + +/// +/// Creates a frozen set from the elements of a span. +/// +/// The input span. +/// +/// The frozen set of the distinct elements of the span; an empty span gives an empty frozen set. +/// +/// +/// O(n). Later changes to the memory under the span do not change the frozen set. The function converts the span to a +/// read-only span and wraps +/// . +/// +[] +val ofSpan : span : Span<'T> -> FrozenSet<'T> + +/// Creates a frozen set of one element. +/// The element. +/// The frozen set of one element. +/// O(1). +[] +val singleton : value : 'T -> FrozenSet<'T> + +// ---- Properties ---- + +/// +/// Returns the equality comparer of the elements of the frozen set. +/// +/// The input frozen set. +/// The equality comparer. +/// Thrown when is . +/// +/// O(1). The function wraps . +/// +[] +val comparer : set : FrozenSet<'T> -> IEqualityComparer<'T> + +/// Returns the number of elements of the frozen set. +/// The input frozen set. +/// The number of elements. +/// Thrown when is . +/// O(1). +[] +val count : set : FrozenSet<'T> -> int + +/// Returns whether the frozen set has no elements. +/// The input frozen set. +/// when the frozen set is empty. +/// Thrown when is . +/// O(1). +[] +val isEmpty : set : FrozenSet<'T> -> bool + +// ---- Access ---- + +/// +/// Returns the element of the frozen set that equals a value, or ValueNone when there is none. +/// +/// The value to search for. +/// The input frozen set. +/// The element that the comparer of the frozen set considers equal to the value, or ValueNone. +/// Thrown when is . +/// +/// O(1) on average. The function wraps . The +/// element can differ from an equal value, for example in case under a case-insensitive comparer; the function returns +/// the instance that the frozen set holds. +/// +[] +val tryGetValue : value : 'T -> set : FrozenSet<'T> -> 'T voption + +// ---- Search ---- + +/// +/// Returns whether the frozen set contains an element equal to a value, in the comparer of the frozen set. +/// +/// The value to find. +/// The input frozen set. +/// when an element equals the value. +/// Thrown when is . +/// +/// O(1) on average. The function wraps . +/// +[] +val contains : element : 'T -> set : FrozenSet<'T> -> bool + +/// +/// Returns whether any element satisfies the predicate. +/// +/// The function to test the elements. +/// The input frozen set. +/// when an element satisfies the predicate. +/// Thrown when is . +/// +/// O(n). The function stops at the first element that satisfies the predicate. +/// +[] +val exists : predicate : ('T -> bool) -> set : FrozenSet<'T> -> bool + +/// +/// Returns whether all elements satisfy the predicate. +/// +/// The function to test the elements. +/// The input frozen set. +/// when every element satisfies the predicate. +/// Thrown when is . +/// +/// O(n). The function stops at the first element that does not satisfy the predicate. +/// +[] +val forall : predicate : ('T -> bool) -> set : FrozenSet<'T> -> bool + +/// +/// Returns whether every element of the first frozen set is in the second one, and the second one has an element that +/// the first one does not have. +/// +/// The potential subset. +/// The set to test against. +/// when the first frozen set is a proper subset of the second one. +/// Thrown when or is . +/// +/// O(n + m), where n and m are the numbers of elements of the first and the second frozen set. The function wraps +/// +/// and compares in the comparer of the first frozen set. +/// +[] +val isProperSubset : set1 : FrozenSet<'T> -> set2 : FrozenSet<'T> -> bool + +/// +/// Returns whether every element of the second frozen set is in the first one, and the first one has an element that +/// the second one does not have. +/// +/// The potential superset. +/// The set to test against. +/// when the first frozen set is a proper superset of the second one. +/// Thrown when or is . +/// +/// O(m), where m is the number of elements of the second frozen set. The function wraps +/// +/// and compares in the comparer of the first frozen set. +/// +[] +val isProperSuperset : set1 : FrozenSet<'T> -> set2 : FrozenSet<'T> -> bool + +/// +/// Returns whether every element of the first frozen set is in the second one. +/// +/// The potential subset. +/// The set to test against. +/// when the first frozen set is a subset of the second one. +/// Thrown when or is . +/// +/// O(n + m), where n and m are the numbers of elements of the first and the second frozen set. The function wraps +/// +/// and compares in the comparer of the first frozen set. +/// +[] +val isSubset : set1 : FrozenSet<'T> -> set2 : FrozenSet<'T> -> bool + +/// +/// Returns whether every element of the second frozen set is in the first one. +/// +/// The potential superset. +/// The set to test against. +/// when the first frozen set is a superset of the second one. +/// Thrown when or is . +/// +/// O(m), where m is the number of elements of the second frozen set. The function wraps +/// +/// and compares in the comparer of the first frozen set. +/// +[] +val isSuperset : set1 : FrozenSet<'T> -> set2 : FrozenSet<'T> -> bool + +/// +/// Returns whether the frozen sets have an element in common. +/// +/// The first frozen set. +/// The second frozen set. +/// when an element of the second frozen set is in the first one. +/// Thrown when or is . +/// +/// O(m), where m is the number of elements of the second frozen set. The function wraps +/// +/// and compares in the comparer of the first frozen set. +/// +[] +val overlaps : set1 : FrozenSet<'T> -> set2 : FrozenSet<'T> -> bool + +/// +/// Returns whether the frozen sets have the same elements. +/// +/// The first frozen set. +/// The second frozen set. +/// when every element of each frozen set is in the other one. +/// Thrown when or is . +/// +/// O(m), where m is the number of elements of the second frozen set. The function wraps +/// +/// and compares in the comparer of the first frozen set. +/// +[] +val setEquals : set1 : FrozenSet<'T> -> set2 : FrozenSet<'T> -> bool + +// ---- Transformation ---- + +/// +/// Returns the values that the chooser returns for the elements. +/// +/// The function that returns a value, or ValueNone to skip the element. +/// The input frozen set. +/// +/// The frozen set of the distinct chosen values, which compares with +/// . +/// +/// Thrown when is . +/// +/// O(n): the function builds a new frozen set. The comparer of the input is not kept, even when the chosen values have +/// the type of the elements: the function cannot keep it for every type without a type test that would make its result +/// depend on the type. +/// +[] +val choose : chooser : ('T -> 'U voption) -> set : FrozenSet<'T> -> FrozenSet<'U> + +/// +/// Returns the elements of the first frozen set that are not in the second one. +/// +/// The first frozen set. +/// The frozen set whose elements are removed from the first one. +/// The difference, with the comparer of the first frozen set. +/// Thrown when or is . +/// +/// O(n + m), where n and m are the numbers of elements of the first and the second frozen set: the function builds a +/// new frozen set. It compares in the comparer of the first frozen set. +/// +[] +val difference : set1 : FrozenSet<'T> -> set2 : FrozenSet<'T> -> FrozenSet<'T> + +/// Returns the elements that satisfy the predicate. +/// The function to test the elements. +/// The input frozen set. +/// The frozen set of the elements that satisfy the predicate, with the comparer of the input. +/// Thrown when is . +/// O(n): the function builds a new frozen set. +[] +val filter : predicate : ('T -> bool) -> set : FrozenSet<'T> -> FrozenSet<'T> + +/// +/// Returns the elements that are in both frozen sets. +/// +/// The first frozen set. +/// The second frozen set. +/// +/// The intersection, with the comparer of the first frozen set; where the comparer considers two different elements +/// equal, the intersection holds the one of the second frozen set. +/// +/// Thrown when or is . +/// +/// O(m), where m is the number of elements of the second frozen set: the function looks them up in the first frozen +/// set, in its comparer, and builds a new frozen set of the elements of the second frozen set that it finds. It keeps +/// the elements of the second frozen set, as +/// +/// and +/// do (ADR 0001, D14). +/// +[] +val intersect : set1 : FrozenSet<'T> -> set2 : FrozenSet<'T> -> FrozenSet<'T> + +/// +/// Returns the elements that are in every frozen set of a sequence. +/// +/// The frozen sets. +/// +/// The intersection, with the comparer of the first frozen set; where the comparer considers different elements equal, +/// the intersection holds the one of the last frozen set, as +/// +/// holds the one of its second frozen set. For a sequence of one frozen set, that frozen set itself. +/// +/// Thrown when is or a default flat list, or one of its frozen sets is . +/// Thrown when the sequence is empty. +/// +/// O(N), where N is the total number of elements of the frozen sets after the first one. The function intersects the +/// frozen sets from the first one to the last one and builds a new frozen set at each step; an intersection never grows, +/// so the steps together take O(N). +/// +[] +val intersectMany : sets : seq> -> FrozenSet<'T> + +/// +/// Returns the results of a function applied to each element. +/// +/// The function to transform the elements. +/// The input frozen set. +/// +/// The frozen set of the distinct results, which compares with +/// . +/// +/// Thrown when is . +/// +/// O(n): the function builds a new frozen set. The comparer of the input is not kept, even when the results have the +/// type of the elements: the function cannot keep it for every type without a type test that would make its result +/// depend on the type. +/// +[] +val map : mapping : ('T -> 'U) -> set : FrozenSet<'T> -> FrozenSet<'U> + +/// +/// Returns the elements that are in exactly one of the frozen sets. +/// +/// The first frozen set. +/// The second frozen set. +/// The symmetric difference, with the comparer of the first frozen set. +/// Thrown when or is . +/// +/// O(n + m), where n and m are the numbers of elements of the first and the second frozen set: the function builds a +/// new frozen set. It compares in the comparer of the first frozen set. +/// +[] +val symmetricDifference : set1 : FrozenSet<'T> -> set2 : FrozenSet<'T> -> FrozenSet<'T> + +/// +/// Returns the elements that are in either frozen set. +/// +/// The first frozen set. +/// The second frozen set. +/// +/// The union, with the comparer of the first frozen set; where the comparer considers two different elements equal, +/// the union holds the one of the first frozen set. +/// +/// Thrown when or is . +/// +/// O(n + m), where n and m are the numbers of elements of the first and the second frozen set: the function builds a +/// new frozen set. It compares in the comparer of the first frozen set. +/// +[] +val union : set1 : FrozenSet<'T> -> set2 : FrozenSet<'T> -> FrozenSet<'T> + +/// +/// Returns the elements that are in any frozen set of a sequence. +/// +/// The frozen sets. +/// +/// The union, with the comparer of the first frozen set; the empty frozen set with the default comparer for an empty +/// sequence. +/// +/// Thrown when is or a default flat list, or one of its frozen sets is . +/// +/// O(N), where N is the total number of elements of the frozen sets. The function collects the elements of the frozen +/// sets from the first one to the last one and builds one new frozen set from them. +/// +[] +val unionMany : sets : seq> -> FrozenSet<'T> + +// ---- Folding ---- + +/// +/// Applies a function to a state and each element, in the order of enumeration. +/// +/// The function that computes the next state. +/// The initial state. +/// The input frozen set. +/// The final state. +/// Thrown when is . +/// +/// O(n). +/// +[] +val fold : folder : ('State -> 'T -> 'State) -> state : 'State -> set : FrozenSet<'T> -> 'State + +/// +/// Applies a function to each element and a state, in the reverse order of enumeration. +/// +/// The function that computes the next state. +/// The input frozen set. +/// The initial state. +/// The final state. +/// Thrown when is . +/// +/// O(n). +/// +[] +val foldBack : folder : ('T -> 'State -> 'State) -> set : FrozenSet<'T> -> state : 'State -> 'State + +/// +/// Applies an action to each element, in the order of enumeration. +/// +/// The action. +/// The input frozen set. +/// Thrown when is . +/// +/// O(n). +/// +[] +val iter : action : ('T -> unit) -> set : FrozenSet<'T> -> unit + +// ---- Partitioning ---- + +/// Splits the elements by a predicate. +/// The function to test the elements. +/// The input frozen set. +/// +/// The frozen set of the elements that satisfy the predicate and the frozen set of the other ones, both with the +/// comparer of the input. +/// +/// Thrown when is . +/// O(n): the function builds two new frozen sets. +[] +val partition : predicate : ('T -> bool) -> set : FrozenSet<'T> -> struct (FrozenSet<'T> * FrozenSet<'T>) + +/// +/// Splits the elements by the choice that a function returns for them. +/// +/// The function that returns Choice1Of2 or Choice2Of2 for an element. +/// The input frozen set. +/// +/// The frozen set of the values of the first choices and the frozen set of the values of the second choices, which +/// compare with . +/// +/// Thrown when is . +/// +/// O(n): the function builds two new frozen sets. +/// +[] +val partitionWith : partitioner : ('T -> Choice<'T1, 'T2>) -> set : FrozenSet<'T> -> struct (FrozenSet<'T1> * FrozenSet<'T2>) + +// ---- Conversion ---- + +/// +/// Returns a new array with the elements, in the order of enumeration. +/// +/// The input frozen set. +/// A copy of the elements. +/// Thrown when is . +/// +/// O(n). +/// +[] +val toArray : set : FrozenSet<'T> -> 'T array + +/// +/// Returns the elements of the frozen set as a flat list, in the order of enumeration. +/// +/// The input frozen set. +/// The flat list of the elements, which shares the storage of the frozen set. +/// Thrown when is . +/// +/// O(1). The function returns without a copy. +/// +[] +val toFlatList : set : FrozenSet<'T> -> FlatList<'T> + +/// Returns a list of the elements, in the order of enumeration. +/// The input frozen set. +/// The list of the elements. +/// Thrown when is . +/// O(n). +[] +val toList : set : FrozenSet<'T> -> 'T list + +/// Returns the frozen set as a sequence. +/// The input frozen set. +/// The sequence of the elements. +/// Thrown when is . +/// O(1). +[] +val toSeq : set : FrozenSet<'T> -> seq<'T> + +#if NET10_0_OR_GREATER +// ---- Type-specific ---- + +/// +/// Returns a lookup that finds the elements of the frozen set by values of an alternate type. +/// +/// The input frozen set. +/// +/// The lookup, whose and +/// take a value of the +/// alternate type. +/// +/// Thrown when is . +/// +/// Thrown when the comparer of the frozen set does not implement +/// for the alternate type and the element type. +/// +/// +/// +/// O(1). The function wraps and exists on +/// net10.0 only. The alternate type comes first, so that F# can infer the element type: +/// FrozenSet.alternateLookup<int, _> set. +/// +/// +/// An F# generic function cannot be instantiated with a byref-like type, so the function cannot look up by a +/// , the alternate type of strings under +/// . Call +/// directly for a span: set.GetAlternateLookup<ReadOnlySpan<char>>(). A frozen set of strings supports +/// no other alternate type. +/// +/// +[] +val alternateLookup<'Alternate, 'T> : set : FrozenSet<'T> -> FrozenSet<'T>.AlternateLookup<'Alternate> + +/// +/// Returns a lookup that finds the elements of the frozen set by values of an alternate type, or ValueNone when +/// the comparer of the frozen set cannot compare that type. +/// +/// The input frozen set. +/// +/// The lookup, or ValueNone when the comparer of the frozen set does not implement +/// for the alternate type and the element type. +/// +/// Thrown when is . +/// +/// +/// O(1). The function wraps +/// +/// and exists on net10.0 only. The alternate type comes first, so that F# can infer the element type: +/// FrozenSet.tryAlternateLookup<int, _> set. +/// +/// +/// An F# generic function cannot be instantiated with a byref-like type, so the function cannot look up by a +/// , the alternate type of strings under +/// . Call +/// +/// directly for a span. A frozen set of strings supports no other alternate type, so the function returns +/// ValueNone for it. +/// +/// +[] +val tryAlternateLookup<'Alternate, 'T> : set : FrozenSet<'T> -> FrozenSet<'T>.AlternateLookup<'Alternate> voption +#endif diff --git a/src/FSharp.Collections.Immutable/FrozenSet/FrozenSetCE.fs b/src/FSharp.Collections.Immutable/FrozenSet/FrozenSetCE.fs new file mode 100644 index 00000000..677caab6 --- /dev/null +++ b/src/FSharp.Collections.Immutable/FrozenSet/FrozenSetCE.fs @@ -0,0 +1,90 @@ +namespace FSharp.Collections.Immutable + +open System.Collections.Frozen +open System.Collections.Generic + +type FrozenSetBuffer<'T> = ResizeArray<'T> -> unit + +[] +type FrozenSetCE<'T> (comparer : IEqualityComparer<'T>) = + + // The builder checks its comparer when it is created, as CustomComparerFrozenSet does, not when an expression runs + do checkNotNull (nameof comparer) comparer + + new () = FrozenSetCE<'T>(EqualityComparer<'T>.Default) + + member inline _.Zero () : FrozenSetBuffer<'T> = ignore + + member inline _.Yield (item : 'T) : FrozenSetBuffer<'T> = fun buffer -> buffer.Add item + + // Each input is checked before the buffer is returned, so that the exception names the parameter of the expression and + // not the parameter of AddRange (collection), and so that a null array is rejected: AddRange does not throw for it. A + // default flat list given as a sequence is rejected as FlatListCE rejects it (ADR 0001, D6) + member inline _.YieldFrom (items : seq<'T>) : FrozenSetBuffer<'T> = + checkNotNullOrDefaultSeq (nameof items) items + fun buffer -> buffer.AddRange items + + member inline _.YieldFrom (items : 'T array) : FrozenSetBuffer<'T> = + checkNotNull (nameof items) items + fun buffer -> buffer.AddRange items + + member inline _.YieldFrom (items : FrozenSet<'T>) : FrozenSetBuffer<'T> = + checkNotNull (nameof items) items + fun buffer -> buffer.AddRange items + + member inline _.YieldFrom (buffer : FrozenSetBuffer<'T>) : FrozenSetBuffer<'T> = buffer + + member inline _.Combine (first : FrozenSetBuffer<'T>, second : FrozenSetBuffer<'T>) : FrozenSetBuffer<'T> = + fun buffer -> + first buffer + second buffer + + member inline _.Delay ([] delayed : unit -> FrozenSetBuffer<'T>) : FrozenSetBuffer<'T> = + fun buffer -> delayed () buffer + + member inline _.For (source : seq<'U>, [] body : 'U -> FrozenSetBuffer<'T>) : FrozenSetBuffer<'T> = + // A null source would throw NullReferenceException and a default flat list InvalidOperationException when the loop + // enumerates; the expression rejects both as yield! does + checkNotNullOrDefaultSeq (nameof source) source + + fun buffer -> + for item in source do + body item buffer + + member inline _.While ([] guard : unit -> bool, body : FrozenSetBuffer<'T>) : FrozenSetBuffer<'T> = + fun buffer -> + while guard () do + body buffer + + member inline _.TryWith + (body : FrozenSetBuffer<'T>, [] handler : exn -> FrozenSetBuffer<'T>) + : FrozenSetBuffer<'T> + = + fun buffer -> + try + body buffer + with ex -> + handler ex buffer + + member inline _.TryFinally + (body : FrozenSetBuffer<'T>, [] compensation : unit -> unit) + : FrozenSetBuffer<'T> + = + fun buffer -> + try + body buffer + finally + compensation () + + member inline _.Using + (resource : 'Resource, [] body : 'Resource -> FrozenSetBuffer<'T>) + : FrozenSetBuffer<'T> + = + fun buffer -> + use resource = resource + body resource buffer + + member _.Run (buffer : FrozenSetBuffer<'T>) : FrozenSet<'T> = + let elements = ResizeArray<'T>() + buffer elements + elements.ToFrozenSet comparer diff --git a/src/FSharp.Collections.Immutable/FrozenSet/FrozenSetCE.fsi b/src/FSharp.Collections.Immutable/FrozenSet/FrozenSetCE.fsi new file mode 100644 index 00000000..aedace4a --- /dev/null +++ b/src/FSharp.Collections.Immutable/FrozenSet/FrozenSetCE.fsi @@ -0,0 +1,89 @@ +namespace FSharp.Collections.Immutable + +open System +open System.Collections.Frozen +open System.Collections.Generic + +/// +/// The body of a frozenSet { } expression: a function that adds the elements to a buffer. +/// +type FrozenSetBuffer<'T> = ResizeArray<'T> -> unit + +/// +/// The builder of the frozenSet { } computation expression. +/// +/// +/// A frozen set has no builder, so the expression collects its elements in a +/// and freezes them once, with +/// +/// and the equality comparer of the builder (ADR 0001, D7). Of the elements that the comparer considers equal, the frozen +/// set holds the first one. frozenSet { } compares with +/// ; an instance created with a comparer builds frozen +/// sets that compare with it. +/// +[] +type FrozenSetCE<'T> = + + /// + /// Creates the builder of frozen sets that compare with an equality comparer. + /// + /// The equality comparer of the elements of the frozen set. + /// Thrown when is . + new : comparer : IEqualityComparer<'T> -> FrozenSetCE<'T> + + /// + /// Creates the builder of frozen sets that compare with . + /// + new : unit -> FrozenSetCE<'T> + + /// An expression without elements. + member inline Zero : unit -> FrozenSetBuffer<'T> + + /// Adds an element. + member inline Yield : item : 'T -> FrozenSetBuffer<'T> + + /// Adds the elements of a sequence. + /// Thrown when the sequence is or a default flat list. + member inline YieldFrom : items : seq<'T> -> FrozenSetBuffer<'T> + + /// Adds the elements of an array. + /// Thrown when the array is . + member inline YieldFrom : items : 'T array -> FrozenSetBuffer<'T> + + /// Adds the elements of a frozen set. + /// Thrown when the frozen set is . + member inline YieldFrom : items : FrozenSet<'T> -> FrozenSetBuffer<'T> + + /// Adds the elements of another expression body. + member inline YieldFrom : buffer : FrozenSetBuffer<'T> -> FrozenSetBuffer<'T> + + /// Adds the elements of two parts of the expression, in order. + member inline Combine : first : FrozenSetBuffer<'T> * second : FrozenSetBuffer<'T> -> FrozenSetBuffer<'T> + + /// + /// Runs the body of the expression each time the elements are collected. + /// + member inline Delay : [] delayed : (unit -> FrozenSetBuffer<'T>) -> FrozenSetBuffer<'T> + + /// Adds the elements of the body for each item of a sequence. + /// Thrown when the sequence is or a default flat list. + member inline For : source : seq<'U> * [] body : ('U -> FrozenSetBuffer<'T>) -> FrozenSetBuffer<'T> + + /// Adds the elements of the body while the guard holds. + member inline While : [] guard : (unit -> bool) * body : FrozenSetBuffer<'T> -> FrozenSetBuffer<'T> + + /// Handles the exceptions of the body. + member inline TryWith : + body : FrozenSetBuffer<'T> * [] handler : (exn -> FrozenSetBuffer<'T>) -> FrozenSetBuffer<'T> + + /// Runs the compensation after the body. + member inline TryFinally : + body : FrozenSetBuffer<'T> * [] compensation : (unit -> unit) -> FrozenSetBuffer<'T> + + /// Disposes a resource after the body. + member inline Using : + resource : 'Resource * [] body : ('Resource -> FrozenSetBuffer<'T>) -> FrozenSetBuffer<'T> + when 'Resource :> IDisposable | null + + /// Collects the elements of the expression and freezes them. + member Run : buffer : FrozenSetBuffer<'T> -> FrozenSet<'T> diff --git a/src/FSharp.Collections.Immutable/FrozenSet/FrozenSetOption.fs b/src/FSharp.Collections.Immutable/FrozenSet/FrozenSetOption.fs new file mode 100644 index 00000000..0f3278f8 --- /dev/null +++ b/src/FSharp.Collections.Immutable/FrozenSet/FrozenSetOption.fs @@ -0,0 +1,28 @@ +[] +module FSharp.Collections.Immutable.Option.FrozenSet + +open System.Collections.Frozen + +// The functions delegate to the default module and convert between options and value options. + +// ---- Access ---- + +[] +let tryGetValue (value : 'T) (set : FrozenSet<'T>) = + FSharp.Collections.Immutable.FrozenSet.tryGetValue value set + |> ValueOption.toOption + +// ---- Transformation ---- + +[] +let choose (chooser : 'T -> 'U option) (set : FrozenSet<'T>) = + FSharp.Collections.Immutable.FrozenSet.choose (fun item -> chooser item |> ValueOption.ofOption) set + +#if NET10_0_OR_GREATER +// ---- Type-specific ---- + +[] +let tryAlternateLookup<'Alternate, 'T> (set : FrozenSet<'T>) = + FSharp.Collections.Immutable.FrozenSet.tryAlternateLookup<'Alternate, 'T> set + |> ValueOption.toOption +#endif diff --git a/src/FSharp.Collections.Immutable/FrozenSet/FrozenSetOption.fsi b/src/FSharp.Collections.Immutable/FrozenSet/FrozenSetOption.fsi new file mode 100644 index 00000000..69f4fd3c --- /dev/null +++ b/src/FSharp.Collections.Immutable/FrozenSet/FrozenSetOption.fsi @@ -0,0 +1,72 @@ +/// +/// 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). has none of +/// these functions; they take and return options as FSharp.Core would. +/// +[] +module FSharp.Collections.Immutable.Option.FrozenSet + +open System.Collections.Frozen + +// ---- Access ---- + +/// +/// Returns the element of the frozen set that equals a value, or None when there is none. +/// +/// The value to search for. +/// The input frozen set. +/// The element that the comparer of the frozen set considers equal to the value, or None. +/// Thrown when is . +/// +/// O(1) on average. The function wraps . The +/// element can differ from an equal value, for example in case under a case-insensitive comparer; the function returns +/// the instance that the frozen set holds. +/// +[] +val tryGetValue : value : 'T -> set : FrozenSet<'T> -> 'T option + +// ---- Transformation ---- + +/// +/// Returns the values that the chooser returns for the elements. +/// +/// The function that returns a value, or None to skip the element. +/// The input frozen set. +/// +/// The frozen set of the distinct chosen values, which compares with +/// . +/// +/// Thrown when is . +/// +/// O(n): the function builds a new frozen set. The comparer of the input is not kept, even when the chosen values have +/// the type of the elements. +/// +[] +val choose : chooser : ('T -> 'U option) -> set : FrozenSet<'T> -> FrozenSet<'U> + +#if NET10_0_OR_GREATER +// ---- Type-specific ---- + +/// +/// Returns a lookup that finds the elements of the frozen set by values of an alternate type, or None when the +/// comparer of the frozen set cannot compare that type. +/// +/// The input frozen set. +/// +/// The lookup, or None when the comparer of the frozen set does not implement +/// for the alternate type and the element type. +/// +/// Thrown when is . +/// +/// O(1). The function wraps +/// +/// and exists on net10.0 only. An F# generic function cannot be instantiated with a byref-like type, so a lookup +/// by a needs that method. +/// +[] +val tryAlternateLookup<'Alternate, 'T> : set : FrozenSet<'T> -> FrozenSet<'T>.AlternateLookup<'Alternate> option +#endif diff --git a/src/FSharp.Collections.Immutable/FrozenSet/FrozenSetTuple.fs b/src/FSharp.Collections.Immutable/FrozenSet/FrozenSetTuple.fs new file mode 100644 index 00000000..74781946 --- /dev/null +++ b/src/FSharp.Collections.Immutable/FrozenSet/FrozenSetTuple.fs @@ -0,0 +1,18 @@ +[] +module FSharp.Collections.Immutable.Tuple.FrozenSet + +open System.Collections.Frozen + +// The functions delegate to the default module and repack its struct tuples. + +// ---- Partitioning ---- + +[] +let partition (predicate : 'T -> bool) (set : FrozenSet<'T>) = + let struct (accepted, rejected) = FSharp.Collections.Immutable.FrozenSet.partition predicate set + accepted, rejected + +[] +let partitionWith (partitioner : 'T -> Choice<'T1, 'T2>) (set : FrozenSet<'T>) = + let struct (first, second) = FSharp.Collections.Immutable.FrozenSet.partitionWith partitioner set + first, second diff --git a/src/FSharp.Collections.Immutable/FrozenSet/FrozenSetTuple.fsi b/src/FSharp.Collections.Immutable/FrozenSet/FrozenSetTuple.fsi new file mode 100644 index 00000000..82c3048c --- /dev/null +++ b/src/FSharp.Collections.Immutable/FrozenSet/FrozenSetTuple.fsi @@ -0,0 +1,43 @@ +/// +/// 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 . +/// +[] +module FSharp.Collections.Immutable.Tuple.FrozenSet + +open System.Collections.Frozen + +// ---- Partitioning ---- + +/// Splits the elements by a predicate. +/// The function to test the elements. +/// The input frozen set. +/// +/// The frozen set of the elements that satisfy the predicate and the frozen set of the other ones, both with the +/// comparer of the input. +/// +/// Thrown when is . +/// O(n): the function builds two new frozen sets. +[] +val partition : predicate : ('T -> bool) -> set : FrozenSet<'T> -> FrozenSet<'T> * FrozenSet<'T> + +/// +/// Splits the elements by the choice that a function returns for them. +/// +/// The function that returns Choice1Of2 or Choice2Of2 for an element. +/// The input frozen set. +/// +/// The frozen set of the values of the first choices and the frozen set of the values of the second choices, which +/// compare with . +/// +/// Thrown when is . +/// +/// O(n): the function builds two new frozen sets. +/// +[] +val partitionWith : partitioner : ('T -> Choice<'T1, 'T2>) -> set : FrozenSet<'T> -> FrozenSet<'T1> * FrozenSet<'T2> 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 acc60db8..6205747f 100644 --- a/tests/FSharp.Collections.Immutable.Tests/FSharp.Collections.Immutable.Tests.fsproj +++ b/tests/FSharp.Collections.Immutable.Tests/FSharp.Collections.Immutable.Tests.fsproj @@ -82,6 +82,12 @@ + + + + + + diff --git a/tests/FSharp.Collections.Immutable.Tests/FrozenSet/FrozenSetComputationExpressionTests.fs b/tests/FSharp.Collections.Immutable.Tests/FrozenSet/FrozenSetComputationExpressionTests.fs new file mode 100644 index 00000000..cd7ce093 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/FrozenSet/FrozenSetComputationExpressionTests.fs @@ -0,0 +1,267 @@ +namespace FSharp.Collections.Immutable.Tests + +open System +open System.Collections.Frozen +open System.Collections.Generic +open FSharp.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// The `frozenSet { }` computation expression and the builders with a comparer. +[] +type FrozenSetComputationExpressionTests () = + + /// The elements of a frozen set, sorted in the order of F# generic comparison. + static let sorted (set : FrozenSet<'T>) = FrozenSet.toArray set |> Array.sort + + /// + /// The expressions that reject their input, by name, with the parameter that names the input and the expression. + /// + /// + /// A default flat list converts to a sequence without a check and fails when the sequence is enumerated. A null + /// sequence or frozen set reaches AddRange, which names its own parameter, and a null array passes + /// AddRange without an exception (ADR 0001, D6). Each expression rejects its input when the buffer is created, + /// not when the expression runs. + /// + static let rejections : Map unit))> = + let nullElements = Unchecked.defaultof> + let defaultElements = Unchecked.defaultof> :> seq + + // The flat list of strings is a sequence of objects, which a type test for the flat list of objects does not see + let widenedElements = FlatListOracles.defaultStringsAsObjects + + Map.ofArray [| + "yield! (null sequence)", struct ("items", (fun () -> frozenSet { yield! nullElements } |> ignore)) + "yield! (default flat list)", struct ("items", (fun () -> frozenSet { yield! defaultElements } |> ignore)) + "yield! (default flat list of strings as objects)", + struct ("items", (fun () -> frozenSet { yield! widenedElements } |> ignore)) + "yield! (null array)", + struct ("items", + (fun () -> + frozenSet { yield! Unchecked.defaultof } + |> ignore + )) + "yield! (null frozen set)", + struct ("items", + (fun () -> + frozenSet { yield! Unchecked.defaultof> } + |> ignore + )) + "for (null sequence)", + struct ("source", + (fun () -> + frozenSet { + for item in nullElements do + yield item + } + |> ignore + )) + "for (default flat list)", + struct ("source", + (fun () -> + frozenSet { + for item in defaultElements do + yield item + } + |> ignore + )) + "for (default flat list of strings as objects)", + struct ("source", + (fun () -> + frozenSet { + for item in widenedElements do + yield item + } + |> ignore + )) + |] + + /// The names of the expressions that reject their input, as rows. + static member Rejections = rejections.Keys + + [] + member _.``an expression without elements is empty`` () = + let set : FrozenSet = frozenSet { () } + Assert.IsTrue (FrozenSet.isEmpty set, "frozenSet { () } must be empty") + + [] + member _.``yield adds each distinct element once`` () = + let set = frozenSet { + yield 1 + yield 2 + yield 1 + } + + CollectionAssert.AreEqual ([| 1; 2 |], sorted set, "frozenSet must add each distinct element once") + + [] + member _.``implicit yields add the elements`` () = + let set = frozenSet { + 3 + 1 + 2 + } + + CollectionAssert.AreEqual ([| 1; 2; 3 |], sorted set, "frozenSet must add the elements of implicit yields") + + [] + member _.``yield! adds a sequence, an array, a frozen set and another expression`` () = + let inner = frozenSet { yield 7 } + + let set = frozenSet { + // A list is the sequence that the overload of sequences takes + yield! [ 1; 2 ] + yield! [| 3; 4 |] + yield! FrozenSet.ofArray [| 5; 6; 1 |] + yield! inner + } + + CollectionAssert.AreEqual ([| 1..7 |], sorted set, "frozenSet must add the elements of yield!") + + [] + member _.``yield! and for reject a null input and a default flat list given as a sequence`` (name : string) = + let struct (paramName, actual) = rejections[name] + FlatListAssert.RejectsNull ($"frozenSet {{ %s{name} }}", paramName, actual) + + [] + member _.``yield! and for take a flat list of strings that is not default as a sequence of objects`` () = + let widened = FlatListOracles.stringsAsObjects (FlatList.ofArray [| "a"; "b" |]) + + let set = frozenSet { + yield! widened + + for item in widened do + yield item + } + + CollectionAssert.AreEquivalent ( + [| ("a" :> obj); ("b" :> obj) |], + FrozenSet.toArray set, + "frozenSet must take the elements of a flat list of strings as objects in yield! and for" + ) + + [] + member _.``for adds the elements of the body for each item`` () = + let set = frozenSet { + for item in -2 .. 2 do + yield item * item + } + + CollectionAssert.AreEqual ([| 0; 1; 4 |], sorted set, "frozenSet must run the body of for for each item") + + [] + member _.``while adds the elements while the guard holds`` () = + let mutable count = 0 + + let set = frozenSet { + while count < 3 do + count <- count + 1 + yield count + } + + CollectionAssert.AreEqual ([| 1; 2; 3 |], sorted set, "frozenSet must run the body of while while the guard holds") + + [] + member _.``if without else and following elements combine`` () = + let set = frozenSet { + if false then + yield 0 + + yield 1 + + if true then + yield 2 + } + + CollectionAssert.AreEqual ([| 1; 2 |], sorted set, "frozenSet must combine conditional and plain elements") + + [] + member _.``try with adds the elements of the handler after an exception`` () = + let set = frozenSet { + try + yield 1 + failwith "fails" + with _ -> + yield 2 + } + + CollectionAssert.AreEqual ( + [| 1; 2 |], + sorted set, + "frozenSet must keep the elements before the exception and add the handler's" + ) + + [] + member _.``try finally runs the compensation`` () = + let mutable compensated = false + + let set = frozenSet { + try + yield 1 + finally + compensated <- true + } + + Assert.IsTrue (compensated, "frozenSet must run the compensation of try finally") + CollectionAssert.AreEqual ([| 1 |], sorted set, "frozenSet must keep the elements of the body of try finally") + + [] + member _.``use disposes the resource after the body`` () = + let mutable disposed = false + + let set = frozenSet { + use _resource = { + new IDisposable with + member _.Dispose () = disposed <- true + } + + yield 1 + } + + Assert.IsTrue (disposed, "frozenSet must dispose the resource of use") + CollectionAssert.AreEqual ([| 1 |], sorted set, "frozenSet must keep the elements of the body of use") + + [] + member _.``frozenSet compares with the default comparer`` () = + let set = frozenSet { + "apple" + "APPLE" + } + + Assert.AreSame ( + EqualityComparer.Default :> IEqualityComparer, + FrozenSet.comparer set, + "frozenSet must compare with the default comparer" + ) + + Assert.HasCount (2, set, "frozenSet must tell the case apart") + + [] + member _.``a builder with a comparer builds frozen sets that compare with it`` () = + let ignoreCase : IEqualityComparer = StringComparer.OrdinalIgnoreCase + + let set = FrozenSetCE ignoreCase { + "apple" + "APPLE" + "Banana" + } + + Assert.AreSame (ignoreCase, FrozenSet.comparer set, "FrozenSetCE must build frozen sets with its comparer") + + CollectionAssert.AreEqual ( + [| "Banana"; "apple" |], + sorted set, + "FrozenSetCE must keep the first of the elements equal in its comparer" + ) + + [] + member _.``a builder rejects a null comparer when it is created`` () = + let ex = + Assert.ThrowsExactly( + Action (fun () -> + FrozenSetCE (Unchecked.defaultof>) + |> ignore + ), + "FrozenSetCE must reject a null comparer" + ) + + Assert.AreEqual("comparer", ex.ParamName, "FrozenSetCE must name the parameter comparer") diff --git a/tests/FSharp.Collections.Immutable.Tests/FrozenSet/FrozenSetContractTests.fs b/tests/FSharp.Collections.Immutable.Tests/FrozenSet/FrozenSetContractTests.fs new file mode 100644 index 00000000..66755a8a --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/FrozenSet/FrozenSetContractTests.fs @@ -0,0 +1,21 @@ +namespace FSharp.Collections.Immutable.Tests + +open System.Collections.Frozen +open Microsoft.VisualStudio.TestTools.UnitTesting + +// A frozen set is built once, so it derives no test class from the mutation contract (ADR 0001, D1). + +/// The reading contract for `FrozenSet`. +[] +type FrozenSetSetTests () = + inherit SetContract> (FrozenSetOps.reading) + +/// The transformation contract for `FrozenSet`. +[] +type FrozenSetTransformTests () = + inherit SetTransformContract> (FrozenSetOps.transform) + +/// The creation contract for `FrozenSet`. +[] +type FrozenSetCreationTests () = + inherit SetCreationContract> (FrozenSetOps.creation) diff --git a/tests/FSharp.Collections.Immutable.Tests/FrozenSet/FrozenSetOps.fs b/tests/FSharp.Collections.Immutable.Tests/FrozenSet/FrozenSetOps.fs new file mode 100644 index 00000000..2cc2047b --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/FrozenSet/FrozenSetOps.fs @@ -0,0 +1,118 @@ +/// The functions of the `FrozenSet` module for the contract tests. +[] +module FSharp.Collections.Immutable.Tests.FrozenSetOps + +open System +open System.Collections.Frozen +open System.Collections.Generic +open FSharp.Collections.Immutable + +let private toArrayPair (struct (first : FrozenSet<'T1>, second : FrozenSet<'T2>)) = + struct (FrozenSet.toArray first, FrozenSet.toArray second) + +/// The functions of the reading facet. +let reading : SetOps> = { + Name = nameof FrozenSet + ParamName = SetParamNames.ofFSharpCore + OfSeq = FrozenSet.ofSeq + OfSeqByRemainder = fun elements -> (FrozenSet.forComparer SetInputs.SameRemainder).ofSeq elements + ToArray = FrozenSet.toArray + + Count = FrozenSet.count + IsEmpty = FrozenSet.isEmpty + + TryGetValue = FrozenSet.tryGetValue + + Contains = FrozenSet.contains + Exists = FrozenSet.exists + Forall = FrozenSet.forall + IsProperSubset = FrozenSet.isProperSubset + IsProperSuperset = FrozenSet.isProperSuperset + IsSubset = FrozenSet.isSubset + IsSuperset = FrozenSet.isSuperset + Overlaps = FrozenSet.overlaps + SetEquals = FrozenSet.setEquals + + Fold = FrozenSet.fold + FoldBack = FrozenSet.foldBack + Iter = FrozenSet.iter + + ToList = FrozenSet.toList + ToSeq = FrozenSet.toSeq +} + +/// The functions of the transformation facet. +let transform : SetTransformOps> = { + Name = nameof FrozenSet + ParamName = SetParamNames.ofFSharpCore + OfSeq = FrozenSet.ofSeq + ToArray = FrozenSet.toArray + + Choose = fun chooser set -> FrozenSet.choose chooser set |> FrozenSet.toArray + Difference = FrozenSet.difference + Filter = FrozenSet.filter + Intersect = FrozenSet.intersect + IntersectMany = FrozenSet.intersectMany + Map = fun mapping set -> FrozenSet.map mapping set |> FrozenSet.toArray + SymmetricDifference = FrozenSet.symmetricDifference + Union = FrozenSet.union + UnionMany = FrozenSet.unionMany + + Partition = FrozenSet.partition + PartitionWith = fun partitioner set -> FrozenSet.partitionWith partitioner set |> toArrayPair +} + +/// The functions of the creation facet; the struct of `forComparer` compares with `SetInputs.SameRemainder`. A frozen set +/// has no mutation facet: it is built once (ADR 0001, D1). +let creation : SetCreationOps> = + let custom = FrozenSet.forComparer SetInputs.SameRemainder + + { + Name = nameof FrozenSet + ParamName = SetParamNames.ofFSharpCore + ToArray = FrozenSet.toArray + Comparer = fun set -> FrozenSet.comparer set :> obj + + Empty = fun () -> FrozenSet.empty + OfArray = FrozenSet.ofArray + OfList = FrozenSet.ofList + OfReadOnlySpan = fun array -> FrozenSet.ofReadOnlySpan (ReadOnlySpan array) + OfSeq = FrozenSet.ofSeq + OfSpan = fun array -> FrozenSet.ofSpan (Span array) + Singleton = FrozenSet.singleton + + CustomComparer = { + Comparer = custom.Comparer :> obj + + Empty = fun () -> custom.empty + OfArray = fun array -> custom.ofArray array + OfList = fun elements -> custom.ofList elements + OfReadOnlySpan = fun array -> custom.ofReadOnlySpan (ReadOnlySpan array) + OfSeq = fun elements -> custom.ofSeq elements + OfSpan = fun array -> custom.ofSpan (Span array) + Singleton = fun value -> custom.singleton value + } + } + +#if NET10_0_OR_GREATER +/// +/// Compares values, and values with them, for the alternate +/// lookups, which exist on net10.0 only. +/// +[] +type Int64WithInt32Comparer () = + + interface IEqualityComparer with + member _.Equals (left : int64, right : int64) = left = right + member _.GetHashCode (value : int64) = value.GetHashCode () + + interface IAlternateEqualityComparer with + member _.Equals (alternate : int, other : int64) = int64 alternate = other + member _.GetHashCode (alternate : int) = (int64 alternate).GetHashCode() + member _.Create (alternate : int) = int64 alternate + +/// +/// A frozen set of the values, whose comparer also compares values with them. +/// +let ofInt64WithInt32Comparer (values : seq) = (FrozenSet.forComparer (Int64WithInt32Comparer ())).ofSeq values +#endif diff --git a/tests/FSharp.Collections.Immutable.Tests/FrozenSet/FrozenSetOptionTests.fs b/tests/FSharp.Collections.Immutable.Tests/FrozenSet/FrozenSetOptionTests.fs new file mode 100644 index 00000000..a2aaf65e --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/FrozenSet/FrozenSetOptionTests.fs @@ -0,0 +1,82 @@ +namespace FSharp.Collections.Immutable.Tests + +open System.Collections.Frozen +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 `FrozenSet` module of the `Option` namespace. +[] +type FrozenSetOptionTests () = + + static let numbers = FrozenSet.ofArray Inputs.Several + + static let negatedIfNegative (item : int) = if item < 0 then Some (-item) else None + + [] + member _.``tryGetValue returns an option`` (value : int) = + let found : int option = FrozenSet.tryGetValue value numbers + + OverrideAssert.OptionMatches ( + FSharp.Collections.Immutable.FrozenSet.tryGetValue value numbers, + found, + "Option.FrozenSet.tryGetValue must match the default" + ) + + [] + member _.``choose takes a chooser of options`` () = + let chosen : FrozenSet = FrozenSet.choose negatedIfNegative numbers + + CollectionAssert.AreEqual ( + Inputs.Several + |> Array.choose negatedIfNegative + |> Array.distinct + |> Array.sort, + FrozenSet.toArray chosen |> Array.sort, + "Option.FrozenSet.choose must keep the distinct values that Array.choose chooses" + ) + + [] + member _.``the functions without an option keep resolving to the default module`` () = + let mapped : FrozenSet = FrozenSet.map (fun item -> item * 2) numbers + let (accepted : FrozenSet), (rejected : FrozenSet) = + FrozenSet.partition (fun item -> item > 0) numbers + + CollectionAssert.AreEqual ( + Inputs.Several + |> Array.map (fun item -> item * 2) + |> Array.distinct + |> Array.sort, + FrozenSet.toArray mapped |> Array.sort, + "FrozenSet.map must resolve to the default module" + ) + + Assert.AreEqual( + FrozenSet.count numbers, + FrozenSet.count accepted + FrozenSet.count rejected, + "FrozenSet.partition must resolve to the Tuple module" + ) + +#if NET10_0_OR_GREATER + [] + member _.``tryAlternateLookup returns an option`` () = + let found : FrozenSet.AlternateLookup option = + FrozenSet.tryAlternateLookup(FrozenSetOps.ofInt64WithInt32Comparer [| 1L; 2L; 3L |]) + + match found with + | Some lookup -> + Assert.IsTrue (lookup.Contains 2, "Option.FrozenSet.tryAlternateLookup must return a lookup that finds 2") + | None -> Assert.Fail "Option.FrozenSet.tryAlternateLookup must return the lookup of a comparer with the alternate type" + + let missing : FrozenSet.AlternateLookup option = + FrozenSet.tryAlternateLookup(FrozenSet.ofArray [| 1L |]) + + Assert.IsTrue ( + missing.IsNone, + "Option.FrozenSet.tryAlternateLookup must return None for a comparer that does not compare the alternate type" + ) +#endif diff --git a/tests/FSharp.Collections.Immutable.Tests/FrozenSet/FrozenSetTests.fs b/tests/FSharp.Collections.Immutable.Tests/FrozenSet/FrozenSetTests.fs new file mode 100644 index 00000000..ea9a3515 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/FrozenSet/FrozenSetTests.fs @@ -0,0 +1,1080 @@ +namespace FSharp.Collections.Immutable.Tests + +open System +open System.Collections.Frozen +open System.Collections.Generic +open System.Globalization +open System.Runtime.InteropServices +open FSharp.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// Builds frozen sets in a scope that opens `System.Collections.Frozen` after this library, as a file does that opens it +/// last: the module `FrozenSet` and the class `FrozenSet` must still resolve side by side. +module private FrozenOpenedLast = + + open System.Collections.Frozen + + /// A frozen set from the module. + let fromModule () = FrozenSet.ofArray [| 3; 1; 2 |] + + /// A frozen set from the extension method of the class, called as a static method. + let fromClass () = FrozenSet.ToFrozenSet [| 3; 1; 2 |] + +/// +/// A check of a frozen set of any element type, which a case of FrozenSetSample runs on the frozen set that it +/// builds. +/// +type IFrozenSetCheck = + /// Checks a frozen set. + abstract Check<'T> : set : FrozenSet<'T> -> unit + +/// +/// The frozen sets of the tests of storage and order, as the cases of their rows. +/// +/// +/// The BCL lays out a frozen set by its size, its element type and its comparer, so the cases cover a small set and large +/// sets of several element types and comparers. A large set prints no readable row, so a case names the set, and +/// Run builds it. +/// +[] +type FrozenSetSample = + /// The elements of `Inputs.Several`, few enough for the BCL to scan them. + | Several + /// The integers from 1 to 100. + | Integers + /// The 64-bit integers from 1 to 100. + | Int64s + /// 100 words, enough for the BCL to hash them instead of scanning them. + | Words + /// The same 100 words, compared without case. + | CaselessWords + /// 100 struct pairs of a number and its text, compared with `HashIdentity.Structural`. + | Pairs + + /// Builds the frozen set of the case and runs a check on it. + member this.Run (check : IFrozenSetCheck) = + let words () = [| for index in 1..100 -> $"Word%d{index}" |] + + match this with + | FrozenSetSample.Several -> check.Check (FrozenSet.ofArray Inputs.Several) + | FrozenSetSample.Integers -> check.Check (FrozenSet.ofArray [| 1..100 |]) + | FrozenSetSample.Int64s -> check.Check (FrozenSet.ofArray [| 1L .. 100L |]) + | FrozenSetSample.Words -> check.Check (FrozenSet.ofArray (words ())) + | FrozenSetSample.CaselessWords -> + check.Check ( + (FrozenSet.forComparer (StringComparer.OrdinalIgnoreCase :> IEqualityComparer)).ofArray(words ()) + ) + | FrozenSetSample.Pairs -> + check.Check ( + (FrozenSet.forComparer HashIdentity.Structural).ofArray [| for index in 1..100 -> struct (index, string index) |] + ) + +/// +/// The functions that visit the elements of a frozen set, as the cases of rows. +/// +[] +type FrozenSetVisit = + | Fold + | FoldBack + | Iter + | ToArray + | ToFlatList + | ToList + | ToSeq + + /// The name of the function in the module, for the messages of the assertions. + member this.Name = + match this with + | FrozenSetVisit.Fold -> nameof FrozenSet.fold + | FrozenSetVisit.FoldBack -> nameof FrozenSet.foldBack + | FrozenSetVisit.Iter -> nameof FrozenSet.iter + | FrozenSetVisit.ToArray -> nameof FrozenSet.toArray + | FrozenSetVisit.ToFlatList -> nameof FrozenSet.toFlatList + | FrozenSetVisit.ToList -> nameof FrozenSet.toList + | FrozenSetVisit.ToSeq -> nameof FrozenSet.toSeq + + /// The elements of a frozen set in the order in which the function visits them. + member this.Apply (set : FrozenSet<'T>) : 'T array = + match this with + | FrozenSetVisit.Fold -> FrozenSet.fold (fun visited item -> Array.append visited [| item |]) [||] set + // foldBack visits the elements from the last one, so it puts each element before the ones that it visited + | FrozenSetVisit.FoldBack -> FrozenSet.foldBack (fun item visited -> Array.append [| item |] visited) set [||] + | FrozenSetVisit.Iter -> + let visited = ResizeArray<'T>() + FrozenSet.iter visited.Add set + visited.ToArray () + | FrozenSetVisit.ToArray -> FrozenSet.toArray set + | FrozenSetVisit.ToFlatList -> Seq.toArray (FrozenSet.toFlatList set) + // The function under test returns a list + | FrozenSetVisit.ToList -> List.toArray (FrozenSet.toList set) + | FrozenSetVisit.ToSeq -> Seq.toArray (FrozenSet.toSeq set) + +/// +/// A relation of a frozen set with a sequence, as the table of its tests holds it: the strings that the test builds the +/// frozen set and the sequence from, the member of the class that takes the sequence, and the function of the module that +/// takes a frozen set. +/// +type private SequenceRelation = { + /// The names of the strings of the frozen set, in the pool of the tests. + SetItems : string array + /// The names of the strings of the sequence, in the pool of the tests. + SequenceItems : string array + /// The member of the class, which takes the sequence. + ClassMember : FrozenSet -> seq -> bool + /// The function of the module, which takes a frozen set. + ModuleFunction : FrozenSet -> FrozenSet -> bool +} + +/// +/// The behaviour of FrozenSet that the shared contracts do not cover: comparers, the sequence forms of the +/// relations, the struct of forComparer, shared storage, the order of large frozen sets, null inputs and the module +/// next to the class System.Collections.Frozen.FrozenSet. +/// +/// +/// The tables of functions are maps by name, and the names are the rows of their tests: MSTest names a row after its +/// values, and neither a function nor a frozen set prints a readable value. +/// +[] +type FrozenSetTests () = + + static let ignoreCase : IEqualityComparer = StringComparer.OrdinalIgnoreCase + + static let defaultComparer : IEqualityComparer = EqualityComparer.Default + + /// Creates frozen sets that compare without case. + static let caseless = FrozenSet.forComparer ignoreCase + + /// Words that compare without case, so that a value can equal an element that differs from it. + static let words () = caseless.ofArray [| "apple"; "Banana"; "cherry" |] + + /// + /// Adds a value to a frozen set, which has no add: union keeps the comparer of the frozen set, and the + /// element of the frozen set where the value equals it in that comparer. + /// + static let add (value : string) (set : FrozenSet) = FrozenSet.union set (FrozenSet.singleton value) + + /// The elements of a frozen set, sorted in the order of F# generic comparison. + static let sorted (set : FrozenSet<'T>) = FrozenSet.toArray set |> Array.sort + + static let isLong (word : string) = word.Length > 5 + + static let byLength (word : string) = if isLong word then Choice1Of2 word else Choice2Of2 word + + static let nullSet = Unchecked.defaultof> + + static let nullComparer = Unchecked.defaultof> + + /// The creation functions without a comparer, by name. + static let defaultCreations : Map FrozenSet> = + Map.ofArray [| + nameof FrozenSet.empty, (fun () -> FrozenSet.empty) + nameof FrozenSet.ofArray, (fun () -> FrozenSet.ofArray [| "a" |]) + // The function under test takes a list + nameof FrozenSet.ofList, (fun () -> FrozenSet.ofList [ "a" ]) + nameof FrozenSet.ofReadOnlySpan, (fun () -> FrozenSet.ofReadOnlySpan (ReadOnlySpan [| "a" |])) + nameof FrozenSet.ofSeq, (fun () -> FrozenSet.ofSeq (Seq.singleton "a")) + nameof FrozenSet.ofSpan, (fun () -> FrozenSet.ofSpan (Span [| "a" |])) + nameof FrozenSet.singleton, (fun () -> FrozenSet.singleton "a") + |] + + /// The members of the struct, by name, each with the frozen set that it creates from "apple" and "APPLE". + static let structCreations : Map FrozenSet> = + Map.ofArray [| + nameof caseless.empty, (fun () -> caseless.empty |> add "apple" |> add "APPLE") + nameof caseless.ofArray, (fun () -> caseless.ofArray [| "apple"; "APPLE" |]) + // The member under test takes a list + nameof caseless.ofList, (fun () -> caseless.ofList [ "apple"; "APPLE" ]) + nameof caseless.ofReadOnlySpan, (fun () -> caseless.ofReadOnlySpan (ReadOnlySpan [| "apple"; "APPLE" |])) + nameof caseless.ofSeq, (fun () -> caseless.ofSeq (Seq.ofArray [| "apple"; "APPLE" |])) + nameof caseless.ofSpan, (fun () -> caseless.ofSpan (Span [| "apple"; "APPLE" |])) + nameof caseless.singleton, (fun () -> caseless.singleton "apple" |> add "APPLE") + |] + + /// The functions that select elements, by name, applied to the words. + static let selections : Map -> FrozenSet> = + Map.ofArray [| + nameof FrozenSet.filter, FrozenSet.filter isLong + ($"%s{nameof FrozenSet.partition} (accepted)", + fun set -> + let struct (accepted, _) = FrozenSet.partition isLong set + accepted) + ($"%s{nameof FrozenSet.partition} (rejected)", + fun set -> + let struct (_, rejected) = FrozenSet.partition isLong set + rejected) + |] + + /// The set algebra, by name, with the elements that it returns for the words and the frozen set of "APPLE" and "date". + static let algebra : Map -> FrozenSet -> FrozenSet) * string array)> = + Map.ofArray [| + nameof FrozenSet.difference, struct (FrozenSet.difference, [| "Banana"; "cherry" |]) + // Intersect keeps the elements of the second set (ADR 0001, D14) + nameof FrozenSet.intersect, struct (FrozenSet.intersect, [| "APPLE" |]) + nameof FrozenSet.symmetricDifference, struct (FrozenSet.symmetricDifference, [| "Banana"; "cherry"; "date" |]) + nameof FrozenSet.union, struct (FrozenSet.union, [| "Banana"; "apple"; "cherry"; "date" |]) + |] + + /// The transformations to another element type, by name, applied to the words. + static let typeChanges : Map -> FrozenSet> = + Map.ofArray [| + nameof FrozenSet.map, FrozenSet.map id + nameof FrozenSet.choose, FrozenSet.choose ValueSome + ($"%s{nameof FrozenSet.partitionWith} (first)", + fun set -> + let struct (first, _) = FrozenSet.partitionWith byLength set + first) + ($"%s{nameof FrozenSet.partitionWith} (second)", + fun set -> + let struct (_, second) = FrozenSet.partitionWith byLength set + second) + |] + + /// The functions that take a frozen set, by name and parameter, with the name of the parameter and a call with null. + static let nullRejections : Map unit))> = + let set = FrozenSet.ofArray Inputs.Several + + Map.ofArray [| + nameof FrozenSet.choose, struct ("set", (fun () -> FrozenSet.choose ValueSome nullSet |> ignore)) + nameof FrozenSet.comparer, struct ("set", (fun () -> FrozenSet.comparer nullSet |> ignore)) + nameof FrozenSet.contains, struct ("set", (fun () -> FrozenSet.contains 1 nullSet |> ignore)) + nameof FrozenSet.count, struct ("set", (fun () -> FrozenSet.count nullSet |> ignore)) + $"%s{nameof FrozenSet.difference} (set1)", struct ("set1", (fun () -> FrozenSet.difference nullSet set |> ignore)) + $"%s{nameof FrozenSet.difference} (set2)", struct ("set2", (fun () -> FrozenSet.difference set nullSet |> ignore)) + nameof FrozenSet.exists, struct ("set", (fun () -> FrozenSet.exists ((=) 1) nullSet |> ignore)) + nameof FrozenSet.filter, struct ("set", (fun () -> FrozenSet.filter ((=) 1) nullSet |> ignore)) + nameof FrozenSet.fold, struct ("set", (fun () -> FrozenSet.fold (+) 0 nullSet |> ignore)) + nameof FrozenSet.foldBack, struct ("set", (fun () -> FrozenSet.foldBack (+) nullSet 0 |> ignore)) + nameof FrozenSet.forall, struct ("set", (fun () -> FrozenSet.forall ((=) 1) nullSet |> ignore)) + $"%s{nameof FrozenSet.intersect} (set1)", struct ("set1", (fun () -> FrozenSet.intersect nullSet set |> ignore)) + $"%s{nameof FrozenSet.intersect} (set2)", struct ("set2", (fun () -> FrozenSet.intersect set nullSet |> ignore)) + $"%s{nameof FrozenSet.intersectMany} (null first)", + struct ("sets", (fun () -> FrozenSet.intersectMany [| nullSet; set |] |> ignore)) + $"%s{nameof FrozenSet.intersectMany} (null second)", + struct ("sets", (fun () -> FrozenSet.intersectMany [| set; nullSet |] |> ignore)) + nameof FrozenSet.isEmpty, struct ("set", (fun () -> FrozenSet.isEmpty nullSet |> ignore)) + $"%s{nameof FrozenSet.isProperSubset} (set1)", + struct ("set1", (fun () -> FrozenSet.isProperSubset nullSet set |> ignore)) + $"%s{nameof FrozenSet.isProperSubset} (set2)", + struct ("set2", (fun () -> FrozenSet.isProperSubset set nullSet |> ignore)) + $"%s{nameof FrozenSet.isProperSuperset} (set1)", + struct ("set1", (fun () -> FrozenSet.isProperSuperset nullSet set |> ignore)) + $"%s{nameof FrozenSet.isProperSuperset} (set2)", + struct ("set2", (fun () -> FrozenSet.isProperSuperset set nullSet |> ignore)) + $"%s{nameof FrozenSet.isSubset} (set1)", struct ("set1", (fun () -> FrozenSet.isSubset nullSet set |> ignore)) + $"%s{nameof FrozenSet.isSubset} (set2)", struct ("set2", (fun () -> FrozenSet.isSubset set nullSet |> ignore)) + $"%s{nameof FrozenSet.isSuperset} (set1)", struct ("set1", (fun () -> FrozenSet.isSuperset nullSet set |> ignore)) + $"%s{nameof FrozenSet.isSuperset} (set2)", struct ("set2", (fun () -> FrozenSet.isSuperset set nullSet |> ignore)) + nameof FrozenSet.iter, struct ("set", (fun () -> FrozenSet.iter ignore nullSet)) + nameof FrozenSet.map, struct ("set", (fun () -> FrozenSet.map id nullSet |> ignore)) + $"%s{nameof FrozenSet.overlaps} (set1)", struct ("set1", (fun () -> FrozenSet.overlaps nullSet set |> ignore)) + $"%s{nameof FrozenSet.overlaps} (set2)", struct ("set2", (fun () -> FrozenSet.overlaps set nullSet |> ignore)) + nameof FrozenSet.partition, struct ("set", (fun () -> FrozenSet.partition ((=) 1) nullSet |> ignore)) + nameof FrozenSet.partitionWith, + struct ("set", + (fun () -> + FrozenSet.partitionWith (fun (item : int) -> Choice.Choice1Of2 item) nullSet + |> ignore + )) + $"%s{nameof FrozenSet.setEquals} (set1)", struct ("set1", (fun () -> FrozenSet.setEquals nullSet set |> ignore)) + $"%s{nameof FrozenSet.setEquals} (set2)", struct ("set2", (fun () -> FrozenSet.setEquals set nullSet |> ignore)) + $"%s{nameof FrozenSet.symmetricDifference} (set1)", + struct ("set1", (fun () -> FrozenSet.symmetricDifference nullSet set |> ignore)) + $"%s{nameof FrozenSet.symmetricDifference} (set2)", + struct ("set2", (fun () -> FrozenSet.symmetricDifference set nullSet |> ignore)) + nameof FrozenSet.toArray, struct ("set", (fun () -> FrozenSet.toArray nullSet |> ignore)) + nameof FrozenSet.toFlatList, struct ("set", (fun () -> FrozenSet.toFlatList nullSet |> ignore)) + nameof FrozenSet.toList, struct ("set", (fun () -> FrozenSet.toList nullSet |> ignore)) + nameof FrozenSet.toSeq, struct ("set", (fun () -> FrozenSet.toSeq nullSet |> ignore)) + nameof FrozenSet.tryGetValue, struct ("set", (fun () -> FrozenSet.tryGetValue 1 nullSet |> ignore)) + $"%s{nameof FrozenSet.union} (set1)", struct ("set1", (fun () -> FrozenSet.union nullSet set |> ignore)) + $"%s{nameof FrozenSet.union} (set2)", struct ("set2", (fun () -> FrozenSet.union set nullSet |> ignore)) + $"%s{nameof FrozenSet.unionMany} (null first)", + struct ("sets", (fun () -> FrozenSet.unionMany [| nullSet; set |] |> ignore)) + $"%s{nameof FrozenSet.unionMany} (null second)", + struct ("sets", (fun () -> FrozenSet.unionMany [| set; nullSet |] |> ignore)) + |] + + /// + /// The functions that take a sequence, by name and by the input that they reject, with the parameter that names the + /// input and the function on it. + /// + /// + /// A default flat list converts to a sequence without a check and fails when the sequence is enumerated, so the other + /// inputs are valid and the function would enumerate the sequence (ADR 0001, D6). + /// + static let sequenceRejections : Map unit))> = + let ints = FrozenSet.forComparer (EqualityComparer.Default :> IEqualityComparer) + let objects = FrozenSet.forComparer (EqualityComparer.Default :> IEqualityComparer) + let nullElements = Unchecked.defaultof> + let defaultElements = Unchecked.defaultof> :> seq + let nullSets = Unchecked.defaultof>> + let defaultSets = Unchecked.defaultof>> :> seq> + + // The flat list of strings is a sequence of objects, which a type test for the flat list of objects does not see + let widenedElements = FlatListOracles.defaultStringsAsObjects + + Map.ofArray [| + $"%s{nameof FrozenSet.intersectMany} (null sets)", + struct ("sets", (fun () -> FrozenSet.intersectMany nullSets |> ignore)) + $"%s{nameof FrozenSet.intersectMany} (default flat list)", + struct ("sets", (fun () -> FrozenSet.intersectMany defaultSets |> ignore)) + $"%s{nameof FrozenSet.ofSeq} (null elements)", struct ("elements", (fun () -> FrozenSet.ofSeq nullElements |> ignore)) + $"%s{nameof FrozenSet.ofSeq} (default flat list)", + struct ("elements", (fun () -> FrozenSet.ofSeq defaultElements |> ignore)) + $"%s{nameof FrozenSet.ofSeq} (default flat list of strings as objects)", + struct ("elements", (fun () -> FrozenSet.ofSeq widenedElements |> ignore)) + $"%s{nameof FrozenSet.unionMany} (null sets)", struct ("sets", (fun () -> FrozenSet.unionMany nullSets |> ignore)) + $"%s{nameof FrozenSet.unionMany} (default flat list)", + struct ("sets", (fun () -> FrozenSet.unionMany defaultSets |> ignore)) + // The struct of forComparer + $"forComparer %s{nameof ints.ofSeq} (null elements)", + struct ("elements", (fun () -> ints.ofSeq nullElements |> ignore)) + $"forComparer %s{nameof ints.ofSeq} (default flat list)", + struct ("elements", (fun () -> ints.ofSeq defaultElements |> ignore)) + $"forComparer %s{nameof ints.ofSeq} (default flat list of strings as objects)", + struct ("elements", (fun () -> objects.ofSeq widenedElements |> ignore)) + |] + + /// + /// The strings of the tests of the sequence forms of the relations, by name: "a1" and "a2" have the text "a" and are + /// different instances, "A" differs from them in case, and "b" has other text. + /// + static let pool : Map = + Map.ofArray [| "a1", String [| 'a' |]; "a2", String [| 'a' |]; "A", "A"; "b", "b" |] + + /// The strings of the pool for their names. + static let strings (names : string array) = names |> Array.map (fun name -> pool[name]) + + /// + /// The comparers of the first frozen set in the tests of the sequence forms, by name, each with whether it finds equal + /// every pair of strings that the default comparer finds equal. + /// + static let sequenceComparers : Map * bool)> = + Map.ofArray [| + "default comparer", struct (defaultComparer, true) + "comparer that ignores case", struct (ignoreCase, true) + // It tells "a1" from "a2", which the default comparer finds equal + "reference comparer", struct (HashIdentity.Reference, false) + |] + + /// + /// The relations of a frozen set with a sequence, by name. + /// + /// + /// The strings tell the comparers apart: when the first frozen set compares by reference, each relation answers + /// differently for the sequence that the default comparer converts, which keeps one of "a1" and "a2", than for the + /// sequence that the comparer of the first frozen set converts. + /// + static let sequenceRelations : Map = + Map.ofArray [| + nameof FrozenSet.isProperSubset, + { + SetItems = [| "a1"; "a2" |] + SequenceItems = [| "a1"; "a2"; "b" |] + ClassMember = fun set elements -> set.IsProperSubsetOf elements + ModuleFunction = FrozenSet.isProperSubset + } + nameof FrozenSet.isProperSuperset, + { + SetItems = [| "a1"; "a2"; "A" |] + SequenceItems = [| "a1"; "a2"; "A" |] + ClassMember = fun set elements -> set.IsProperSupersetOf elements + ModuleFunction = FrozenSet.isProperSuperset + } + nameof FrozenSet.isSubset, + { + SetItems = [| "a1"; "a2"; "A" |] + SequenceItems = [| "a1"; "a2"; "A" |] + ClassMember = fun set elements -> set.IsSubsetOf elements + ModuleFunction = FrozenSet.isSubset + } + nameof FrozenSet.isSuperset, + { + SetItems = [| "a1" |] + SequenceItems = [| "a1"; "a2" |] + ClassMember = fun set elements -> set.IsSupersetOf elements + ModuleFunction = FrozenSet.isSuperset + } + nameof FrozenSet.overlaps, + { + SetItems = [| "a1" |] + SequenceItems = [| "a2"; "a1" |] + ClassMember = fun set elements -> set.Overlaps elements + ModuleFunction = FrozenSet.overlaps + } + nameof FrozenSet.setEquals, + { + SetItems = [| "a1"; "a2"; "A" |] + SequenceItems = [| "a1"; "a2"; "A" |] + ClassMember = fun set elements -> set.SetEquals elements + ModuleFunction = FrozenSet.setEquals + } + |] + + /// The relations that need no conversion, by name, as the changelog gives them: each tests the elements with contains. + static let streamingRecipes : Map -> seq -> bool> = + Map.ofArray [| + nameof FrozenSet.isSuperset, (fun set elements -> Seq.forall (fun item -> FrozenSet.contains item set) elements) + nameof FrozenSet.overlaps, (fun set elements -> Seq.exists (fun item -> FrozenSet.contains item set) elements) + |] + + /// + /// The frozen sets of 3, 1 and 2, by the way that builds them: the module and the class, each with + /// System.Collections.Frozen opened before and after this library. + /// + static let resolutions : Map FrozenSet> = + Map.ofArray [| + "the module", (fun () -> FrozenSet.ofArray [| 3; 1; 2 |]) + "the class", (fun () -> FrozenSet.ToFrozenSet [| 3; 1; 2 |]) + "the module with System.Collections.Frozen opened last", FrozenOpenedLast.fromModule + "the class with System.Collections.Frozen opened last", FrozenOpenedLast.fromClass + |] + + /// Asserts that toFlatList returns the items of a frozen set without a copy and in the order of enumeration. + static let sharesItems (set : FrozenSet<'T>) = + let flatList = FrozenSet.toFlatList set + + Assert.AreSame ( + ImmutableCollectionsMarshal.AsArray set.Items, + ImmutableCollectionsMarshal.AsArray flatList, + "FrozenSet.toFlatList must return the items without a copy" + ) + + CollectionAssert.AreEqual ( + Seq.toArray set, + Seq.toArray flatList, + "FrozenSet.toFlatList must keep the order of enumeration" + ) + + /// The names of the creation functions without a comparer, as rows. + static member DefaultCreations = defaultCreations.Keys + + /// The names of the members of the struct that create frozen sets, as rows. + static member StructCreations = structCreations.Keys + + /// The names of the functions that select elements, as rows. + static member Selections = selections.Keys + + /// The names of the set algebra, as rows. + static member Algebra = algebra.Keys + + /// The names of the transformations to another element type, as rows. + static member TypeChanges = typeChanges.Keys + + /// The names of the functions that take a frozen set, with the parameter where a function takes two, as rows. + static member NullRejections = nullRejections.Keys + + /// The names of the functions that take a sequence, with the input that they reject, as rows. + static member SequenceRejections = sequenceRejections.Keys + + /// The relations of a frozen set with a sequence, each with every comparer of the first frozen set, as rows. + static member SequenceRelationRows = seq { + for relation in sequenceRelations.Keys do + for comparer in sequenceComparers.Keys do + struct (relation, comparer) + } + + /// The relations that need no conversion, each with every comparer of the first frozen set, as rows. + static member StreamingRows = seq { + for relation in streamingRecipes.Keys do + for comparer in sequenceComparers.Keys do + struct (relation, comparer) + } + + /// The ways that build a frozen set through the module or the class, as rows. + static member Resolutions = resolutions.Keys + + /// The frozen sets whose items toFlatList returns: a small one and two large ones, as rows. + static member SharingSamples = [| FrozenSetSample.Several; FrozenSetSample.Integers; FrozenSetSample.CaselessWords |] + + /// + /// The large frozen sets with every function that visits the elements, as rows: the contracts run on small sets, and + /// the BCL stores large ones in hash tables of different layouts. + /// + static member Visits = seq { + for sample in + [| + FrozenSetSample.Integers + FrozenSetSample.Int64s + FrozenSetSample.Words + FrozenSetSample.CaselessWords + FrozenSetSample.Pairs + |] do + for visit in + [| + FrozenSetVisit.Fold + FrozenSetVisit.FoldBack + FrozenSetVisit.Iter + FrozenSetVisit.ToArray + FrozenSetVisit.ToFlatList + FrozenSetVisit.ToList + FrozenSetVisit.ToSeq + |] do + struct (sample, visit) + } + + // ---- Comparers ---- + + [] + member _.``creation without a comparer compares with the default comparer`` (name : string) = + Assert.AreSame ( + defaultComparer, + FrozenSet.comparer (defaultCreations[name]()), + $"FrozenSet.%s{name} must compare with EqualityComparer.Default" + ) + + [] + member _.``forComparer creates the struct with the comparer`` () = + Assert.AreSame (ignoreCase, (FrozenSet.forComparer ignoreCase).Comparer, "FrozenSet.forComparer must keep the comparer") + + Assert.AreSame ( + ignoreCase, + (CustomComparerFrozenSet ignoreCase).Comparer, + "The constructor of CustomComparerFrozenSet must keep the comparer" + ) + + [] + member _.``the members of the struct create frozen sets that compare with its comparer`` (name : string) = + let set = structCreations[name]() + Assert.AreSame (ignoreCase, FrozenSet.comparer set, $"CustomComparerFrozenSet.%s{name} must keep the comparer") + + Assert.AreEqual( + "apple", + Assert.ContainsSingle (set, $"CustomComparerFrozenSet.%s{name} must compare without case"), + $"CustomComparerFrozenSet.%s{name} must keep the first of the elements that the comparer considers equal" + ) + + // The shared contract states that one element of each class stays. Which one stays is a property of the BCL member that + // creates the set, so each module pins it, here in both orders of the input. + [] + member _.``the span members of the struct keep the first of the elements that its comparer considers equal`` + (name : string, first : string, second : string) + = + let input = [| first; second |] + + let set = + match name with + | "ofReadOnlySpan" -> caseless.ofReadOnlySpan (ReadOnlySpan input) + | "ofSpan" -> caseless.ofSpan (Span input) + | name -> failwith $"CustomComparerFrozenSet has no span member %s{name}" + + Assert.AreEqual( + first, + Assert.ContainsSingle (set, $"CustomComparerFrozenSet.%s{name} must compare without case"), + $"CustomComparerFrozenSet.%s{name} must keep the first of the elements that the comparer considers equal" + ) + + [] + member _.``the span functions keep the first of the elements that are equal in the default comparer`` (name : string) = + // Two strings that are equal and are two instances, so the instance tells which one stays + let first = String ('x', 3) + let second = String ('x', 3) + + let create (input : string array) = + match name with + | "ofReadOnlySpan" -> FrozenSet.ofReadOnlySpan (ReadOnlySpan input) + | "ofSpan" -> FrozenSet.ofSpan (Span input) + | name -> failwith $"FrozenSet has no span function %s{name}" + + Assert.AreSame ( + first, + Assert.ContainsSingle (create [| first; second |], $"FrozenSet.%s{name} must hold equal elements once"), + $"FrozenSet.%s{name} must keep the first of the elements that are equal" + ) + + Assert.AreSame ( + second, + Assert.ContainsSingle (create [| second; first |], $"FrozenSet.%s{name} must hold equal elements once"), + $"FrozenSet.%s{name} must keep the first of the elements that are equal in either order" + ) + + [] + member _.``the struct gives an existing frozen set its comparer`` () = + let set = FrozenSet.ofArray [| "apple"; "APPLE"; "Banana" |] + let ignoringCase = caseless.ofSeq set + + Assert.AreSame ( + ignoreCase, + FrozenSet.comparer ignoringCase, + "CustomComparerFrozenSet.ofSeq must give the frozen set the comparer" + ) + + Assert.HasCount ( + 2, + ignoringCase, + "CustomComparerFrozenSet.ofSeq must merge the elements that the comparer considers equal" + ) + + Assert.HasCount (3, set, "CustomComparerFrozenSet.ofSeq must not change the frozen set") + + [] + member _.``the default value of the struct compares with the default comparer`` () = + let defaultValue = Unchecked.defaultof> + let set = defaultValue.ofArray [| "apple"; "APPLE" |] + + Assert.AreSame ( + defaultComparer, + defaultValue.Comparer, + "The default CustomComparerFrozenSet must return the default comparer" + ) + + Assert.AreSame ( + defaultComparer, + FrozenSet.comparer set, + "The default CustomComparerFrozenSet must create frozen sets with it" + ) + + Assert.HasCount (2, set, "The default CustomComparerFrozenSet must tell the case apart") + + Assert.AreSame ( + FrozenSet.Empty, + defaultValue.empty, + "The default CustomComparerFrozenSet must return the empty frozen set of the class" + ) + + [] + member _.``filter and partition keep the comparer`` (name : string) = + Assert.AreSame (ignoreCase, FrozenSet.comparer (selections[name](words ())), $"FrozenSet.%s{name} must keep the comparer") + + [] + member _.``partition keeps comparing without case`` () = + let struct (accepted, _) = FrozenSet.partition isLong (words ()) + Assert.IsTrue (FrozenSet.contains "BANANA" accepted, "FrozenSet.partition must keep comparing without case") + + [] + member _.``set algebra keeps the comparer of the first set and compares with it`` (name : string) = + let struct (combine, expected) = algebra[name] + let result = combine (words ()) (FrozenSet.ofArray [| "APPLE"; "date" |]) + + Assert.AreSame (ignoreCase, FrozenSet.comparer result, $"FrozenSet.%s{name} must keep the comparer of the first set") + CollectionAssert.AreEqual (expected, sorted result, $"FrozenSet.%s{name} must compare with the comparer of the first set") + + [] + member _.``intersect compares with the comparer of the first set when it is the default one`` () = + let intersection = FrozenSet.intersect (FrozenSet.ofArray [| "APPLE"; "date" |]) (words ()) + + Assert.AreSame ( + defaultComparer, + FrozenSet.comparer intersection, + "FrozenSet.intersect must keep the comparer of the first set" + ) + + Assert.IsEmpty (intersection, "FrozenSet.intersect must tell APPLE from apple in the default comparer of the first set") + + [] + member _.``isProperSuperset counts the elements that the comparer of the first set finds equal once`` () = + // The second set tells "apple" and "APPLE" apart; for the first set they are one element, and it holds "Banana" + // besides. FrozenSet answers with set semantics already, as the documentation of IsProperSupersetOf says (ADR 0001, + // D14) + Assert.IsTrue ( + FrozenSet.isProperSuperset (caseless.ofArray [| "apple"; "Banana" |]) (FrozenSet.ofArray [| "apple"; "APPLE" |]), + "FrozenSet.isProperSuperset must count the elements that the comparer of the first set finds equal once" + ) + + [] + member _.``the predicates of two sets compare with the comparer of the first set`` () = + let apple = caseless.ofArray [| "APPLE" |] + let set = words () + + Assert.IsTrue (FrozenSet.isSubset apple set, "FrozenSet.isSubset must compare without case") + Assert.IsTrue (FrozenSet.isProperSubset apple set, "FrozenSet.isProperSubset must compare without case") + Assert.IsTrue (FrozenSet.isSuperset set apple, "FrozenSet.isSuperset must compare without case") + Assert.IsTrue (FrozenSet.isProperSuperset set apple, "FrozenSet.isProperSuperset must compare without case") + Assert.IsTrue (FrozenSet.overlaps apple set, "FrozenSet.overlaps must compare without case") + + Assert.IsTrue ( + FrozenSet.setEquals set (FrozenSet.ofArray [| "APPLE"; "BANANA"; "CHERRY" |]), + "FrozenSet.setEquals must compare without case" + ) + + Assert.IsFalse ( + FrozenSet.isSubset (FrozenSet.ofArray [| "APPLE" |]) set, + "FrozenSet.isSubset must compare with the default comparer of the first set" + ) + + // The way to pass a sequence to a relation, as the changelog gives it: convert the sequence with the comparer of + // the first set. The default comparer gives the same answer when the comparer of the first set finds equal what the + // default comparer finds equal, and another answer when it tells such strings apart, as the reference comparer does + [] + member _.``a relation with a sequence takes the sequence that the comparer of the first set converts`` + (relation : string, comparer : string) + = + Assert.AreNotSame (pool["a1"], pool["a2"], "The test needs two strings with the same text that are different instances") + + let relationOfSequence = sequenceRelations[relation] + let struct (firstComparer, defaultFindsEqualWhatItFindsEqual) = sequenceComparers[comparer] + let set = (FrozenSet.forComparer firstComparer).ofArray(strings relationOfSequence.SetItems) + let elements = strings relationOfSequence.SequenceItems + let expected = relationOfSequence.ClassMember set elements + + Assert.AreEqual( + expected, + relationOfSequence.ModuleFunction set ((FrozenSet.forComparer (FrozenSet.comparer set)).ofSeq elements), + $"FrozenSet.%s{relation} must answer as the member of the class for the sequence that the comparer of the first set converts" + ) + + let withDefaultComparer = relationOfSequence.ModuleFunction set (FrozenSet.ofSeq elements) + + if defaultFindsEqualWhatItFindsEqual then + Assert.AreEqual( + expected, + withDefaultComparer, + $"FrozenSet.%s{relation} must answer as the member of the class for the sequence that the default comparer converts, because the comparer of the first set finds equal what the default comparer finds equal" + ) + else + Assert.AreNotEqual( + expected, + withDefaultComparer, + $"FrozenSet.%s{relation} must answer differently for the sequence that the default comparer converts, because the comparer of the first set tells apart strings that the default comparer finds equal" + ) + + [] + member _.``overlaps and isSuperset with a sequence test each element with contains`` (relation : string, comparer : string) = + let relationOfSequence = sequenceRelations[relation] + let struct (firstComparer, _) = sequenceComparers[comparer] + let set = (FrozenSet.forComparer firstComparer).ofArray(strings relationOfSequence.SetItems) + let elements = strings relationOfSequence.SequenceItems + + Assert.AreEqual( + relationOfSequence.ClassMember set elements, + streamingRecipes[relation] set elements, + $"%s{relation} with a sequence must test each element with FrozenSet.contains, in the comparer of the first set" + ) + + [] + member _.``intersectMany and unionMany keep the comparer of the first set`` () = + let sets = [| words (); FrozenSet.ofArray [| "APPLE"; "date" |] |] + let intersection = FrozenSet.intersectMany sets + let union = FrozenSet.unionMany sets + + Assert.AreSame ( + ignoreCase, + FrozenSet.comparer intersection, + "FrozenSet.intersectMany must keep the comparer of the first set" + ) + + Assert.AreSame (ignoreCase, FrozenSet.comparer union, "FrozenSet.unionMany must keep the comparer of the first set") + + CollectionAssert.AreEqual ( + [| "APPLE" |], + sorted intersection, + "FrozenSet.intersectMany must compare without case and hold the element of the last set" + ) + + CollectionAssert.AreEqual ( + [| "Banana"; "apple"; "cherry"; "date" |], + sorted union, + "FrozenSet.unionMany must compare without case" + ) + + [] + member _.``unionMany of no sets is empty with the default comparer`` () = + let union = FrozenSet.unionMany Seq.empty> + Assert.IsEmpty (union, "FrozenSet.unionMany of no sets must be empty") + + Assert.AreSame ( + defaultComparer, + FrozenSet.comparer union, + "FrozenSet.unionMany of no sets must compare with the default comparer" + ) + + [] + member _.``map, choose and partitionWith compare with the default comparer of the result`` (name : string) = + Assert.AreSame ( + defaultComparer, + FrozenSet.comparer (typeChanges[name](words ())), + $"FrozenSet.%s{name} must compare with the default comparer" + ) + + [] + member _.``map does not keep the comparer of its input`` () = + // The default comparer tells the case apart, although the element type is the same + Assert.IsFalse ( + FrozenSet.contains "APPLE" (FrozenSet.map id (words ())), + "FrozenSet.map must not keep the comparer of its input" + ) + + // ---- Sharing and order ---- + + [] + member _.``empty is the empty frozen set of the class`` () = + Assert.AreSame (FrozenSet.Empty, FrozenSet.empty, "FrozenSet.empty must be the empty frozen set of the class") + + [] + member _.``ofSeq of a frozen set with the default comparer returns it`` () = + let set = FrozenSet.ofArray Inputs.Several + Assert.AreSame (set, FrozenSet.ofSeq set, "FrozenSet.ofSeq of a frozen set with the default comparer must not copy it") + + let copy = FrozenSet.ofSeq (words ()) + Assert.AreSame (defaultComparer, FrozenSet.comparer copy, "FrozenSet.ofSeq must compare with the default comparer") + Assert.HasCount (3, copy, "FrozenSet.ofSeq must keep the elements of a frozen set with another comparer") + + [] + member _.``the struct returns a frozen set that has its comparer`` () = + let set = words () + + Assert.AreSame ( + set, + caseless.ofSeq set, + "CustomComparerFrozenSet.ofSeq of a frozen set with the comparer must not copy it" + ) + + // ToFrozenSet returns a frozen set whose comparer Equals the comparer, not only the same instance (ADR 0001, D14) + [] + member _.``the struct returns a frozen set whose comparer equals its comparer`` () = + let first : IEqualityComparer = StringComparer.Create (CultureInfo.InvariantCulture, true) + let second : IEqualityComparer = StringComparer.Create (CultureInfo.InvariantCulture, true) + Assert.AreNotSame (first, second, "The test needs two comparer instances") + Assert.AreEqual(first, second, "The test needs two comparers that are equal") + + let set = (FrozenSet.forComparer first).ofArray [| "apple"; "Banana" |] + + Assert.AreSame ( + set, + (FrozenSet.forComparer second).ofSeq set, + "CustomComparerFrozenSet.ofSeq must return a frozen set whose comparer equals its comparer" + ) + + [] + member _.``intersectMany of one frozen set returns it`` () = + let set = words () + Assert.AreSame (set, FrozenSet.intersectMany [| set |], "FrozenSet.intersectMany of one frozen set must return it") + + [] + member _.``toFlatList returns the items of the frozen set without a copy`` (sample : FrozenSetSample) = + sample.Run + { + new IFrozenSetCheck with + member _.Check set = sharesItems set + } + + [] + member _.``the functions visit the elements of large sets in the order of enumeration`` + (sample : FrozenSetSample, visit : FrozenSetVisit) + = + sample.Run + { + new IFrozenSetCheck with + member _.Check set = + CollectionAssert.AreEqual ( + Seq.toArray set, + visit.Apply set, + $"FrozenSet.%s{visit.Name} must visit the elements in the order of enumeration" + ) + } + + // ---- Access and search ---- + + [] + member _.``tryGetValue returns the element that the frozen set holds`` () = + let stored = String ('x', 3) + let set = caseless.ofArray [| stored; "apple" |] + + match FrozenSet.tryGetValue "XXX" set with + | ValueSome found -> Assert.AreSame (stored, found, "FrozenSet.tryGetValue must return the instance of the frozen set") + | ValueNone -> Assert.Fail "FrozenSet.tryGetValue must find the element that equals the value in the comparer" + + Assert.AreEqual( + ValueSome "apple", + FrozenSet.tryGetValue "APPLE" set, + "FrozenSet.tryGetValue must compare without case" + ) + + Assert.AreEqual( + ValueNone, + FrozenSet.tryGetValue "date" set, + "FrozenSet.tryGetValue must return nothing for a missing value" + ) + + [] + member _.``contains compares with the comparer of the frozen set`` () = + Assert.IsTrue (FrozenSet.contains "BANANA" (words ()), "FrozenSet.contains must compare without case") + + Assert.IsFalse ( + FrozenSet.contains "BANANA" (FrozenSet.ofArray [| "Banana" |]), + "FrozenSet.contains must compare with the default comparer" + ) + + // ---- Null inputs ---- + + [] + member _.``functions reject a null frozen set`` (name : string) = + let struct (paramName, actual) = nullRejections[name] + + let ex = + Assert.ThrowsExactly(Action actual, $"FrozenSet.%s{name} must reject a null frozen set") + + Assert.AreEqual(paramName, ex.ParamName, $"FrozenSet.%s{name} must name the parameter of the frozen set") + + [] + member _.``functions that take a sequence reject a null sequence and a default flat list given as the sequence`` + (name : string) + = + let struct (paramName, actual) = sequenceRejections[name] + FlatListAssert.RejectsNull ($"FrozenSet.%s{name}", paramName, actual) + + [] + member _.``a flat list that is not default passes as a sequence of objects and as a sequence of frozen sets`` () = + let widened = FlatListOracles.stringsAsObjects (FlatList.ofArray [| "a"; "b" |]) + let sets = FlatList.ofArray [| FrozenSet.ofArray [| 1; 2 |]; FrozenSet.ofArray [| 2; 3 |] |] + + CollectionAssert.AreEquivalent ( + [| ("a" :> obj); ("b" :> obj) |], + FrozenSet.toArray (FrozenSet.ofSeq widened), + "FrozenSet.ofSeq must take the elements of a flat list of strings as objects" + ) + + CollectionAssert.AreEquivalent ( + [| ("a" :> obj); ("b" :> obj) |], + FrozenSet.toArray ((FrozenSet.forComparer (EqualityComparer.Default :> IEqualityComparer)).ofSeq widened), + "CustomComparerFrozenSet.ofSeq must take the elements of a flat list of strings as objects" + ) + + CollectionAssert.AreEquivalent ( + [| 1; 2; 3 |], + FrozenSet.toArray (FrozenSet.unionMany sets), + "FrozenSet.unionMany must take the frozen sets of a flat list" + ) + + CollectionAssert.AreEquivalent ( + [| 2 |], + FrozenSet.toArray (FrozenSet.intersectMany sets), + "FrozenSet.intersectMany must take the frozen sets of a flat list" + ) + + // forComparer calls the constructor of the struct, which checks the comparer, so both name its parameter (D12) + [] + member _.``forComparer rejects a null comparer`` () = + let ex = + Assert.ThrowsExactly( + Action (fun () -> FrozenSet.forComparer nullComparer |> ignore), + "FrozenSet.forComparer must reject a null comparer" + ) + + Assert.AreEqual("comparer", ex.ParamName, "FrozenSet.forComparer must name the parameter comparer") + + [] + member _.``the constructor of the struct rejects a null comparer`` () = + let ex = + Assert.ThrowsExactly( + Action (fun () -> CustomComparerFrozenSet nullComparer |> ignore), + "The constructor of CustomComparerFrozenSet must reject a null comparer" + ) + + Assert.AreEqual( + "comparer", + ex.ParamName, + "The constructor of CustomComparerFrozenSet must name the parameter comparer" + ) + + // ---- Name resolution ---- + + [] + member _.``the module and the class FrozenSet resolve side by side`` (name : string) = + // This file opens System.Collections.Frozen before this library, and FrozenOpenedLast opens it after this library. + // FrozenSet.ofArray compiles only against the module and FrozenSet.ToFrozenSet only against the class. + CollectionAssert.AreEqual ( + [| 1; 2; 3 |], + sorted (resolutions[name]()), + $"FrozenSet must build the frozen set through %s{name}" + ) + +#if NET10_0_OR_GREATER +/// +/// The alternate lookups of FrozenSet, which exist on net10.0 only (ADR 0001, D11). +/// +/// +/// The frozen sets of 64-bit integers whose comparer also compares 32-bit integers are rows of their sizes: the BCL scans +/// a small one and hashes a large one. The frozen sets whose comparers compare no 32-bit integers are rows of the names of +/// the comparers. +/// +[] +type FrozenSetAlternateLookupTests () = + + static let nullSet = Unchecked.defaultof> + + /// The frozen set of the 64-bit integers from 1 to a count, whose comparer also compares 32-bit integers. + static let numbers (count : int) = FrozenSetOps.ofInt64WithInt32Comparer [| 1L .. int64 count |] + + /// The frozen sets of 64-bit integers whose comparers compare no 32-bit integers, by the name of the comparer. + static let withoutAlternate : Map FrozenSet> = + Map.ofArray [| + "HashIdentity.Structural", (fun () -> (FrozenSet.forComparer HashIdentity.Structural).ofArray [| 1L; 2L; 3L |]) + "EqualityComparer.Default", (fun () -> FrozenSet.ofArray [| 1L; 2L; 3L |]) + |] + + /// The alternate lookups, by name, each with a call with a null frozen set. + static let nullRejections : Map unit> = + Map.ofArray [| + nameof FrozenSet.alternateLookup, (fun () -> FrozenSet.alternateLookup nullSet |> ignore) + nameof FrozenSet.tryAlternateLookup, (fun () -> FrozenSet.tryAlternateLookup nullSet |> ignore) + |] + + /// The names of the comparers that compare no 32-bit integers, as rows. + static member WithoutAlternate = withoutAlternate.Keys + + /// The names of the alternate lookups, as rows. + static member NullRejections = nullRejections.Keys + + [] + member _.``alternateLookup finds the elements by a value of the alternate type`` (count : int) = + let set = numbers count + let lookup = FrozenSet.alternateLookup set + Assert.AreSame (set, lookup.Set, "FrozenSet.alternateLookup must look up in the frozen set") + Assert.IsTrue (lookup.Contains 2, "FrozenSet.alternateLookup must find 2") + Assert.IsFalse (lookup.Contains 1000, "FrozenSet.alternateLookup must not find 1000") + + match lookup.TryGetValue 3 with + | true, actual -> Assert.AreEqual(3L, actual, "FrozenSet.alternateLookup must return the element") + | false, _ -> Assert.Fail "FrozenSet.alternateLookup must find 3" + + [] + member _.``alternateLookup throws for a comparer without the alternate type`` (comparer : string) = + let set = withoutAlternate[comparer]() + + Assert.ThrowsExactly( + Action (fun () -> FrozenSet.alternateLookup set |> ignore), + "FrozenSet.alternateLookup must throw for a comparer that does not compare the alternate type" + ) + |> ignore + + [] + member _.``tryAlternateLookup returns the lookup of a comparer with the alternate type`` (count : int) = + let set = numbers count + + match FrozenSet.tryAlternateLookup set with + | ValueSome lookup -> + Assert.AreSame (set, lookup.Set, "FrozenSet.tryAlternateLookup must look up in the frozen set") + Assert.IsTrue (lookup.Contains 2, "FrozenSet.tryAlternateLookup must return a lookup that finds 2") + | ValueNone -> Assert.Fail "FrozenSet.tryAlternateLookup must return the lookup of a comparer with the alternate type" + + [] + member _.``tryAlternateLookup returns nothing for a comparer without the alternate type`` (comparer : string) = + Assert.IsTrue ( + (FrozenSet.tryAlternateLookup(withoutAlternate[comparer]())).IsNone, + "FrozenSet.tryAlternateLookup must return ValueNone for a comparer that does not compare the alternate type" + ) + + [] + member _.``a span alternate takes the method of the class`` () = + // An F# generic function cannot be instantiated with ReadOnlySpan, so the module cannot wrap this lookup + let ordinal : IEqualityComparer = StringComparer.Ordinal + let words = (FrozenSet.forComparer ordinal).ofArray [| "apple"; "Banana" |] + let lookup = words.GetAlternateLookup>() + Assert.IsTrue (lookup.Contains ("apple".AsSpan()), "The alternate lookup of the class must find a span") + Assert.IsFalse (lookup.Contains ("APPLE".AsSpan()), "The alternate lookup of the class must compare ordinally") + + [] + member _.``the alternate lookups reject a null frozen set`` (name : string) = + let ex = + Assert.ThrowsExactly( + Action (nullRejections[name]), + $"FrozenSet.%s{name} must reject a null frozen set" + ) + + Assert.AreEqual("set", ex.ParamName, $"FrozenSet.%s{name} must name the parameter set") +#endif diff --git a/tests/FSharp.Collections.Immutable.Tests/FrozenSet/FrozenSetTupleTests.fs b/tests/FSharp.Collections.Immutable.Tests/FrozenSet/FrozenSetTupleTests.fs new file mode 100644 index 00000000..bce47f5a --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/FrozenSet/FrozenSetTupleTests.fs @@ -0,0 +1,62 @@ +namespace FSharp.Collections.Immutable.Tests + +open System.Collections.Frozen +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 `FrozenSet` module of the `Tuple` namespace. +[] +type FrozenSetTupleTests () = + + static let numbers = FrozenSet.ofArray Inputs.Several + + static let expected = Set.ofArray Inputs.Several + + [] + member _.``partition returns a reference tuple`` () = + let (accepted : FrozenSet), (rejected : FrozenSet) = + FrozenSet.partition (fun item -> item > 0) numbers + let expectedAccepted, expectedRejected = Set.partition (fun item -> item > 0) expected + + CollectionAssert.AreEqual ( + Set.toArray expectedAccepted, + FrozenSet.toArray accepted |> Array.sort, + "Tuple.FrozenSet.partition must return the accepted elements" + ) + + CollectionAssert.AreEqual ( + Set.toArray expectedRejected, + FrozenSet.toArray rejected |> Array.sort, + "Tuple.FrozenSet.partition must return the rejected elements" + ) + + [] + member _.``partitionWith returns a reference tuple`` () = + let partitioner (item : int) = + if item > 0 then + Choice1Of2 item + else + Choice2Of2 (string item) + + let (first : FrozenSet), (second : FrozenSet) = FrozenSet.partitionWith partitioner numbers + let expectedFirst, expectedSecond = Set.partitionWith partitioner expected + + CollectionAssert.AreEqual ( + Set.toArray expectedFirst, + FrozenSet.toArray first |> Array.sort, + "Tuple.FrozenSet.partitionWith must return the first choices" + ) + + CollectionAssert.AreEqual ( + Set.toArray expectedSecond, + FrozenSet.toArray second |> Array.sort, + "Tuple.FrozenSet.partitionWith must return the second choices" + ) + + [] + member _.``the functions without a tuple keep resolving to the default module`` () = + let found : int voption = FrozenSet.tryGetValue 8 numbers + Assert.AreEqual(ValueSome 8, found, "FrozenSet.tryGetValue must resolve to the default module")