Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>.UnionWith` and `ICollection<T>.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<int, _> set`, and a lookup by a
`ReadOnlySpan<char>` 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

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/)
Expand Down
1 change: 1 addition & 0 deletions docsSrc/Explanations/Background.md
Original file line number Diff line number Diff line change
Expand Up @@ -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>` | – |

---
Expand Down
1 change: 1 addition & 0 deletions docsSrc/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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>` | – |

---
Expand Down
3 changes: 3 additions & 0 deletions src/FSharp.Collections.Immutable/ComputationExpressions.fs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,8 @@ module FSharp.Collections.Immutable.ComputationExpressions

let flatList = FlatListCE ()

[<GeneralizableValue>]
let frozenSet<'T> = FrozenSetCE<'T>()

[<GeneralizableValue>]
let hashSet<'T> = HashSetCE<'T>()
27 changes: 27 additions & 0 deletions src/FSharp.Collections.Immutable/ComputationExpressions.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,33 @@ module FSharp.Collections.Immutable.ComputationExpressions
/// </example>
val flatList : FlatListCE

/// <summary>
/// Builds a frozen set that compares with <see cref="P:System.Collections.Generic.EqualityComparer`1.Default"/>.
/// </summary>
/// <remarks>
/// The value is a type function, so that one value builds frozen sets of every element type. A
/// <see cref="T:FSharp.Collections.Immutable.FrozenSetCE`1"/> 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.
/// </remarks>
/// <example id="frozenSet-1">
/// <code lang="fsharp">
/// let squares = frozenSet {
/// for i in -2..2 do
/// yield i * i
/// yield! [ 4; 9 ]
/// }
/// </code>
/// Evaluates to a frozen set of 0, 1, 4 and 9.
/// </example>
/// <example id="frozenSet-2">
/// <code lang="fsharp">
/// let names = FrozenSetCE StringComparer.OrdinalIgnoreCase { "Ann"; "ANN"; "Bob" }
/// </code>
/// Evaluates to a frozen set of "Ann" and "Bob" that ignores case.
/// </example>
[<GeneralizableValue>]
val frozenSet<'T> : FrozenSetCE<'T>

/// <summary>
/// Builds a hash set that compares with <see cref="P:System.Collections.Generic.EqualityComparer`1.Default"/>.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,20 @@
<Compile Include="HashSet\ISetTuple.fs" />
<Compile Include="HashSet\HashSetCE.fsi" />
<Compile Include="HashSet\HashSetCE.fs" />
<Compile Include="FrozenSet\FrozenSet.CustomComparer.fsi">
<DependentUpon>FrozenSet.fsi</DependentUpon>
</Compile>
<Compile Include="FrozenSet\FrozenSet.CustomComparer.fs">
<DependentUpon>FrozenSet.fs</DependentUpon>
</Compile>
<Compile Include="FrozenSet\FrozenSet.fsi" />
<Compile Include="FrozenSet\FrozenSet.fs" />
<Compile Include="FrozenSet\FrozenSetOption.fsi" />
<Compile Include="FrozenSet\FrozenSetOption.fs" />
<Compile Include="FrozenSet\FrozenSetTuple.fsi" />
<Compile Include="FrozenSet\FrozenSetTuple.fs" />
<Compile Include="FrozenSet\FrozenSetCE.fsi" />
<Compile Include="FrozenSet\FrozenSetCE.fs" />
<Compile Include="Stack.fs" />
<Compile Include="ImmutableList.fs" />
<Compile Include="Queue.fs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
[<Struct; NoComparison; NoEquality>]
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
[<CompiledName "Empty">]
member this.empty : FrozenSet<'T> = FrozenSet.Create<'T>(this.Comparer, ReadOnlySpan<'T>.Empty)

[<CompiledName "OfArray">]
member this.ofArray (array : 'T array) : FrozenSet<'T> =
checkNotNull (nameof array) array
array.ToFrozenSet this.Comparer

[<CompiledName "OfList">]
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)
[<CompiledName "OfReadOnlySpan">]
member this.ofReadOnlySpan (span : ReadOnlySpan<'T>) : FrozenSet<'T> = FrozenSet.Create<'T>(this.Comparer, span)

[<CompiledName "OfSeq">]
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
[<CompiledName "OfSpan">]
member this.ofSpan (span : Span<'T>) : FrozenSet<'T> = FrozenSet.Create<'T>(this.Comparer, span)

[<CompiledName "Singleton">]
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
namespace FSharp.Collections.Immutable

open System
open System.Collections.Frozen
open System.Collections.Generic

/// <summary>
/// Creates frozen sets that compare their elements with an equality comparer (ADR 0001, D12).
/// </summary>
/// <remarks>
/// <para>
/// <see cref="M:FSharp.Collections.Immutable.FrozenSet.ForComparer``1(System.Collections.Generic.IEqualityComparer{``0})"/>
/// creates the struct. Its members are the creation functions of <see cref="T:FSharp.Collections.Immutable.FrozenSet"/>
/// 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 <c>builder</c> member.
/// </para>
/// <para>
/// The struct holds only the comparer, so creating it allocates nothing. Its default value, which no constructor
/// creates, compares with <see cref="P:System.Collections.Generic.EqualityComparer`1.Default"/>.
/// </para>
/// </remarks>
/// <example id="customComparerFrozenSet-1">
/// <code lang="fsharp">
/// let names = (FrozenSet.forComparer StringComparer.OrdinalIgnoreCase).ofList [ "Ann"; "ANN"; "Bob" ]
/// </code>
/// Evaluates to a frozen set of "Ann" and "Bob" that compares without case.
/// </example>
[<Struct; NoComparison; NoEquality>]
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

/// <summary>Creates the struct for an equality comparer.</summary>
/// <param name="comparer">The equality comparer of the elements of the frozen sets that the struct creates.</param>
/// <exception cref="T:System.ArgumentNullException">Thrown when <paramref name="comparer"/> is <see langword="null"/>.</exception>
new : comparer : IEqualityComparer<'T> -> CustomComparerFrozenSet<'T>

// ---- Creation ----

/// <summary>
/// The empty frozen set that compares with the comparer.
/// </summary>
/// <remarks>
/// O(1). The member wraps
/// <see cref="M:System.Collections.Frozen.FrozenSet.Create``1(System.Collections.Generic.IEqualityComparer{``0},System.ReadOnlySpan{``0})"/>,
/// which returns <see cref="P:System.Collections.Frozen.FrozenSet`1.Empty"/> for
/// <see cref="P:System.Collections.Generic.EqualityComparer`1.Default"/>.
/// </remarks>
[<CompiledName "Empty">]
member empty : FrozenSet<'T>

/// <summary>
/// Creates a frozen set from the elements of an array, compared with the comparer.
/// </summary>
/// <param name="array">The input array.</param>
/// <returns>
/// 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.
/// </returns>
/// <exception cref="T:System.ArgumentNullException">Thrown when <paramref name="array"/> is <see langword="null"/>.</exception>
/// <remarks>
/// O(n). Later changes to the array do not change the frozen set. The member wraps
/// <see cref="M:System.Collections.Frozen.FrozenSet.ToFrozenSet``1(System.Collections.Generic.IEnumerable{``0},System.Collections.Generic.IEqualityComparer{``0})"/>.
/// </remarks>
[<CompiledName "OfArray">]
member ofArray : array : 'T array -> FrozenSet<'T>

/// <summary>
/// Creates a frozen set from the elements of a list, compared with the comparer.
/// </summary>
/// <param name="elements">The input list.</param>
/// <returns>
/// 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.
/// </returns>
/// <exception cref="T:System.ArgumentNullException">Thrown when <paramref name="elements"/> is <see langword="null"/>.</exception>
/// <remarks>
/// O(n).
/// </remarks>
[<CompiledName "OfList">]
member ofList : elements : 'T list -> FrozenSet<'T>

/// <summary>
/// Creates a frozen set from the elements of a read-only span, compared with the comparer.
/// </summary>
/// <param name="span">The input span.</param>
/// <returns>
/// 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.
/// </returns>
/// <remarks>
/// O(n). Later changes to the memory under the span do not change the frozen set. The member wraps
/// <see cref="M:System.Collections.Frozen.FrozenSet.Create``1(System.Collections.Generic.IEqualityComparer{``0},System.ReadOnlySpan{``0})"/>.
/// </remarks>
[<CompiledName "OfReadOnlySpan">]
member ofReadOnlySpan : span : ReadOnlySpan<'T> -> FrozenSet<'T>

/// <summary>
/// Creates a frozen set from the elements of a sequence, compared with the comparer.
/// </summary>
/// <param name="elements">The input sequence, which can be a frozen set with another comparer.</param>
/// <returns>
/// 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.
/// </returns>
/// <exception cref="T:System.ArgumentNullException">Thrown when <paramref name="elements"/> is <see langword="null"/> or a default flat list.</exception>
/// <remarks>
/// <para>
/// O(n). The member wraps
/// <see cref="M:System.Collections.Frozen.FrozenSet.ToFrozenSet``1(System.Collections.Generic.IEnumerable{``0},System.Collections.Generic.IEqualityComparer{``0})"/>.
/// </para>
/// <para>
/// 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.
/// </para>
/// </remarks>
[<CompiledName "OfSeq">]
member ofSeq : elements : seq<'T> -> FrozenSet<'T>

/// <summary>
/// Creates a frozen set from the elements of a span, compared with the comparer.
/// </summary>
/// <param name="span">The input span.</param>
/// <returns>
/// 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.
/// </returns>
/// <remarks>
/// 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
/// <see cref="M:System.Collections.Frozen.FrozenSet.Create``1(System.Collections.Generic.IEqualityComparer{``0},System.ReadOnlySpan{``0})"/>.
/// </remarks>
[<CompiledName "OfSpan">]
member ofSpan : span : Span<'T> -> FrozenSet<'T>

/// <summary>
/// Creates a frozen set of one element that compares with the comparer.
/// </summary>
/// <param name="value">The element.</param>
/// <returns>The frozen set of one element.</returns>
/// <remarks>
/// O(1).
/// </remarks>
[<CompiledName "Singleton">]
member singleton : value : 'T -> FrozenSet<'T>

// ---- Properties ----

/// <summary>
/// The equality comparer of the elements of the frozen sets that the struct creates.
/// </summary>
/// <remarks>
/// O(1). The default value of the struct returns <see cref="P:System.Collections.Generic.EqualityComparer`1.Default"/>.
/// </remarks>
member Comparer : IEqualityComparer<'T>
Loading