Skip to content

docs: add ADR 0001 for the per-collection module rewrite - #23

Open
xperiandri wants to merge 12 commits into
mainfrom
docs/adr-collection-modules
Open

xperiandri wants to merge 12 commits into
mainfrom
docs/adr-collection-modules

Conversation

@xperiandri

@xperiandri xperiandri commented Oct 10, 2026 •

Copy link
Copy Markdown
Collaborator

Proposed Changes

Adds docsSrc/Explanations/ADR_0001_Collection_Modules.md, an architecture decision record for rewriting the collection modules before any code lands.

It decides:

  • one folder per collection, with root modules and .fsi signature files;
  • new FrozenMap/FrozenSet modules, with the alternate lookup on net10.0;
  • a nested FlatList.Parallel module;
  • voption results and struct tuples by default, with Option/Tuple override namespaces;
  • comparers only through one struct per map (CustomComparers*) and per set (CustomComparer*), which the modules create with forComparers/forKeyComparer/forComparer;
  • map functions that take bindings in every shape: creation through overloads for KeyValuePairs, reference tuples and struct tuples; addRange/setItems over KeyValuePairs with Tuple/ValueTuple submodules;
  • test categories on the test classes only.

It also fixes:

  • a category-based function order, checked by a script;
  • an implementation that uses the BCL and System.Linq first and keeps the FSharp.Core semantics of exceptions and sort stability;
  • contract test suites shared between collections;
  • delivery as one pull request per collection.

The appendices are the function-by-collection support matrices for sequence-like collections, maps and sets. fsdocs publishes the page next to Background.

Types of changes

What types of changes does your code introduce to FSharp.Collections.Immutable?
Put an x in the boxes that apply

  • Bugfix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)

Documentation only: the ADR describes breaking changes that the following pull requests will make, but this one changes no code.

Checklist

Put an x in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code.

  • Build and tests pass locally (not run for this pull request – it changes one Markdown file; CI builds it)
  • I have added tests that prove my fix is effective or that my feature works (not applicable – documentation only)
  • I have added necessary documentation (if appropriate)

Further comments

The matrices are the contract every following pull request implements, so wrong cells are cheapest to fix here.

The fixes for both Copilot reviews are folded into the commits that introduced the text:

  • the Option namespace is defined by representation, with the exact FSharp.Core signature only where FSharp.Core has the function; the Tuple namespace holds every function whose signature mentions a struct tuple;
  • FlatList ranges copy, because ImmutableArray.Slice allocates unless the range is empty or whole; IndexedList ranges cost time and memory in proportion to their length, because ImmutableList.GetRange builds a new tree;
  • every Stack constructor pushes from the end, because the BCL Create/CreateRange leave the last element on top;
  • sort stability follows the mirrored FSharp.Core module (FlatList unstable like Array.sort*, the others stable through LINQ OrderBy), all sorts using F# structural comparison;
  • each function throws what the FSharp.Core function of the mirrored module throws for the same input, with a measured table and the deliberate deviations;
  • transformations keep each comparer whose type does not change, and map and choose give the result the default comparer of the new type;
  • the alternate lookup section states why F#-declared wrappers cannot take ReadOnlySpan<char> (F# consumes, but cannot declare, allows ref struct).

Each point was checked at runtime against System.Collections.Immutable 10.0.8 and FSharp.Core 10.

Three later commits redesign parts after the review of #30.

  • e49a394:
    • D2: root modules, and a types file per folder, because a root module cannot declare a type of its namespace.
    • D12: the comparer structs.
    • D13: the static extension overloads of map creation.
  • d81372c: D8 puts the test categories on the test classes only, as the instructions on main now say.
  • 4c822eb:
    • D12: the structs are named CustomComparers* (maps) and CustomComparer* (sets), and the modules create them with forComparers/forKeyComparer/forComparer.
    • D13: addRange/setItems take KeyValuePairs, with Tuple/ValueTuple submodules for reference and struct tuples.
    • D2: the types file is XTypes.fs without a signature file. A signature file alone fails with FS0240.

A prototype with F# 10 checked these points:

  • overload resolution through the type abbreviation, including pipelines;
  • that a module function of the same name hides the extension members;
  • FS0816: curried members cannot be overloaded;
  • FS0340: a member cannot constrain the type parameters of the extended type, so the sorted map extensions declare their own;
  • that the Tuple/ValueTuple submodules resolve after open FSharp.Collections.Immutable.Tuple;
  • that a types file without a signature file works.

🤖 Generated with Claude Code

Copilot AI balanced review requested due to automatic review settings October 10, 2026 23:44
@xperiandri
xperiandri requested a review from Thorium October 10, 2026 23:46

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Several documented contracts conflict with underlying collection behavior, and the ADR is future-dated.

4 open findings
What changed in this PR

Adds ADR 0001 to define the planned per-collection module rewrite.

Changes:

  • Defines API, layout, implementation, and testing decisions.
  • Introduces Frozen collection support and value-type defaults.
  • Documents per-collection support matrices and delivery plan.
File Description
docsSrc/​Explanations/​ADR_0001_Collection_Modules.md Adds the architectural decision record and API matrices.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread docsSrc/Explanations/ADR_0001_Collection_Modules.md Outdated
Comment thread docsSrc/Explanations/ADR_0001_Collection_Modules.md Outdated
Comment thread docsSrc/Explanations/ADR_0001_Collection_Modules.md Outdated
Comment thread docsSrc/Explanations/ADR_0001_Collection_Modules.md Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread docsSrc/Explanations/ADR_0001_Collection_Modules.md Outdated
Comment thread docsSrc/Explanations/ADR_0001_Collection_Modules.md Outdated
Comment thread docsSrc/Explanations/ADR_0001_Collection_Modules.md Outdated
Comment thread docsSrc/Explanations/ADR_0001_Collection_Modules.md Outdated
xperiandri and others added 6 commits October 18, 2026 14:50
Records the decisions behind the rewrite of the collection modules: one
folder and signature file per collection, `FrozenMap`/`FrozenSet`
support, `voption` and struct-tuple defaults with `Option`/`Tuple`
override namespaces, category-based function order, BCL/LINQ-first
implementation, shared contract tests, one pull request per collection,
and the function-by-collection support matrices.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…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>
- `IndexedList`, `Queue`, `Stack` and their interface modules get the
  struct partial patterns `Cons` and `Nil` instead of the multi-case
  `(|Cons|Nil|)`, which allocates a `Choice` per match (ADR 0002 §5).
- Sorts avoid culture-sensitive comparers such as
  `Comparer<string>.Default`; `Comparer<'T>.Default` stays usable where
  it agrees with F# comparison, as ADR 0002 §3 uses it.
- Operation test categories come from `nameof`, never from string
  literals; a contract base takes the name from any module.
- `EquatableArray` is named in D1, in the computation expressions, in
  the pull request chain as `feat/equatable-array`, and in Appendix A;
  step 1 of ADR 0002 may ship from `main` before the chain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…in ADR 0001

The review of the HashMap pull request (#30) changes three parts of the design:

- D2: a file with one module declares it as the root module of the file; the type
  abbreviations of a folder move to `XTypes.fsi`/`XTypes.fs`, because a root module
  cannot declare a type of its namespace. Modules carry no `[<CompiledName>]`.
- D12 (new): the comparers that a map or a set keeps come only through one
  `Comparable*` struct per collection, whose members are the creation functions
  with the comparers applied. It replaces the `*WithComparer(s)`, `*WithKeyComparer`
  and `with*Comparer(s)` functions.
- D13 (new): `ofArray`, `ofList` and `ofSeq` of the maps are static extension members
  in an `[<AutoOpen>]` `XExtensions` module, overloaded for `KeyValuePair`, reference
  tuples and struct tuples. `ofKeyValuePairs` and the `Tuple` creation functions go;
  `setItems` takes struct tuples like `addRange`.

The ADR also records what the implementation found: the explicit type parameters of
the alternate lookups (FS0687), the `comparison` constraint and the cached default
comparer of the sorted collections, the O(n) `maxKeyValue` of `SortedMap`, and why
`keys`/`values` return different types.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
D8 put an operation category on every test method through `TestCategory (nameof …)`.
The test category rules forbid categories written as strings and put the attribute of the
examined component on the class (#32), so the categories of the contract and
collection tests are the `TestCategories.fs` attributes on the test classes, and test
methods carry none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…aluePair` map functions in ADR 0001

- D12: the structs that create collections with comparers are named after what they
  hold, `CustomComparers*` for maps and `CustomComparer*` for sets. The modules create
  them with `forComparers`/`forKeyComparer` (maps) and `forComparer` (sets), in the
  Creation category.
- D13: `addRange` and `setItems` take `KeyValuePair`s, the shape of every dictionary.
  The nested submodules `Tuple` and `ValueTuple` of the map modules hold them for
  reference and struct tuples; after `open FSharp.Collections.Immutable.Tuple` the
  submodules still resolve. They leave the `Tuple` namespace.
- D2: the types file of a folder is `XTypes.fs` without a signature file, because a
  signature file without an implementation file fails with FS0240.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@xperiandri
xperiandri force-pushed the docs/adr-collection-modules branch from d3f830a to 4c822eb Compare October 18, 2026 13:03
xperiandri and others added 2 commits October 18, 2026 15:19
… `rev` in ADR 0001

- D2: `tryHead`/`tryTail`/`tryUncons` compile as `TryPeek`/`TryDequeue`/`TryPeekDequeue`
  for `Queue` and as `TryPeek`/`TryPop`/`TryPeekPop` for `Stack`; `enqueueMany` as
  `EnqueueMany` and `consMany` as `PushMany`, so compiled code has one .NET vocabulary.
- D5: a member gives way to a measured faster path that keeps the contract.
  `IndexedList.rev` copies, reverses and builds with `CreateRange`, about 2.5 times
  faster than `ImmutableList.Reverse` at the same allocation; Appendix A notes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
D2 listed `CustomComparersX`/`CustomComparerX` after the module, but `forComparers`,
`forKeyComparer` and `forComparer` of the module return the struct, so the struct
compiles first; the extension module follows both, because its members are those of
the struct with the default comparers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri and others added 4 commits October 18, 2026 18:06
…computation expressions, and D14 in ADR 0001

D2: the comparer structs and the extension modules live in `X.CustomComparers.fs` (`X.CustomComparer.fs` for sets) and
`X.Extensions.fs`, with `DependentUpon` on the files of the module, so that Visual Studio shows them under it. Every file
with public API has a signature file, the builders and the computation expressions included; only `XTypes.fs` has none.

D14: where FSharp.Core and the BCL disagree in an edge case, the modules follow .NET: which of two equal elements
`intersect` and `union` keep, the implementation that `append` keeps, repeated keys in `ofSeqBy`, values that a value
comparer finds equal, comparer identity of the frozen collections, and the default value of a comparer struct.
`IsProperSupersetOf` of the immutable sets counts repeated elements, against its documentation; the modules answer with
set semantics.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The relations of the set modules take two sets, so the defect of `IsProperSupersetOf` shows when the second set holds
elements that the first set's comparer finds equal; a sequence with repetitions is the BCL's own example of it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A probe of `ImmutableDictionary` and `ImmutableSortedDictionary` with case-insensitive comparers shows that both keep
the stored key and value when the value comparer finds the new value equal, in `SetItem`, `SetItems`, `AddRange` and the
builder indexers, and that both replace the key and the value for another value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`ImmutableHashSet.Union` always keeps the element of the first set. `ImmutableSortedSet.Union` keeps the element of the
larger set when the other operand is a sorted set with the same comparer, because it then adds the smaller set to the
larger one; `SortedSet.union` follows the member that it wraps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@xperiandri
xperiandri force-pushed the main branch 2 times, most recently from 5d56ba0 to 9d7316e Compare October 20, 2026 16:16

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants