Skip to content

feat: FlatList module with the vocabulary of the FSharp.Core Array module - #26

Merged
xperiandri merged 27 commits into
mainfrom
feat/flat-list
Oct 18, 2026
Merged

xperiandri merged 27 commits into
mainfrom
feat/flat-list

Conversation

@xperiandri

@xperiandri xperiandri commented Oct 18, 2026 •

Copy link
Copy Markdown
Collaborator

Proposed Changes

Rewrites FlatList (ImmutableArray<'T>) after ADR 0001 (#23), on top of the foundation merged in #25.

  • FlatList/FlatList.fsi and FlatList.fs: the root module FSharp.Collections.Immutable.FlatList with 161 functions in the canonical category order, each documented in the signature file with its parameters, result, exceptions and complexity. The set is the FlatList column of ADR Appendix A. It has every function of the FSharp.Core Array module that applies to an immutable array. It has the BCL wrappers (add, asMemory, asSpan, binarySearch, binarySearchRange, binarySearchRangeWithComparer, binarySearchWithComparer, indexOf, indexOfRange, indexOfRangeWith, indexOfWith, lastIndexOf, lastIndexOfRange, lastIndexOfRangeWith, lastIndexOfWith, ofReadOnlySpan, ofSpan, remove, removeMany, removeManyWith, removeWith, replace, replaceWith, sortRange, sortRangeWith, sortRangeWithComparer, sortWithComparer, the builder functions, isDefault, isDefaultOrEmpty). It also has the extras skipLast, takeLast, tryTail, build and update.
  • The flat list parameter is array, as in Array. insertAt, insertManyAt, removeAt, removeManyAt and updateAt call it source, as their FSharp.Core 10 counterparts do. So do the removals by value: remove, removeWith, removeMany and removeManyWith.
  • sub array startIndex count has the meaning, the exceptions and the messages of Array.sub. It wraps ImmutableArray<'T>.Slice, so an empty or a whole range copies nothing. It compares the count with the length minus the start index. A huge count cannot overflow that check, as it does in Array.sub.
  • remove item source and removeWith comparer item source wrap Remove. They remove the first element that equals the item. They return the input itself when no element equals it. removeMany items source and removeManyWith comparer items source wrap RemoveRange. Each value removes the first remaining element that equals it. A value without an equal element removes nothing. removeMany checks the flat list before the items, as RemoveRange does.
  • remove and removeMany compare in EqualityComparer<'T>.Default, as indexOf does. The With variants take the equality comparer. contains compares in F# equality. The two differ for nan, which remove removes, and for arrays, which remove compares by reference.
  • sortRange index count array, sortRangeWith comparison index count array and sortRangeWithComparer comparer index count array wrap Sort(index, count, comparer). The elements outside the range keep their places. The sort is not stable. sortRange compares in F# generic comparison, as sort does. A range of at most one element, or a range that is sorted, returns the input itself. A range out of range throws ArgumentOutOfRangeException for index or count, as the BCL does.
  • indexOfRange startIndex count value array, indexOfRangeWith, lastIndexOfRange and lastIndexOfRangeWith wrap the overloads of IndexOf and LastIndexOf that take a range. They return an index of the flat list, not of the range, and -1 when no element is equal. A search from the back starts at startIndex and goes down over count elements. They throw what the BCL members throw. On .NET 8 and .NET 10 that includes an empty range at a start index that equals the length, and an empty flat list accepts only the range 0, 0. They replace the indexFrom and indexRange families of 1.0.0, which the changelog now says.
  • replace oldValue newValue array and replaceWith comparer oldValue newValue array wrap Replace. They replace the first element that equals the old value. They throw ArgumentException when no element does, and they return a new flat list also when the new value equals the old one.
  • binarySearchWithComparer comparer value array, binarySearchRange index count value array and binarySearchRangeWithComparer comparer index count value array wrap ImmutableArray.BinarySearch. binarySearchRange compares as binarySearch does, through the cached DefaultComparer<'T>. The other two take the comparer, and null stands for Comparer<'T>.Default. A comparer that throws gives InvalidOperationException with the exception inside. The BCL calls the count of a range length. The functions check index and count first, with the type and the message of the BCL, and name the parameter count. A range beyond the end stays with the BCL.
  • FlatList/FlatListTypes.fs holds the FlatList<'T> abbreviation (D2): a root module cannot declare a type of the namespace, and a file of abbreviations needs no signature file (a signature file alone fails with FS0240). Every other file with public API has a signature file: FlatListBuilder.fsi, FlatListCE.fsi and ComputationExpressions.fsi included.
  • Most functions unwrap the underlying array without a copy (ImmutableCollectionsMarshal), call the Array function and wrap its fresh result, so they keep the semantics and the exceptions of Array (D5, D6). The range functions repeat the checks of Array.take, skip and splitAt and slice, which copies nothing for an empty or a whole range.
  • binarySearch and every sort use F# generic comparison through DefaultComparer<'T> in Helper.fs, one cached comparer per type: a comparer created per call breaks the same-comparer fast paths of the BCL for tuples.
  • try* functions return voption, tuples are struct tuples (D3). The root modules Option.FlatList and Tuple.FlatList repeat those functions in the namespaces FSharp.Collections.Immutable.Option and .Tuple with options and reference tuples; opening both, Option last, gives the signatures of the Array module. Per-element tuple producers share one algorithm that takes the tuple constructor, so neither variant pays a second pass.
  • FlatListBuilder works on ImmutableArray<'T>.Builder with 36 functions. The functions that change the builder return it. removeWith returns whether it removed an element, as remove does. flatList { } collects into a builder and drains it without a copy when it is full (D7).
  • The builder has the searches of a range, insertManyAt, removeAll, removeMany, removeManyAt, removeManyWith, removeWith, replace, replaceWith, the range sorts sortRange, sortRangeWith and sortRangeWithComparer, and blit. They wrap the members of the builder with the same arguments as the functions of the flat list, in the same words. The range sorts are not stable. replace and replaceWith leave the builder as it is when no element equals the old value, as ImmutableArray<'T>.Builder.Replace does, where FlatList.replace throws. removeManyAt and blit name the parameters count, target and targetIndex, where the BCL says length, destination and destinationIndex, and they check the ranges in the order and with the messages of the BCL.
  • A seq<'T> parameter has one check, checkNotNullOrDefaultSeq. null and a default flat list given as a sequence throw ArgumentNullException for that parameter in concat, transpose, except, insertManyAt, ofSeq, removeMany, removeManyWith, addRange, yield! and for. A default flat list given as a sequence is a boxed value, not null. Without the check, it fails later with another exception.
  • The check also finds a default flat list of a derived reference type that arrives as a sequence of a base type, by the covariance of seq: a default FlatList<string> as seq<obj> is an ImmutableArray<string>, which the type test for ImmutableArray<obj> does not see. The type test stays the first step and allocates nothing. For any other object, the check asks for the generic type and reads IsDefault through reflection. It is inline, because the public members of the computation expression call it, and an inline member cannot call an internal function.
  • Helper.fs gets the FSharp.Core message texts, exact replicas of its DetailedExceptions helpers, the shared tuple-constructor algorithms, DefaultComparer<'T> and checkNotNullOrDefaultSeq.
  • The legacy FlatList.fs and the Seq module of cross conversions are removed.
  • Documentation: the changelog lists the new functions and names the replacement of every function of 1.0.0 that is gone. It was checked against the published package, so that Removed and Changed name only what 1.0.0 had: the Seq module and unfold were never released.

Tests (2447 per target framework, 2442 of them new): the shared contract bases of the sequence-like modules (SeqLike as a chain of abstract classes, one file per category, plus SeqCreation and Indexed) compute their expectations with FSharp.Core Array over the enumerated elements. They compare every exception with the mirrored FSharp.Core function. Every case is a data row: DynamicData over the shared Inputs and the sources of each contract, DataRow for constants, so each of the 703 cases of the former loops runs and reports as a test of its own. The tests compare arrays with CollectionAssert. FlatList derives one class per facet and adds tests for default values, shared storage, the extras, the builder, the computation expression and both namespaces. The categories are on the test classes only (D8).

  • sub, the range sorts and the removals have 214 tests. They compare the results with Array.sub, with Array.sortWith on the range and with a first-occurrence oracle, in default and in case-insensitive equality. They also check the exceptions, the parameter names and the cases that return the input itself. The test of sub compares the second line of the message too, which names the arguments and the length.
  • The 1122 tests of FlatListSearchTests cover the searches of a range of the flat list and of its builder, and the binary searches. Each search runs on every valid range of its inputs, against a search written by hand. The ranges that the BCL rejects run against the BCL member, with the parameter names of the functions. The searches with a comparer run with the comparer that ignores the case.
  • The tests of replace, replaceWith and the functions of the builder compare the results with the oracles of FlatListOracles: the removal and the replacement of the first equal element, Array.insertManyAt, Array.removeManyAt and Array.blit. The ranges out of range run against the BCL member. The functions that change the builder are proved to return it.
  • Rows pin the sequence check of each function and its parameter name. They also pin the order of the checks: the flat list before the items in removeMany, the items to exclude before the array in except, the index before the values in insertManyAt of the builder. A default FlatList<string> as seq<obj> runs through ofSeq, insertManyAt, except, removeMany, removeManyWith, addRange, yield! and for, and a flat list of strings that is not default must pass.

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)

try* functions return voption, tuples are struct tuples, take and splitAt throw InvalidOperationException for a count beyond the length, a default flat list throws ArgumentNullException, sub takes the flat list first and has the meaning of Array.sub, and the functions listed under Removed in the changelog are gone. removeAll, removeAllWith and removeRange are among them: removeMany, removeManyWith and removeManyAt do their job, and filter with the negated predicate removes by a predicate. So are the index and lastIndex families: indexOf, indexOfRange and their counterparts replace them, and they compare in EqualityComparer<'T>.Default, where the removed functions compared in F# structural equality. The nested FlatList.Builder module is now FlatListBuilder.

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
  • I have added tests that prove my fix is effective or that my feature works (if appropriate)
  • I have added necessary documentation (if appropriate)

Further comments

Array.concat dereferences a null element array without a check; FlatList.concat and transpose throw ArgumentNullException for a default element, as D6 requires for a default FlatList.

F# ignores [<CompiledName>] on modules, so the modules compile under their F# names (FlatList, Option.FlatList), as the legacy 1.0.0 modules did.

🤖 Generated with Claude Code

Copilot AI balanced review requested due to automatic review settings October 18, 2026 10:26

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

Default-value exception handling is inconsistent, and countBy introduces avoidable linear auxiliary storage.

3 open issues
What changed in this PR

Rewrites FlatList around FSharp.Core’s Array vocabulary, implementing ADR 0001 on the foundation from #25.

Changes:

  • Adds expanded collection operations, Option/Tuple variants, builder helpers and flatList { }.
  • Introduces shared contract tests and FlatList-specific coverage.
  • Removes legacy APIs and documents breaking changes.
File Description
tests/​FSharp.Collections.Immutable.Tests/​FSharp.Collections.Immutable.Tests.fsproj Registers new tests and contracts.
tests/​FSharp.Collections.Immutable.Tests/​FlatList/​FlatListTupleTests.fs Tests reference-tuple overrides.
tests/​FSharp.Collections.Immutable.Tests/​FlatList/​FlatListTests.fs Tests defaults, storage sharing and extras.
tests/​FSharp.Collections.Immutable.Tests/​FlatList/​FlatListOptionTests.fs Tests option overrides.
tests/​FSharp.Collections.Immutable.Tests/​FlatList/​FlatListOps.fs Connects FlatList to shared contracts.
tests/​FSharp.Collections.Immutable.Tests/​FlatList/​FlatListContractTests.fs Instantiates collection contracts.
tests/​FSharp.Collections.Immutable.Tests/​FlatList/​FlatListComputationExpressionTests.fs Tests computation-expression behavior.
tests/​FSharp.Collections.Immutable.Tests/​FlatList/​FlatListBuilderTests.fs Tests builder operations.
tests/​FSharp.Collections.Immutable.Tests/​Contracts/​SeqOps.fs Defines contract operation records.
tests/​FSharp.Collections.Immutable.Tests/​Contracts/​SeqLike/​SeqLikeTransformationContract.fs Tests transformations.
tests/​FSharp.Collections.Immutable.Tests/​Contracts/​SeqLike/​SeqLikeSortingContract.fs Tests sorting.
tests/​FSharp.Collections.Immutable.Tests/​Contracts/​SeqLike/​SeqLikeSearchContract.fs Tests searches.
tests/​FSharp.Collections.Immutable.Tests/​Contracts/​SeqLike/​SeqLikeRandomContract.fs Tests random operations.
tests/​FSharp.Collections.Immutable.Tests/​Contracts/​SeqLike/​SeqLikePartitioningContract.fs Tests grouping and ranges.
tests/​FSharp.Collections.Immutable.Tests/​Contracts/​SeqLike/​SeqLikeFoldingContract.fs Tests folds and aggregation.
tests/​FSharp.Collections.Immutable.Tests/​Contracts/​SeqLike/​SeqLikeContract.fs Completes contracts with conversions.
tests/​FSharp.Collections.Immutable.Tests/​Contracts/​SeqLike/​SeqLikeAccessContract.fs Tests properties and access.
tests/​FSharp.Collections.Immutable.Tests/​Contracts/​SeqCreationContract.fs Tests collection creation.
tests/​FSharp.Collections.Immutable.Tests/​Contracts/​IndexedContract.fs Tests indexed operations.
tests/​FSharp.Collections.Immutable.Tests/​Contracts/​ContractBase.fs Shares oracle and exception helpers.
src/​FSharp.Collections.Immutable/​Seq.fs Removes cross-conversion module.
src/​FSharp.Collections.Immutable/​Helper.fs Adds array algorithms and exception helpers.
src/​FSharp.Collections.Immutable/​FSharp.Collections.Immutable.fsproj Registers replacement modules.
src/​FSharp.Collections.Immutable/​FlatList/​FlatListTuple.fsi Declares reference-tuple APIs.
src/​FSharp.Collections.Immutable/​FlatList/​FlatListTuple.fs Implements reference-tuple APIs.
src/​FSharp.Collections.Immutable/​FlatList/​FlatListOption.fsi Declares option APIs.
src/​FSharp.Collections.Immutable/​FlatList/​FlatListOption.fs Implements option APIs.
src/​FSharp.Collections.Immutable/​FlatList/​FlatListCE.fs Implements computation-expression builder.
src/​FSharp.Collections.Immutable/​FlatList/​FlatListBuilder.fs Adds mutable-builder helpers.
src/​FSharp.Collections.Immutable/​FlatList.fs Removes legacy implementation.
src/​FSharp.Collections.Immutable/​ComputationExpressions.fs Exposes flatList.
README.md Lists computation-expression support.
docsSrc/​index.md Updates collection overview.
docsSrc/​Explanations/​Background.md Updates collection support table.
CHANGELOG.md Records additions and breaking changes.

Review effort: Balanced

Comment thread src/FSharp.Collections.Immutable/FlatList/FlatList.fs Outdated
Comment thread src/FSharp.Collections.Immutable/FlatList/FlatList.fs Outdated
Comment thread src/FSharp.Collections.Immutable/Helper.fs Outdated
xperiandri added a commit that referenced this pull request Oct 18, 2026
… `Array` module

Review of #26:

- `ofSeq` rejects a default flat list passed as a sequence with
  `ArgumentNullException`, as every function rejects a default flat
  list (D6); a valid flat list is still returned without a copy.
- `collect` rejects a default flat list that the mapping returns with
  `ArgumentNullException`, as `concat` does.
- `countBy` keeps one counter per key instead of every element: a
  dictionary over keys wrapped in a struct, which admits `null` keys,
  with F# structural equality and the order of first occurrence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@xperiandri
xperiandri force-pushed the feat/flat-list branch 2 times, most recently from 2b5179a to 9d2526e Compare October 18, 2026 15:35
@xperiandri
xperiandri requested a balanced review from Copilot October 18, 2026 15:42

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 src/FSharp.Collections.Immutable/FlatList/FlatListCE.fs Outdated
Base automatically changed from chore/foundation to main October 18, 2026 18:25
xperiandri and others added 13 commits October 18, 2026 20:44
…` module

Rewrites `FlatList` after ADR 0001 in `FlatList/FlatList.fsi` and
`FlatList/FlatList.fs`: 144 functions in the canonical category order,
each documented in the signature file.

- Most functions unwrap the underlying array without a copy, call the
  `Array` function and wrap its fresh result, so that they keep the
  semantics and the exceptions of `Array`. The range functions repeat
  the checks of `Array.take`, `skip` and `splitAt` and slice, which
  copies nothing for an empty or a whole range.
- `try*` functions return `voption`, tuples are struct tuples, and the
  `Option` and `Tuple` namespaces repeat those functions with options
  and reference tuples; per-element tuple producers share one algorithm
  that takes the tuple constructor.
- `FlatListBuilder` works on the builder, and `flatList { }` collects
  its elements in a builder.
- `Helper.fs` gets the FSharp.Core exception messages and helpers that
  the modules mirroring `List` will reuse.
- The legacy `FlatList.fs` and the `Seq` module are removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- `SeqOps`, `SeqCreationOps` and `IndexedOps` record the functions of a
  module; the `SeqLike` facet is a chain of abstract classes, one file
  per category, and `SeqCreation` and `Indexed` complete it.
- Expected values come from the FSharp.Core `List` module over the
  enumerated elements, expected exceptions from the mirrored module.
- `FlatList` derives one test class per facet and adds its own tests:
  default values, shared storage, the builder, the computation
  expression and the `Option` and `Tuple` namespaces.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… `Array` module

F# ignores `[<CompiledName>]` on a module: the modules compile as
`FlatList`, `Option.FlatList`, `Tuple.FlatList` and `FlatListBuilder`,
as the legacy modules of the released package did. The attributes are
removed, and the documentation references use the real IDs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… `Array` module

Review of #26:

- `ofSeq` rejects a default flat list passed as a sequence with
  `ArgumentNullException`, as every function rejects a default flat
  list (D6); a valid flat list is still returned without a copy.
- `collect` rejects a default flat list that the mapping returns with
  `ArgumentNullException`, as `concat` does.
- `countBy` keeps one counter per key instead of every element: a
  dictionary over keys wrapped in a struct, which admits `null` keys,
  with F# structural equality and the order of first occurrence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… `Array` module

Follows the reworked ADR 0001 (D2, D5):

- `FlatList`, `Option.FlatList`, `Tuple.FlatList`, `FlatListBuilder` and
  `ComputationExpressions` are the root modules of their files.
- The `FlatList<'T>` abbreviation moves to `FlatList/FlatListTypes.fs`, which needs no
  signature file.
- `Helper.fs` caches the comparer of F# generic comparison per type
  (`DefaultComparer<'T>`); `binarySearch` and the builder sort use it, so a call no
  longer creates a comparer for a type that is not primitive.
- The cref of `FastGenericComparer` in the remarks of `binarySearch` names the method.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…FlatList`

Test methods carry no categories: the classes carry them (ADR 0001, D8). `FlatListOps`
is the root module of its file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… `Array` module

Two crefs name the right members: `ImmutableArray<T>.Empty` is a field, and the ID of
`ofBuilder` writes the nested generic builder as `ImmutableArray.Builder{``0}`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…FlatList`

The tests compare arrays and take their cases as data rows, so every case runs and
reports as a test of its own (708 tests per target framework instead of 267):

- The ops records observe a collection with its `toArray`, and results of another
  element type are arrays. `SeqOps.ToArray` is both the observation and the `toArray`
  under test; `toList` gets a field and a test of its own, because it no longer
  observes the collections. `Concat` and `Transpose` take the sequence that the module
  takes; `Average`, `Unzip` and `Unzip3` take arrays.
- `SeqContractBase.ForEach` is gone: a test takes its input from
  `DynamicData (nameof Inputs.All, typeof<Inputs>)` and builds the collection and its
  elements with `Create`. Loops inside a test become rows from a source next to it
  (`InputsAndSizes`, `InputsAndCounts`, ...) or `DataRow` constants.
- A row holds printable data, because MSTest names a row by its values: predicates are
  the cases of `ElementPredicate` and `PairPredicate`, whose `Matches` the test takes,
  and the tables of `FlatListTests` give their names as rows.
- Expectations come from the FSharp.Core `Array` functions; exceptions still come from
  the `Array` or the `List` oracle, the latter on `List.ofArray`. Lists remain only as
  the oracles, as the input of `ofList`, the list case of `ofSeq` and the list inputs
  of the functions under test.
- Tuples and value options that hold arrays are compared by their parts.
- The empty collection of `lastIndexOf` and `lastIndexOfWith` is a test of its own
  instead of a check repeated by every row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… `Array` module

A `<summary>` and a `<remarks>` stay on one line each only when both are short (up to 60 characters of text);
otherwise both span several lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…FlatList`

A `<summary>` and a `<remarks>` stay on one line each only when both are short; the results of the ops records that
hold arrays of arrays are `int[][]`, so that a type has one postfix level at most.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… `Array` module

- `FlatListBuilder.fsi`, `FlatListCE.fsi` and `ComputationExpressions.fsi` carry the
  signatures and the XML documentation of their files, compiled right before them: every
  public binding file has a signature file (ADR 0001, D2). `ComputationExpressions` groups
  its instances under the `Creation` banner, so that `check-order.fsx` checks it as well.
- `flatList { yield! array }` rejects a default flat list with `ArgumentNullException`, as
  the functions of `FlatList` do (D6), instead of the `NullReferenceException` of the
  builder; a default flat list given as a sequence is rejected as `FlatList.ofSeq` rejects
  it, instead of the `InvalidOperationException` of its enumeration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…FlatList`

`yield!` of a default flat list, given as a flat list or as a sequence, throws
`ArgumentNullException` for the parameter `items`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

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.

🔵 Needs a closer look

The broad breaking API rewrite warrants human validation, and default-value handling remains inconsistent across sequence inputs.

1 open issue
2 resolved since last review

Review effort: Balanced

Comment thread src/FSharp.Collections.Immutable/FlatList/FlatList.fs Outdated
xperiandri and others added 2 commits October 18, 2026 21:16
… `Array` module

A default flat list given as a sequence is rejected with `ArgumentNullException`, as it is
when it is given as a flat list (D6). It converts to a sequence without a check and fails
with `InvalidOperationException` when a function enumerates it, so every function that takes
a sequence calls the new `checkNotDefaultSeq` of `Helper.fs` next to its `null` check:

- `concat` and `transpose` for the outer sequence;
- `except` for the items to exclude, also when the input is empty, and before the input is
  checked, as FSharp.Core checks them;
- `insertManyAt` for the values, after the check of the index, because FSharp.Core
  enumerates the values only then;
- `FlatListBuilder.addRange`, and `for` of the `flatList` expression; `yield!` of a sequence
  uses the helper instead of its own copy of the check.

The signatures document the exceptions.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…FlatList`

A default flat list given as a sequence throws `ArgumentNullException` that names the
parameter: `concat`, `transpose`, `insertManyAt` and `except` are the rows of one test, and
`addRange` and `for` have a test each. Rows and tests pin the order of the checks as well:
`insertManyAt` checks its source first and its index before the values, and `except` checks
the items before the array and also for an empty input, as the FSharp.Core functions do.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Comment thread tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListTupleTests.fs Outdated
xperiandri and others added 6 commits October 18, 2026 21:42
… `Array` module

ADR 0001, D1 promises every applicable function of the `Array` module and the wrappers of
the BCL members, so the functions that the rewrite had dropped come back:

- `sub list startIndex count` mirrors `Array.sub` (compiled `GetSubArray`): the same checks in
  the same order and the same exceptions. It wraps `ImmutableArray.Slice`, which copies
  unless the range is empty or the whole flat list.
- `sortRange`, `sortRangeWith` and `sortRangeWithComparer` wrap `Sort (index, count, comparer)`;
  `sortRange` compares with F# generic comparison. They are not stable and throw what the BCL
  member throws for a range out of range.
- `remove` and `removeWith` remove the first element that equals a value, `removeMany` and
  `removeManyWith` one element for each of the given values (`Remove` and `RemoveRange`); the
  input itself comes back when nothing matches. The remarks of `removeManyAt` and `filter`
  say that they are `RemoveRange (index, length)` and, with a negated predicate, `RemoveAll`.

The check of a sequence is one call, `checkNotNullOrDefaultSeq` of `Helper.fs`, which throws
`ArgumentNullException` for a null sequence and for a default flat list given as one. It
replaces `checkNotDefaultSeq` in `concat`, `transpose`, `except`, `insertManyAt`,
`FlatListBuilder.addRange` and the `yield!` and `for` of `flatList { }`, and it is used by
`removeMany` and `removeManyWith`. A null sequence reports the parameter of the function:
`values` for `insertManyAt` (`Array.insertManyAt` says `source`) and `addRange` (the BCL says
`items`), and `for` over a null sequence throws `ArgumentNullException` instead of
`NullReferenceException`.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…FlatList`

Tests of the restored functions and of the checks of a sequence, as rows:

- `sub` against `Array.sub` for every range of the contract inputs, and for the ranges that
  `Array.sub` rejects with the exception compared by `ExceptionAssert.Same` (its `array` is
  the `list` of `FlatList.sub`); a default flat list; the empty and the whole range.
- The three range sorts against `Array.sub`, `Array.sortWith` and `Array.concat` over the
  sorted part and the parts that stay, for ranges out of range and for ranges that need no
  sorting.
- The removals by value against oracles written by hand (the first equal element, one
  element for each value), in the default equality and with a case-insensitive comparer.
- A null sequence and a default flat list given as a sequence, in the tables of the inputs
  that a function rejects; `concat`, `transpose`, `except` and `insertManyAt` compare a null
  sequence with the `Array` oracle, and the order of the checks of `insertManyAt` is pinned.
- The pipelines of the tests that start with a flat list use LINQ `Select`, which reads the
  struct without a copy, and end with `Seq.toArray`.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
`remove`, `removeMany`, `removeManyWith` and `removeWith` are added. `sub` takes the meaning
of `Array.sub`, which changes its arguments, and `sortRange`, `sortRangeWith` and
`sortRangeWithComparer` stay. `removeAll`, `removeAllWith` and `removeRange` are removed with
their replacements named.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… `Array` module

The flat list parameter of the eight functions that came back has the name that the
neighbours use, so that named arguments and the documentation read alike in the module:

- `sub` and the range sorts (`sortRange`, `sortRangeWith`, `sortRangeWithComparer`) call it
  `array`, as the `Array` functions do. `Array.sub` names it `array`, so the check of a
  default flat list reports the same parameter, and the message of the check of the range
  names `array.Length`, word for word as `Array.sub` does.
- `remove`, `removeWith`, `removeMany` and `removeManyWith` call it `source`, as `removeAt`,
  `removeManyAt`, `insertAt`, `insertManyAt` and `updateAt` do.

The signature file, its parameter documentation and the names that the checks report follow.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…FlatList`

The tests follow the new parameter names: `array` for `sub` and the range sorts, `source` for
the removals by value.

`sub` is compared with `Array.sub` without a mapping of the parameter name now, so the
`subParamName` helper goes away. The rows of `removeMany` and `removeManyWith` for a default
flat list together with default items say "default source", as the rows of `insertManyAt`
do.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The changelog writes `sub array startIndex count`, with the name that the flat list parameter
has now.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

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

Covariantly widened default flat lists bypass validation and violate the documented exception contract.

1 open issue
1 resolved since last review

Review effort: Balanced

Comment thread src/FSharp.Collections.Immutable/Helper.fs Outdated
xperiandri and others added 5 commits October 18, 2026 23:56
… `Array` module

The gap analysis found members of `ImmutableArray<'T>` and of its builder that no function
wrapped, and the checks of a default flat list given as a sequence of a base type:

- `FlatList` gets `indexOfRange`, `indexOfRangeWith`, `lastIndexOfRange` and
  `lastIndexOfRangeWith` (the overloads of `IndexOf` and `LastIndexOf` with a range, which the
  `indexFrom` and `indexRange` families of 1.0.0 were), `replace` and `replaceWith`
  (`Replace`), and `binarySearchRange`, `binarySearchRangeWithComparer` and
  `binarySearchWithComparer` (`ImmutableArray.BinarySearch`). The range search without a
  comparer uses the cached comparer of F# generic comparison, as `binarySearch` does. The BCL
  calls the count of a range `length`, so the two range searches check the index and the count
  first, with the type and the message of the BCL, and name the parameter `count`.
- `FlatListBuilder` gets `indexOfRange`, `indexOfRangeWith`, `lastIndexOfRange`,
  `lastIndexOfRangeWith`, `insertManyAt`, `removeAll`, `removeMany`, `removeManyAt`,
  `removeManyWith`, `removeWith`, `replace`, `replaceWith`, `sortRange`, `sortRangeWith`,
  `sortRangeWithComparer` and `blit`. They return the builder, except `removeWith`, which
  reports the removal as `remove` does. `replace` and `replaceWith` leave the builder as it is
  when no element equals the old value, as the BCL does, and the documentation says so.
  `removeManyAt` and `blit` name the count `count` and the target `target` and `targetIndex`
  where the BCL says `length`, `destination` and `destinationIndex`, and they keep the order of
  the checks, the exceptions and the messages of the BCL. `insertManyAt` checks the index
  before the values, as `InsertRange` does.
- The parameter of a sequence is checked by `checkNotNullOrDefaultSeq`: a default
  `FlatList<string>` seen as a `seq<obj>`, by the covariance of `seq`, is an
  `ImmutableArray<string>`, and the type test for `ImmutableArray<obj>` did not see it, so it
  failed in the enumerator with `InvalidOperationException`. The type test stays the first
  step. For any other object the check asks for the generic type and reads `IsDefault`
  through reflection. The check is inline, because the public members of the computation
  expression call it, and an inline member cannot call an internal function. `ofSeq` takes the
  same path.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…FlatList`

Tests of the new functions, as rows:

- `FlatListSearchTests` runs the searches of a range of the flat list and of its builder on
  every valid range of the inputs, against a search written by hand, with the comparer that
  ignores the case for the functions with a comparer. The ranges that the BCL rejects run against
  the BCL member (`ExceptionAssert.Same`), with the parameter names of the functions. The binary
  searches of a range run on every valid range of sorted integers, in ascending and in descending
  order, and on strings that a comparer sorts; the ordinal order of the range search without a
  comparer, `null` for the default comparer, and a comparer that throws are pinned.
- `replace` and `replaceWith` against a first-occurrence oracle, the exception for a value that
  no element equals, and a new flat list for equal values.
- The functions of the builder against `Array` and the oracles: `insertManyAt`, `removeManyAt`,
  `removeMany`, `removeManyWith`, `removeWith`, `removeAll`, `replace`, `replaceWith`, the range
  sorts and `blit`. The ranges out of range run against the BCL member, with the parameter names
  of the functions, and a test proves that every function that changes the builder returns it.
- A default `FlatList<string>` seen as a `seq<obj>` runs through `ofSeq`, `insertManyAt`,
  `except`, `removeMany`, `removeManyWith`, the functions of the builder, `yield!` and `for`, with
  the parameter names, and a flat list of strings that is not default must pass.
- `sub` also compares the second line of the message, which names the arguments and the length.

The oracles move to `FlatListOracles` and the assertion of a rejected null to `FlatListAssert`, so
that the tests of the flat list and of the builder share them.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The changelog lists the new functions of `FlatList` and of `FlatListBuilder` and names the
replacement of each removed function: `indexOf` and its counterparts for the `index` and
`lastIndex` functions, `indexOfRange` and its counterparts for the `indexRange` and `indexFrom`
functions, `removeMany` for `removeAll` and `removeManyAt` for `removeRange`, with the change of
the default equality from F# structural equality to `EqualityComparer<'T>.Default`. The nested
`FlatList.Builder` module is now `FlatListBuilder`.

The entries were checked against the published 1.0.0. `Seq`, the module of conversions between
the collections, was never released and the `ImmutableArray` module alias cannot be used from
another assembly, so neither is listed as removed. `unfold` is new, so it is not listed as
changed, `sortWithComparer` and `tryTail` are not new, and `take` and `splitAt` threw in 1.0.0:
the entry says what changed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… `Array` module

The documentation of the searches of a range said that the start index must be less than the
length. That is how the BCL of .NET 8 and .NET 10 behaves: the forward search of .NET 11
accepts an empty range at a start index that equals the length. The remarks now say that the
function throws what the member throws, and name the versions of the behavior that they
describe.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…FlatList`

The comments of the rows of the searches of a range say which BCL they describe: the rows
with an empty range at a start index that equals the length throw on .NET 8 and .NET 10, the
targets of the project, and not in the forward search of .NET 11.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

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.

🔵 Needs a closer look

The broad breaking API rewrite needs human confirmation of compatibility and behavior across both target frameworks.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

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.

🔵 Needs a closer look

The broad breaking API migration requires human validation of compatibility and shared-storage behavior across both target frameworks.

No open issues 🎉

Review effort: Balanced

@xperiandri
xperiandri merged commit 58af8f1 into main Oct 18, 2026
9 checks passed
@xperiandri
xperiandri deleted the feat/flat-list branch October 18, 2026 22:34
xperiandri added a commit that referenced this pull request Oct 20, 2026
…` module (#26)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
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