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
50 changes: 50 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,56 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
modules that repeat the functions that use value options or return struct tuples with options and reference tuples.
* `hashMap { }` builds a hash map from `yield struct (key, value)` and `yield KeyValuePair (key, value)`; `HashMapCE` takes
a key comparer, and a `null` one throws `ArgumentNullException`.
* `FrozenMap` wraps `FrozenDictionary<'Key, 'Value>`, a map that is built once and read many times. It has the functions of
the FSharp.Core `Map` module that read a map or transform the whole map, with their semantics and exceptions, plus
`choose`, `containsValue` and `singleton`, and the wrappers `containsBinding`, `copyTo`, `keyComparer`, `ofReadOnlySpan`,
`ofSeqBy` and `ofSpan` of the members of `FrozenDictionary`, which keep the semantics of the BCL. It has no function that
adds or removes a binding: `choose`, `filter`, `map` and `partition` build a new frozen map with the key comparer of the
input. `keys` and `values` return flat lists over the arrays of the map, without a copy, and `FlatList.asSpan` views them
as spans.
* `FrozenMap.containsBinding` tells whether a key is bound to a value, as `Contains` of a `KeyValuePair` does. A frozen map
has no value comparer, so it compares the value with `EqualityComparer<'Value>.Default`, as `containsValue` does.
`tryFind` followed by `=` compares with F# equality, which differs for some types: `nan = nan` is `false`.
* `FrozenMap.ofReadOnlySpan` and `FrozenMap.ofSpan` create a frozen map from a `ReadOnlySpan<KeyValuePair<'Key, 'Value>>` or
a `Span<KeyValuePair<'Key, 'Value>>` with `FrozenDictionary.Create`, and the members `ofReadOnlySpan` and `ofSpan` of
`CustomComparersFrozenMap` do so with its key comparer. A repeated key never throws: the last binding wins and the first
key stays. A `Span` or an array converts to the parameter of `ofReadOnlySpan` only when the types of the call are known;
`ofSpan` takes a `Span` where they are not.
* `FrozenMap.copyTo` copies the bindings into an array of `KeyValuePair`s from an index, as `CopyTo` does, and throws what
it throws: `ArgumentNullException` for a `null` array, `ArgumentOutOfRangeException` for an index below zero or beyond
the length of the array, and `ArgumentException` when the array has no room for the bindings.
* `FrozenMap.ofArray`, `FrozenMap.ofList` and `FrozenMap.ofSeq` take the bindings as `KeyValuePair`s, reference tuples or
struct tuples: they are static members of `FrozenDictionary<'Key, 'Value>` in the auto-opened `FrozenMapExtensions`
module, one overload per shape, and every shape keeps the last binding of a repeated key, as `Map.ofSeq` does: the value
of the last binding, and the key of the first one when the comparer finds distinct keys equal, as `ToFrozenDictionary`
does. They take a dictionary, an F# `Map` or another frozen map too. C# does not see them as members of
`FrozenDictionary`; it calls the public static methods `FrozenMapExtensions.OfArray`, `OfList` and `OfSeq`, or the members
of the struct below. A reflection over the `FrozenMap` module does not list them.
* `FrozenMap.forKeyComparer` returns a `CustomComparersFrozenMap<'Key, 'Value>` struct whose members `empty`, `ofArray`,
`ofList`, `ofReadOnlySpan`, `ofSeq`, `ofSeqBy`, `ofSpan` and `singleton` create frozen maps with its key comparer:
`(FrozenMap.forKeyComparer StringComparer.OrdinalIgnoreCase).ofSeq pairs`. A `null` comparer throws
`ArgumentNullException`.
* `ofSeq` of that struct also gives an existing frozen map another key comparer:
`(FrozenMap.forKeyComparer comparer).ofSeq table`. A frozen map whose comparer equals the key comparer of the struct comes
back itself, as `ToFrozenDictionary` does. Any other frozen map is rebuilt, and keys that the new comparer makes equal
merge: the first key stays, with the value of the last binding.
* The single-key idioms of the BCL are functions of the module: `tryFind` is `TryGetValue`, and `tryFind` followed by
`ValueOption.defaultValue` is `GetValueOrDefault`. `ToFrozenDictionary` with an element selector has no function. To build
the same map, pass the elements mapped to `KeyValuePair`s to `FrozenMap.ofSeq`, which keeps the last binding of a repeated
key where the BCL rejects every repeated key; `FrozenMap.ofSeqBy` rejects them like the BCL, with the elements as the
values. `GetValueRefOrNullRef` and the indexer return a reference, which a function cannot: `find` and `tryFind` cover the
lookup.
* On `net10.0`, `FrozenMap.alternateLookup` and `FrozenMap.tryAlternateLookup` look the bindings up by keys of another type
through `FrozenDictionary.GetAlternateLookup` and `FrozenDictionary.TryGetAlternateLookup`; the type of the alternate
keys is their first type argument: `FrozenMap.alternateLookup<int, _, _> table`. The BCL members come with .NET 9 and the
package has no `net9.0` target, so the two functions are not on `net8.0`.
* The `FSharp.Collections.Immutable.Option` and `FSharp.Collections.Immutable.Tuple` namespaces have `FrozenMap` modules
that repeat the functions that use value options or return struct tuples with options and reference tuples.
* `frozenMap { }` builds a frozen map from `yield struct (key, value)` and `yield KeyValuePair (key, value)`; `FrozenMapCE`
takes a key comparer, and a `null` one throws `ArgumentNullException`.
* `FrozenMap` keeps the convention of `FrozenDictionary` where it differs from `Map`: a later binding of a key replaces the
value of an earlier one and leaves its key, `ofSeqBy` rejects every repeated key, and `ofSeq` of a frozen map whose
comparer equals the requested one returns that map.

### Changed

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Please also join the [F# Open Source Group](http://fsharp.github.com)
| `HashMap` | `ImmutableDictionary<'Key, 'Value>` | `hashMap { }` |
| `IMap` | `IImmutableDictionary<'Key, 'Value>` | – |
| `SortedMap` | `ImmutableSortedDictionary<'Key, 'Value>` | – |
| `FrozenMap` | `FrozenDictionary<'Key, 'Value>` | `frozenMap { }` |
| `HashSet` | `ImmutableHashSet<'T>` | – |
| `SortedSet` | `ImmutableSortedSet<'T>` | – |
| `IIndexedSeq` | `IReadOnlyList<'T>` | – |
Expand Down
1 change: 1 addition & 0 deletions docsSrc/Explanations/Background.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ While F# has its own built-in immutable collections, System.Collections.Immutabl
| `HashMap` | `ImmutableDictionary<'Key, 'Value>` | `hashMap { }` |
| `IMap` | `IImmutableDictionary<'Key, 'Value>` | – |
| `SortedMap` | `ImmutableSortedDictionary<'Key, 'Value>` | – |
| `FrozenMap` | `FrozenDictionary<'Key, 'Value>` | `frozenMap { }` |
| `HashSet` | `ImmutableHashSet<'T>` | – |
| `SortedSet` | `ImmutableSortedSet<'T>` | – |
| `IIndexedSeq` | `IReadOnlyList<'T>` | – |
Expand Down
1 change: 1 addition & 0 deletions docsSrc/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ F# bindings for [System.Collections.Immutable](https://learn.microsoft.com/en-us
| `HashMap` | `ImmutableDictionary<'Key, 'Value>` | `hashMap { }` |
| `IMap` | `IImmutableDictionary<'Key, 'Value>` | – |
| `SortedMap` | `ImmutableSortedDictionary<'Key, 'Value>` | – |
| `FrozenMap` | `FrozenDictionary<'Key, 'Value>` | `frozenMap { }` |
| `HashSet` | `ImmutableHashSet<'T>` | – |
| `SortedSet` | `ImmutableSortedSet<'T>` | – |
| `IIndexedSeq` | `IReadOnlyList<'T>` | – |
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 frozenMap<'Key when 'Key : not null> = FrozenMapCE<'Key>()

[<GeneralizableValue>]
let hashMap<'Key when 'Key : not null> = HashMapCE<'Key>()
23 changes: 23 additions & 0 deletions src/FSharp.Collections.Immutable/ComputationExpressions.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,29 @@ module FSharp.Collections.Immutable.ComputationExpressions
/// </example>
val flatList : FlatListCE

/// <summary>
/// Builds a frozen map with the default key comparer.
/// </summary>
/// <remarks>
/// A later binding of a key replaces the value of an earlier one, and the key of the earlier binding stays, as in
/// <see cref="M:System.Collections.Frozen.FrozenDictionary.ToFrozenDictionary``2(System.Collections.Generic.IEnumerable{System.Collections.Generic.KeyValuePair{``0,``1}},System.Collections.Generic.IEqualityComparer{``0})"/>
/// (ADR 0001, D14). The expression freezes its bindings once, when it runs. The instance is generic in the type of the
/// keys, which each expression infers; <see cref="T:FSharp.Collections.Immutable.FrozenMapCE`1"/> takes a key comparer.
/// </remarks>
/// <example id="frozenMap-1">
/// <code lang="fsharp">
/// let squares = frozenMap {
/// for i in 1..3 do
/// yield struct (i, i * i)
/// yield KeyValuePair (10, 100)
/// yield struct (1, -1)
/// }
/// </code>
/// Evaluates to a frozen map of 1 to -1, 2 to 4, 3 to 9 and 10 to 100.
/// </example>
[<GeneralizableValue>]
val frozenMap<'Key when 'Key : not null> : FrozenMapCE<'Key>

/// <summary>
/// Builds a hash map with the default comparers.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<PropertyGroup Label="NuGet">
<PackageId>FSharp.Collections.Immutable</PackageId>
<Title>FSharp.Collections.Immutable</Title>
<Description>F# bindings for System.Collections.Immutable</Description>
<Description>F# bindings for System.Collections.Immutable and System.Collections.Frozen</Description>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)'=='Release'">
Expand Down Expand Up @@ -72,6 +72,27 @@
<Compile Include="HashMap\IMapTuple.fs" />
<Compile Include="HashMap\HashMapCE.fsi" />
<Compile Include="HashMap\HashMapCE.fs" />
<Compile Include="FrozenMap\FrozenMapTypes.fs" />
<Compile Include="FrozenMap\FrozenMap.CustomComparers.fsi">
<DependentUpon>FrozenMap.fsi</DependentUpon>
</Compile>
<Compile Include="FrozenMap\FrozenMap.CustomComparers.fs">
<DependentUpon>FrozenMap.fs</DependentUpon>
</Compile>
<Compile Include="FrozenMap\FrozenMap.fsi" />
<Compile Include="FrozenMap\FrozenMap.fs" />
<Compile Include="FrozenMap\FrozenMapOption.fsi" />
<Compile Include="FrozenMap\FrozenMapOption.fs" />
<Compile Include="FrozenMap\FrozenMapTuple.fsi" />
<Compile Include="FrozenMap\FrozenMapTuple.fs" />
<Compile Include="FrozenMap\FrozenMap.Extensions.fsi">
<DependentUpon>FrozenMap.fsi</DependentUpon>
</Compile>
<Compile Include="FrozenMap\FrozenMap.Extensions.fs">
<DependentUpon>FrozenMap.fs</DependentUpon>
</Compile>
<Compile Include="FrozenMap\FrozenMapCE.fsi" />
<Compile Include="FrozenMap\FrozenMapCE.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,109 @@
namespace FSharp.Collections.Immutable

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

// The struct holds the key comparer and creates frozen maps with it (ADR 0001, D12). A struct cannot run code in a primary
// constructor, so the constructor that checks the comparer is explicit. Every struct also has a default value that no
// constructor checks; its field is null, and the property gives it the default comparer, which the members use.
[<Struct; NoComparison; NoEquality>]
type CustomComparersFrozenMap<'Key, 'Value when 'Key : not null> =

val private keyComparer : IEqualityComparer<'Key> | null

new (keyComparer : IEqualityComparer<'Key>)
=
checkNotNull (nameof keyComparer) keyComparer
{ keyComparer = keyComparer }

/// Sets the bindings, split into keys and values, one by one in a dictionary with the key comparer and freezes it, so
/// that a later binding of a key replaces the value of an earlier one, as in `Map.ofSeq`, and the earlier key stays, as
/// in ToFrozenDictionary (D14); every shape of the bindings creates its maps here (D13). A frozen map has no builder;
/// ToFrozenDictionary reads a dictionary whose comparer equals the given one directly, without building another one.
member inline private this.Build ([<InlineIfLambda>] split : 'Binding -> struct ('Key * 'Value), elements : seq<'Binding>) =
let comparer = this.KeyComparer
let bindings = Dictionary<'Key, 'Value> comparer
Dictionaries.setAll split bindings elements
FrozenDictionary.ToFrozenDictionary (bindings, comparer)

// ---- Creation ----

[<CompiledName "Empty">]
member this.empty : FrozenMap<'Key, 'Value> =
// An empty source gives FrozenDictionary.Empty for the default comparer and an empty map of the comparer otherwise
FrozenDictionary.ToFrozenDictionary (Array.Empty<KeyValuePair<'Key, 'Value>>(), this.KeyComparer)

[<CompiledName "OfArray">]
member this.ofArray (elements : KeyValuePair<'Key, 'Value> array) =
checkNotNull (nameof elements) elements
this.Build (Dictionaries.splitKeyValuePair, elements)

[<CompiledName "OfArray">]
member this.ofArray (elements : ('Key * 'Value) array) =
checkNotNull (nameof elements) elements
this.Build (Dictionaries.splitTuple, elements)

[<CompiledName "OfArray">]
member this.ofArray (elements : struct ('Key * 'Value) array) =
checkNotNull (nameof elements) elements
this.Build (id, elements)

[<CompiledName "OfList">]
member this.ofList (elements : KeyValuePair<'Key, 'Value> list) = this.Build (Dictionaries.splitKeyValuePair, elements)

[<CompiledName "OfList">]
member this.ofList (elements : ('Key * 'Value) list) = this.Build (Dictionaries.splitTuple, elements)

[<CompiledName "OfList">]
member this.ofList (elements : struct ('Key * 'Value) list) = this.Build (id, elements)

[<CompiledName "OfReadOnlySpan">]
member this.ofReadOnlySpan (span : ReadOnlySpan<KeyValuePair<'Key, 'Value>>) : FrozenMap<'Key, 'Value> =
// Create reads the span into a dictionary with the comparer, so that a later binding of a key replaces the value of
// an earlier one and the earlier key stays, as for the other shapes (ADR 0001, D14)
FrozenDictionary.Create (this.KeyComparer, span)

[<CompiledName "OfSeq">]
member this.ofSeq (elements : seq<KeyValuePair<'Key, 'Value>>) =
// A default flat list given as a sequence fails when it is enumerated, so it is rejected as a null sequence is (D6)
checkNotNullOrDefaultSeq (nameof elements) elements

match elements with
// The keys of a frozen map are distinct in its own comparer, so a map whose comparer equals the comparer of the
// struct comes back itself, as ToFrozenDictionary returns it (ADR 0001, D14); another comparer can make keys equal,
// and then the keys merge: the earlier key stays and the later value wins
| :? FrozenDictionary<'Key, 'Value> as table when table.Comparer.Equals this.KeyComparer -> table
| _ -> this.Build (Dictionaries.splitKeyValuePair, elements)

[<CompiledName "OfSeq">]
member this.ofSeq (elements : seq<'Key * 'Value>) =
checkNotNullOrDefaultSeq (nameof elements) elements
this.Build (Dictionaries.splitTuple, elements)

[<CompiledName "OfSeq">]
member this.ofSeq (elements : seq<struct ('Key * 'Value)>) =
checkNotNullOrDefaultSeq (nameof elements) elements
this.Build (id, elements)

[<CompiledName "OfSeqBy">]
member this.ofSeqBy (projection : 'Value -> 'Key) (source : seq<'Value>) : FrozenMap<'Key, 'Value> =
checkNotNullOrDefaultSeq (nameof source) source
FrozenDictionary.ToFrozenDictionary (source, Func<'Value, 'Key> projection, this.KeyComparer)

// Span.op_Implicit is the conversion that F# applies by itself where the type of the parameter is known, with the warning
// FS3391; calling it explicitly avoids the warning
[<CompiledName "OfSpan">]
member this.ofSpan (span : Span<KeyValuePair<'Key, 'Value>>) : FrozenMap<'Key, 'Value> =
this.ofReadOnlySpan (Span<KeyValuePair<'Key, 'Value>>.op_Implicit span)

[<CompiledName "Singleton">]
member this.singleton (key : 'Key) (value : 'Value) : FrozenMap<'Key, 'Value> =
FrozenDictionary.ToFrozenDictionary ([| KeyValuePair (key, value) |], this.KeyComparer)

// ---- Properties ----

member this.KeyComparer : IEqualityComparer<'Key> =
match this.keyComparer with
| null -> EqualityComparer<'Key>.Default
| comparer -> comparer
Loading