Skip to content

Commit 4cb6fde

Browse files
xperiandriclaude
andcommitted
docs: add FlatList.Parallel and the Frozen alternate lookup to ADR 0001
Moves both items from follow-ups into the decision: a nested `FlatList.Parallel` module mirroring `Array.Parallel` through the zero-copy bridge, delivered as its own pull request after `FlatList`, and `alternateLookup`/`tryAlternateLookup` on `FrozenMap`/`FrozenSet` under `#if NET10_0_OR_GREATER`, delivered inside the Frozen pull requests. The appendices gain the matching matrix rows. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent d087784 commit 4cb6fde

1 file changed

Lines changed: 56 additions & 11 deletions

File tree

‎docsSrc/Explanations/ADR_0001_Collection_Modules.md‎

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,8 @@ Maps and sets measure their size with `count` (FSharp.Core `Map.count`/`Set.coun
9393
* The conversion rules are about representation, not about FSharp.Core having the function. Where FSharp.Core has it
9494
(`tryFind`, `tryHead`, `choose`, `pick`, `unfold`, `zip`, `partition`, `Map.tryFind`, `Map.change`, …), opening both
9595
namespaces with `.Option` last yields exactly its signature. Extras without a counterpart (`tryTail`, `uncons`,
96-
`tryUncons`, `tryGetKey`, `tryGetValue`, map and set `choose`, `SortedSet.tryItem`) get the same representation, which is
97-
the one FSharp.Core would use, but there is no FSharp.Core signature to match.
96+
`tryUncons`, `tryGetKey`, `tryGetValue`, `tryAlternateLookup`, map and set `choose`, `SortedSet.tryItem`) get the same
97+
representation, which is the one FSharp.Core would use, but there is no FSharp.Core signature to match.
9898

9999
This relies on F# name resolution, verified in `src/Compiler/Checking/NameResolution.fs` of the compiler: opened modules are
100100
prepended to the environment (most recently opened first), the resolver takes the first module that contains the member and
@@ -175,8 +175,8 @@ F# `Set<string>` is.
175175
### D6. Exceptions mirror FSharp.Core
176176

177177
Each function validates its arguments as the FSharp.Core function of the mirrored module does for the same input, and
178-
throws the same exception type with the same message. The mirrored module is `Array` for `FlatList`,
179-
`List` for `IndexedList`, `Queue`, `Stack` and their interface modules, `Map` for the map modules and
178+
throws the same exception type with the same message. The mirrored module is `Array` for `FlatList`, `Array.Parallel` for
179+
`FlatList.Parallel`, `List` for `IndexedList`, `Queue`, `Stack` and their interface modules, `Map` for the map modules and
180180
`Set` for the set modules. The rules differ per operation and per module (FSharp.Core 10, measured):
181181

182182
| Input | `Array` | `List` |
@@ -230,17 +230,48 @@ after conversion.
230230
### D9. Delivery: one pull request per collection, stacked
231231

232232
`chore/foundation` (helper module, order check script, test categories and contract bases, changelog and documentation
233-
skeleton, target frameworks), then `feat/flat-list`, `feat/indexed-list`, `feat/queue`, `feat/stack`, `feat/hash-map` (with
234-
`IMap`), `feat/sorted-map`, `feat/hash-set` (with `ISet`), `feat/sorted-set`, `feat/frozen-map`, `feat/frozen-set`, and an
235-
optional `chore/benchmarks` (BenchmarkDotNet, excluded from the solution filter). Each pull request deletes the legacy file
233+
skeleton, target frameworks), then `feat/flat-list`, `feat/flat-list-parallel` (the nested `Parallel` module of D11),
234+
`feat/indexed-list`, `feat/queue`, `feat/stack`, `feat/hash-map` (with `IMap`), `feat/sorted-map`, `feat/hash-set` (with
235+
`ISet`), `feat/sorted-set`, `feat/frozen-map`, `feat/frozen-set` (both with the alternate lookup of D11), and an optional
236+
`chore/benchmarks` (BenchmarkDotNet, excluded from the solution filter). Each pull request deletes the legacy file
236237
it replaces, keeps the build green and updates the changelog and the documentation table. No release is cut mid-chain.
237238

238239
### D10. Target frameworks
239240

240241
`net8.0;net10.0`. The published package already targeted `net8.0` only; `netstandard2.0` is not added (the LINQ surface
241242
used in D5 would need conditional fallbacks); `net9.0` is not added (short-term support, ended 2026-05; the `net8.0` asset
242-
serves it). `net10.0` enables `#if NET10_0_OR_GREATER` paths for newer LINQ members and, as a follow-up, the Frozen
243-
alternate lookup.
243+
serves it). `net10.0` enables `#if NET10_0_OR_GREATER` paths for newer LINQ members and the Frozen alternate lookup (D11).
244+
245+
### D11. `FlatList.Parallel` and the Frozen alternate lookup
246+
247+
`FlatList` gets a nested module `Parallel` that mirrors `Array.Parallel` through the zero-copy bridge of D5: every function
248+
unwraps the array, calls `Array.Parallel.*` and wraps the fresh result (`map`, `mapi`, `choose`, `collect`, `filter`, `init`,
249+
`iter`, `iteri`, `zip`, `groupBy`, `partition`, `partitionWith`, `sort`, `sortBy`, `sortByDescending`, `sortDescending`,
250+
`sortWith`, `reduce`, `reduceBy`, `sum`, `sumBy`, `average`, `averageBy`, `max`, `maxBy`, `min`, `minBy`, `exists`, `forall`,
251+
`tryFind`, `tryFindIndex`, `tryPick`); the three `sortInPlace*` functions are omitted because they mutate. The nested module
252+
lives in `FlatList.fsi`/`FlatList.fs` (as `Array.Parallel` lives in `array.fs`) with its own category banners, and has its
253+
`Option` and `Tuple` counterparts (`choose`, `tryFind`, `tryFindIndex`, `tryPick`; `groupBy`, `partition`, `partitionWith`,
254+
`zip`). Its tests compare every function with the sequential `FlatList` counterpart on the same inputs.
255+
256+
`FrozenMap` and `FrozenSet` expose `alternateLookup<'Alternate>` (`GetAlternateLookup`) and `tryAlternateLookup<'Alternate>`
257+
(`TryGetAlternateLookup`, returning `voption`) under `#if NET10_0_OR_GREATER`, because the API exists only in the `net9.0`
258+
and later assets of the package. They return the BCL `AlternateLookup<'Alternate>` struct, whose `ContainsKey`, `TryGetValue`
259+
and indexer (map) or `Contains` and `TryGetValue` (set) do the lookups; no `findAlternate`-style wrappers over those members
260+
are added. The comparer must implement `IAlternateEqualityComparer<'Alternate, 'Key>`; `tryAlternateLookup` returns
261+
`ValueNone` otherwise. The tests for these functions compile only for `net10.0`.
262+
263+
The wrappers cannot serve `ReadOnlySpan<char>`, the alternate key type that string keys use with `StringComparer.Ordinal`.
264+
Since F# 9, F# can instantiate generic parameters that C# declares with the `allows ref struct` anti-constraint, so
265+
`map.GetAlternateLookup<ReadOnlySpan<char>> ()` and the members of the returned struct work when called directly. F# cannot
266+
declare that anti-constraint on its own type parameters, though: the
267+
[F# byrefs documentation](https://learn.microsoft.com/dotnet/fsharp/language-reference/byrefs) states that generic code
268+
supporting byref-like types cannot be authored in F#, and [dotnet/fsharp#16800](https://github.com/dotnet/fsharp/issues/16800)
269+
limited the F# 9 support to consumption. An F# wrapper instantiated with `ReadOnlySpan<char>` therefore fails with FS0412
270+
("A type instantiation involves a byref type").
271+
[fslang-suggestions#1476](https://github.com/fsharp/fslang-suggestions/issues/1476) and RFC FS-1356 propose that F# declares
272+
the anti-constraint. When that ships, the wrappers declare `'Alternate : allows ref struct` and accept spans. Until then, the
273+
documentation of both functions points span users to the BCL methods, and the wrappers serve alternate key types that are
274+
not byref-like, such as an `int` alternate for `int64` keys with a custom comparer.
244275

245276
## Consequences
246277

@@ -390,6 +421,12 @@ their element-type-changing transformations return the concrete type.
390421
| **Type-specific** | | | | | | | | | |
391422
| `clear` | | – | – | ᵇ | – | ᵇ | – | ᵇ | keeps the implementation |
392423
| `isDefault` / `isDefaultOrEmpty` | | ᵇ | – | – | – | – | – | – | accept a default `FlatList` |
424+
| **Parallel** (nested module `FlatList.Parallel`, mirrors `Array.Parallel`; same category order inside) | | | | | | | | | |
425+
| `average` / `averageBy` / `max` / `maxBy` / `min` / `minBy` / `reduce` / `reduceBy` / `sum` / `sumBy` | | ✓ | – | – | – | – | – | – | Folding |
426+
| `choose` / `collect` / `filter` / `init` / `iter` / `iteri` / `map` / `mapi` / `zip` | Ⓥ (`choose`), Ⓣ (`zip`) | ✓ | – | – | – | – | – | – | Transformation (`init`: Creation) |
427+
| `exists` / `forall` / `tryFind` / `tryFindIndex` / `tryPick` | Ⓥ (`try*`) | ✓ | – | – | – | – | – | – | Search |
428+
| `groupBy` / `partition` / `partitionWith` | Ⓣ | ✓ | – | – | – | – | – | – | Partitioning and grouping |
429+
| `sort` / `sortBy` / `sortByDescending` / `sortDescending` / `sortWith` | | ✓ | – | – | – | – | – | – | Sorting, unstable like `Array.Parallel.sort*`; `sortInPlace*` omitted |
393430

394431
`Option.*`: `choose`, `pick`, `tryExactlyOne`, `tryFind`, `tryFindBack`, `tryFindIndex`, `tryFindIndexBack`, `tryHead`,
395432
`tryItem`, `tryLast`, `tryPick`, `tryTail`, `tryUncons`, `unfold`.
@@ -443,8 +480,12 @@ their element-type-changing transformations return the concrete type.
443480
| `partition` | Ⓣ | ✓ | ✓ | ✓ | ✓ | |
444481
| **Conversion** | | | | | | |
445482
| `toArray` / `toList` / `toSeq` | Ⓣ | ✓ | ✓ | ✓ | ✓ | `struct ('Key * 'Value)` |
483+
| **Type-specific** | | | | | | |
484+
| `alternateLookup<'Alternate>` | | – | – | – | ᵇ | `GetAlternateLookup`; `#if NET10_0_OR_GREATER` |
485+
| `tryAlternateLookup<'Alternate>` | Ⓥ | – | – | – | ᵇ | `TryGetAlternateLookup`; `#if NET10_0_OR_GREATER` |
446486

447-
`Option.*`: `change`, `choose`, `pick`, `tryFind`, `tryFindKey`, `tryGetKey`, `tryPick`. `Tuple.*`: `addRange`, `ofArray`,
487+
`Option.*`: `change`, `choose`, `pick`, `tryFind`, `tryFindKey`, `tryGetKey`, `tryPick` (+ `tryAlternateLookup` for
488+
`FrozenMap`, `net10.0` only). `Tuple.*`: `addRange`, `ofArray`,
448489
`ofList`, `ofSeq`, `ofSeqWithKeyComparer`, `partition`, `toArray`, `toList`, `toSeq` (+ `maxKeyValue`, `minKeyValue` for
449490
`SortedMap`).
450491

@@ -488,5 +529,9 @@ their element-type-changing transformations return the concrete type.
488529
| **Conversion** | | | | | | |
489530
| `toArray` / `toList` / `toSeq` | | ✓ | ✓ | ✓ | ✓ | |
490531
| `toFlatList` | | – | – | – | ᵇ | O(1) via `Items` |
532+
| **Type-specific** | | | | | | |
533+
| `alternateLookup<'Alternate>` | | – | – | – | ᵇ | `GetAlternateLookup`; `#if NET10_0_OR_GREATER` |
534+
| `tryAlternateLookup<'Alternate>` | Ⓥ | – | – | – | ᵇ | `TryGetAlternateLookup`; `#if NET10_0_OR_GREATER` |
491535

492-
`Option.*`: `choose`, `tryGetValue` (+ `tryItem` for `SortedSet`). `Tuple.*`: `partition`, `partitionWith`.
536+
`Option.*`: `choose`, `tryGetValue` (+ `tryItem` for `SortedSet`, `tryAlternateLookup` for `FrozenSet`, `net10.0` only).
537+
`Tuple.*`: `partition`, `partitionWith`.

0 commit comments

Comments
 (0)