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
56 changes: 56 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,60 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
semantics and exceptions. Its `tryFind`, `tryFindIndex` and `tryPick` return `voption`, `choose` and `tryPick` take
functions that return `voption`, and `groupBy`, `partition`, `partitionWith` and `zip` return struct tuples; the
`Parallel` modules of the `Option` and `Tuple` namespaces repeat them with options and reference tuples.
* `HashSet` has every function of the FSharp.Core `Set` module that applies to a hash set, with its semantics, argument
order and exceptions, plus `addRange`, `build`, `builder`, `choose`, `clear`, `comparer`, `forComparer`, `ofBuilder`,
`ofReadOnlySpan`, `ofSpan`, `overlaps`, `removeRange`, `setEquals`, `symmetricDifference`, `toBuilder`, `tryGetValue`
and `update`.
* `HashSet.addRange elements set` and `removeRange elements set` wrap the `Union` and `Except` members that take a
sequence. The result keeps the comparer of the hash set, an element of the sequence that the comparer finds equal to
an element of the hash set does not replace it, and the input itself comes back when nothing changes.
* `HashSet.ofReadOnlySpan` and `ofSpan` create a hash set from a span, `build` fills a builder with an action, and
`update` applies an action to a builder of a hash set and returns its contents, so that the in-place operations of the
builder, such as `IntersectWith`, are one expression.
* `HashSet.forComparer comparer` creates the `CustomComparerHashSet` struct, whose members `build`, `builder`, `empty`,
`ofArray`, `ofList`, `ofReadOnlySpan`, `ofSeq`, `ofSpan` and `singleton` create hash sets and builders that compare
with the comparer, as in `(HashSet.forComparer StringComparer.OrdinalIgnoreCase).ofSeq names`. They replace the BCL
members `Create(comparer, ...)`, `CreateRange(comparer, items)`, `ToImmutableHashSet(items, comparer)` and
`CreateBuilder(comparer)`. `ofSeq` also gives an existing hash set the comparer, as `WithComparer` does: the hash set
itself comes back for the same comparer instance, and an equal but distinct instance rebuilds it.
`WithComparer(null)` is `HashSet.ofSeq set`. A `null` comparer throws `ArgumentNullException`.
* `HashSet.add` and `remove` return the input itself when nothing changes, so a reference comparison of the input and
the result tells whether `Add` changed anything, as the `bool` result of `Builder.Add` does. Hash sets have no builder
module: `builder`, `toBuilder` and `ofBuilder` are `CreateBuilder`, `ToBuilder` and `ToImmutable`, and the builder is
used directly. The mutating members of the BCL interfaces, such as `ISet<T>.UnionWith` and `ICollection<T>.Add`,
throw `NotSupportedException` on a hash set and have no function.
* `add`, `addRange`, `remove`, `removeRange`, `clear`, `update`, `filter` and `partition` of `HashSet` keep the comparer
of their input, and the set algebra keeps the comparer of the first hash set; `map`, `choose` and `partitionWith`
compare with the default comparer of the result type. Every function throws `ArgumentNullException` for a `null` hash
set, sequence or array, and for a default flat list that is given as a sequence; `yield!` and `for` of
`hashSet { }` do the same. `unionMany` of an empty sequence returns an empty hash set, as `Set.unionMany` does.
`tryGetValue` returns a `voption`, `choose` takes a function that returns a `voption`, and `partition` and
`partitionWith` return struct tuples.
* The set algebra and the relations of `HashSet` take hash sets, as the `Set` functions do. The BCL members that take a
sequence map as follows. `Union` and `Except` are `addRange` and `removeRange`, and `Overlaps` and `IsSupersetOf` are
`Seq.exists` and `Seq.forall` with `HashSet.contains`, which compare in the comparer of the hash set for every
comparer. To pass a sequence to `Intersect`, `SymmetricExcept`, `IsSubsetOf`, `IsProperSubsetOf`,
`IsProperSupersetOf` or `SetEquals`, convert it with `(HashSet.forComparer (HashSet.comparer set)).ofSeq elements`:
the elements get the comparer of the first hash set, as they do in the members. `HashSet.ofSeq elements` converts
them with the default comparer instead, which merges the elements that it finds equal before the function 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.
* The element that stays when the comparer finds two elements equal follows the BCL: `union` and `addRange` keep the
element of the first hash set, `intersect` keeps the elements of the second one, and the creation functions keep the
first element of their input. `isProperSuperset` is the exception: it counts the elements of the second hash set that
the comparer of the first one finds equal once, as the documentation of `IsProperSupersetOf` says, where the member
counts each of them.
* The `ISet` module works on any `IImmutableSet<'T>` with the functions of `HashSet` that need neither a creation nor a
comparer, `addRange` and `removeRange` included; the transformations that keep the element type return the
implementation of their input. The interface cannot tell its comparer, so a sequence for a relation, `intersect` or
`symmetricDifference` is converted with `ISet.addRange elements (ISet.clear set)`, which has the implementation and
the comparer of `set`, where `HashSet` uses `(HashSet.forComparer (HashSet.comparer set)).ofSeq elements`.
* The `FSharp.Collections.Immutable.Option.HashSet`, `FSharp.Collections.Immutable.Tuple.HashSet`,
`FSharp.Collections.Immutable.Option.ISet` and `FSharp.Collections.Immutable.Tuple.ISet` modules repeat `choose` and
`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`.

### Changed

Expand Down Expand Up @@ -61,6 +115,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
compared in F# structural equality. The removed functions did not remove the elements that satisfy a predicate: use
`filter` with the negated predicate for that.
* `FlatList.removeRange`: use `removeManyAt`.
* The `ISet<'T>` abbreviation of `IImmutableSet<'T>`, because it shadowed `System.Collections.Generic.ISet<'T>`: use
`IImmutableSet<'T>` and the `ISet` module.

## [1.0.0] - 2020-02-23

Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ Please also join the [F# Open Source Group](http://fsharp.github.com)
| `Queue` | `ImmutableQueue<'T>` | – |
| `HashMap` | `ImmutableDictionary<'Key, 'Value>` | – |
| `SortedMap` | `ImmutableSortedDictionary<'Key, 'Value>` | – |
| `HashSet` | `ImmutableHashSet<'T>` | – |
| `HashSet` | `ImmutableHashSet<'T>` | `hashSet { }` |
| `SortedSet` | `ImmutableSortedSet<'T>` | – |
| `ISet` | `IImmutableSet<'T>` | – |
| `IIndexedSeq` | `IReadOnlyList<'T>` | – |

[Documentation](https://fsprojects.github.io/FSharp.Collections.Immutable/)
Expand Down
3 changes: 2 additions & 1 deletion docsSrc/Explanations/Background.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ While F# has its own built-in immutable collections, System.Collections.Immutabl
| `Queue` | `ImmutableQueue<'T>` | – |
| `HashMap` | `ImmutableDictionary<'Key, 'Value>` | – |
| `SortedMap` | `ImmutableSortedDictionary<'Key, 'Value>` | – |
| `HashSet` | `ImmutableHashSet<'T>` | – |
| `HashSet` | `ImmutableHashSet<'T>` | `hashSet { }` |
| `SortedSet` | `ImmutableSortedSet<'T>` | – |
| `ISet` | `IImmutableSet<'T>` | – |
| `IIndexedSeq` | `IReadOnlyList<'T>` | – |

---
Expand Down
3 changes: 2 additions & 1 deletion docsSrc/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ F# bindings for [System.Collections.Immutable](https://learn.microsoft.com/en-us
| `Queue` | `ImmutableQueue<'T>` | – |
| `HashMap` | `ImmutableDictionary<'Key, 'Value>` | – |
| `SortedMap` | `ImmutableSortedDictionary<'Key, 'Value>` | – |
| `HashSet` | `ImmutableHashSet<'T>` | – |
| `HashSet` | `ImmutableHashSet<'T>` | `hashSet { }` |
| `SortedSet` | `ImmutableSortedSet<'T>` | – |
| `ISet` | `IImmutableSet<'T>` | – |
| `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 @@ -4,3 +4,6 @@ module FSharp.Collections.Immutable.ComputationExpressions
// ---- Creation ----

let flatList = FlatListCE ()

[<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 @@ -19,3 +19,30 @@ module FSharp.Collections.Immutable.ComputationExpressions
/// Evaluates to a flat list of 0, 1, 4, 9, 100 and 200.
/// </example>
val flatList : FlatListCE

/// <summary>
/// Builds a hash 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 hash sets of every element type. A
/// <see cref="T:FSharp.Collections.Immutable.HashSetCE`1"/> created with an equality comparer builds hash sets that
/// compare with it.
/// </remarks>
/// <example id="hashSet-1">
/// <code lang="fsharp">
/// let squares = hashSet {
/// for i in -2..2 do
/// yield i * i
/// yield! [ 4; 9 ]
/// }
/// </code>
/// Evaluates to a hash set of 0, 1, 4 and 9.
/// </example>
/// <example id="hashSet-2">
/// <code lang="fsharp">
/// let names = HashSetCE StringComparer.OrdinalIgnoreCase { "Ann"; "ANN"; "Bob" }
/// </code>
/// Evaluates to a hash set of "Ann" and "Bob" that ignores case.
/// </example>
[<GeneralizableValue>]
val hashSet<'T> : HashSetCE<'T>
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
<Compile Include="Helper\ArrayChecks.fs" />
<Compile Include="Helper\Arrays.fs" />
<Compile Include="Helper\ParallelArrays.fs" />
<Compile Include="Helper\Sets.fs" />
<Compile Include="Helper\HashSets.fs" />
<Compile Include="FlatList\FlatListTypes.fs" />
<Compile Include="FlatList\FlatList.fsi" />
<Compile Include="FlatList\FlatList.fs" />
Expand All @@ -44,6 +46,27 @@
<Compile Include="FlatList\FlatListBuilder.fs" />
<Compile Include="FlatList\FlatListCE.fsi" />
<Compile Include="FlatList\FlatListCE.fs" />
<Compile Include="HashSet\HashSetTypes.fs" />
<Compile Include="HashSet\HashSet.CustomComparer.fsi">
<DependentUpon>HashSet.fsi</DependentUpon>
</Compile>
<Compile Include="HashSet\HashSet.CustomComparer.fs">
<DependentUpon>HashSet.fs</DependentUpon>
</Compile>
<Compile Include="HashSet\HashSet.fsi" />
<Compile Include="HashSet\HashSet.fs" />
<Compile Include="HashSet\HashSetOption.fsi" />
<Compile Include="HashSet\HashSetOption.fs" />
<Compile Include="HashSet\HashSetTuple.fsi" />
<Compile Include="HashSet\HashSetTuple.fs" />
<Compile Include="HashSet\ISet.fsi" />
<Compile Include="HashSet\ISet.fs" />
<Compile Include="HashSet\ISetOption.fsi" />
<Compile Include="HashSet\ISetOption.fs" />
<Compile Include="HashSet\ISetTuple.fsi" />
<Compile Include="HashSet\ISetTuple.fs" />
<Compile Include="HashSet\HashSetCE.fsi" />
<Compile Include="HashSet\HashSetCE.fs" />
<Compile Include="Stack.fs" />
<Compile Include="ImmutableList.fs" />
<Compile Include="Queue.fs" />
Expand Down
78 changes: 78 additions & 0 deletions src/FSharp.Collections.Immutable/HashSet/HashSet.CustomComparer.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
namespace FSharp.Collections.Immutable

open System
open System.Collections.Generic
open System.Collections.Immutable
// Opened after System.Collections.Generic, so that HashSet names the abbreviation of this library, not the mutable set
open FSharp.Collections.Immutable

// 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 CustomComparerHashSet<'T> =

val private comparer : IEqualityComparer<'T> | null

new (comparer : IEqualityComparer<'T>)
=
checkNotNull (nameof comparer) comparer
{ comparer = comparer }

// ---- Creation ----

[<CompiledName "Empty">]
member this.empty : HashSet<'T> = ImmutableHashSet.Create<'T> this.Comparer

[<CompiledName "OfArray">]
member this.ofArray (array : 'T array) : HashSet<'T> =
checkNotNull (nameof array) array
ImmutableHashSet.CreateRange<'T>(this.Comparer, array)

[<CompiledName "OfList">]
member this.ofList (elements : 'T list) : HashSet<'T> =
// Set.ofList dereferences a null list without a check; a null input throws ArgumentNullException here (D6)
checkNotNull (nameof elements) elements
let builder = ImmutableHashSet.CreateBuilder<'T> this.Comparer

for item in elements do
builder.Add item |> ignore

builder.ToImmutable ()

// The elements of the span are added in order, so the first of the elements that the comparer finds equal stays
[<CompiledName "OfReadOnlySpan">]
member this.ofReadOnlySpan (span : ReadOnlySpan<'T>) : HashSet<'T> = ImmutableHashSet.Create<'T>(this.Comparer, span)

[<CompiledName "OfSeq">]
member this.ofSeq (elements : seq<'T>) : HashSet<'T> =
// A default flat list given as a sequence would fail with InvalidOperationException in the BCL call (ADR 0001, D6)
checkNotNullOrDefaultSeq (nameof elements) elements
// A hash set gets the comparer through WithComparer, which returns the hash set itself for the same comparer
// instance (the test is reference equality, ADR 0001, D14)
ImmutableHashSet.CreateRange<'T>(this.Comparer, elements)

// Create takes a read-only span; F# converts the span of the argument
[<CompiledName "OfSpan">]
member this.ofSpan (span : Span<'T>) : HashSet<'T> = ImmutableHashSet.Create<'T>(this.Comparer, span)

[<CompiledName "Singleton">]
member this.singleton (value : 'T) : HashSet<'T> = ImmutableHashSet.Create<'T>(this.Comparer, value)

// ---- Builder ----

[<CompiledName "Build">]
member this.build (action : HashSet<'T>.Builder -> unit) : HashSet<'T> =
let builder = ImmutableHashSet.CreateBuilder<'T> this.Comparer
action builder
builder.ToImmutable ()

[<CompiledName "CreateBuilder">]
member this.builder () : HashSet<'T>.Builder = ImmutableHashSet.CreateBuilder<'T> this.Comparer

// ---- Properties ----

member this.Comparer : IEqualityComparer<'T> =
match this.comparer with
| null -> EqualityComparer<'T>.Default :> IEqualityComparer<'T>
| comparer -> comparer
Loading
Loading