Repository navigation
feat: IndexedList and IIndexedList modules with the vocabulary of the FSharp.Core List module - #31
Open
xperiandri wants to merge 3 commits into
Open
xperiandri wants to merge 3 commits into
xperiandri wants to merge 3 commits into
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Overflowing removal ranges violate the promised List behavior, and stability tests need stronger regression inputs.
2 open issues
What changed in this PR
Replaces the legacy immutable-list API with IndexedList and IIndexedList, continuing ADR 0001’s per-collection rewrite.
Changes:
- Adds List-compatible operations, stable sorting, and implementation-preserving interface operations.
- Adds Option/Tuple variants, builder helpers, and
indexedList { }. - Expands contract tests and updates migration documentation.
| File | Description |
|---|---|
| tests/FSharp.Collections.Immutable.Tests/IndexedList/IndexedListTupleTests.fs | Tests reference-tuple overrides. |
| tests/FSharp.Collections.Immutable.Tests/IndexedList/IndexedListTests.fs | Tests concrete-list semantics and extras. |
| tests/FSharp.Collections.Immutable.Tests/IndexedList/IndexedListOptionTests.fs | Tests option overrides. |
| tests/FSharp.Collections.Immutable.Tests/IndexedList/IndexedListOps.fs | Connects both modules to contracts. |
| tests/FSharp.Collections.Immutable.Tests/IndexedList/IndexedListContractTests.fs | Instantiates shared contracts. |
| tests/FSharp.Collections.Immutable.Tests/IndexedList/IndexedListComputationExpressionTests.fs | Tests computation expressions. |
| tests/FSharp.Collections.Immutable.Tests/IndexedList/IndexedListBuilderTests.fs | Tests builder operations and stability. |
| tests/FSharp.Collections.Immutable.Tests/IndexedList/IIndexedListTupleTests.fs | Tests interface tuple overrides. |
| tests/FSharp.Collections.Immutable.Tests/IndexedList/IIndexedListTests.fs | Tests implementation preservation and validation. |
| tests/FSharp.Collections.Immutable.Tests/IndexedList/IIndexedListOptionTests.fs | Tests interface option overrides. |
| tests/FSharp.Collections.Immutable.Tests/FSharp.Collections.Immutable.Tests.fsproj | Includes new test files. |
| tests/FSharp.Collections.Immutable.Tests/Contracts/StableSortContract.fs | Adds shared stability checks. |
| tests/FSharp.Collections.Immutable.Tests/Contracts/SeqCreationContract.fs | Adjusts null-array expectations. |
| tests/FSharp.Collections.Immutable.Tests/Contracts/IndexedContract.fs | Covers nonpositive removal counts. |
| src/FSharp.Collections.Immutable/IndexedSeq.fs | Removes legacy indexed-sequence bindings. |
| src/FSharp.Collections.Immutable/IndexedList/IndexedListTuple.fsi | Declares tuple overrides. |
| src/FSharp.Collections.Immutable/IndexedList/IndexedListTuple.fs | Implements tuple overrides. |
| src/FSharp.Collections.Immutable/IndexedList/IndexedListOption.fsi | Declares option overrides. |
| src/FSharp.Collections.Immutable/IndexedList/IndexedListOption.fs | Implements option overrides. |
| src/FSharp.Collections.Immutable/IndexedList/IndexedListCE.fs | Implements the computation-expression builder. |
| src/FSharp.Collections.Immutable/IndexedList/IndexedListBuilder.fs | Adds mutable builder helpers. |
| src/FSharp.Collections.Immutable/IndexedList/IIndexedListTuple.fsi | Declares interface tuple overrides. |
| src/FSharp.Collections.Immutable/IndexedList/IIndexedListTuple.fs | Implements interface tuple overrides. |
| src/FSharp.Collections.Immutable/IndexedList/IIndexedListOption.fsi | Declares interface option overrides. |
| src/FSharp.Collections.Immutable/IndexedList/IIndexedListOption.fs | Implements interface option overrides. |
| src/FSharp.Collections.Immutable/ImmutableList.fs | Removes the legacy module. |
| src/FSharp.Collections.Immutable/Helper.fs | Adds shared List checks and algorithms. |
| src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj | Registers replacement modules. |
| src/FSharp.Collections.Immutable/ComputationExpressions.fs | Exposes indexedList. |
| README.md | Updates the collection catalog. |
| docsSrc/Tutorials/Getting_Started.md | Migrates the list-construction example. |
| docsSrc/index.md | Updates the collection catalog. |
| docsSrc/Explanations/Background.md | Updates supported collection bindings. |
| CHANGELOG.md | Records additions and breaking changes. |
Review effort: Balanced
xperiandri
force-pushed
the
feat/indexed-list
branch
from
October 18, 2026 13:03
3a6e19b to
f5818ff
Compare
xperiandri
force-pushed
the
feat/flat-list
branch
2 times, most recently
from
October 18, 2026 15:35
2b5179a to
9d2526e
Compare
xperiandri
force-pushed
the
feat/indexed-list
branch
from
October 18, 2026 15:35
f5818ff to
56e9ddf
Compare
This was referenced Oct 18, 2026
xperiandri
force-pushed
the
feat/flat-list
branch
from
October 18, 2026 18:44
9d2526e to
047ee9b
Compare
xperiandri
added a commit
that referenced
this pull request
Oct 18, 2026
…lary of the FSharp.Core `List` module Follows the reworked ADR 0001 (D2, D5) and the review of #31: - `IndexedList`, `IIndexedList`, their `Option` and `Tuple` modules and `IndexedListBuilder` are the root modules of their files. The `IndexedList<'T>` abbreviation moves to `IndexedList/IndexedListTypes.fs`, which needs no signature file. - `binarySearch`, the sorts of both modules and the sort of the builder compare with the cached `DefaultComparer<'T>.Instance`, so a call no longer creates a comparer. - `IndexedList.rev` reverses a copy of the elements with `Array.Reverse` and builds the result with `ImmutableList.CreateRange`. At 100 000 elements, this is about 2.5 times faster than `ImmutableList.Reverse`, with the same allocation. `IIndexedList.rev` reverses its copy in place too and keeps the implementation of the input. - `removeManyAt` of both modules removes nothing when `index + count` overflows, as `List.removeManyAt` does. Before, the range went to `RemoveRange`, which threw. - `Helper.fs` gets the internal `Lengths` module for `Queue`, `Stack` and their interface modules: `queueUpTo` and `stackUpTo` count a queue or a stack only up to a limit. - Two crefs name the right members: `FastGenericComparer` is a method, and the cref of `ofBuilder` writes the nested generic builder as the documentation ID does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri
added a commit
that referenced
this pull request
Oct 18, 2026
…table sorting contract Follows the reworked ADR 0001 (D2, D8) and the review of #31: - Test methods carry no categories: the classes carry them. The classes with the tests of the `Cons` and `Nil` patterns carry `PatternMatchingTestCategory`. - `IndexedListOps` and `IIndexedListOps` are the root modules of their own files. - `Inputs.manyPairs` has 20 pairs with repeated keys. The sort of .NET orders up to 16 elements by insertion, which keeps equal keys in order, so the six `Inputs.pairs` could not tell a stable sort from an unstable one. The stable sorting contract and the stability test of the builder sorts use the new input, and an unstable sort such as `ImmutableList.Sort` fails them. - A contract test checks `removeManyAt` against FSharp.Core for ranges whose end overflows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri
force-pushed
the
feat/indexed-list
branch
from
October 18, 2026 18:49
56e9ddf to
e7a6257
Compare
xperiandri
added a commit
that referenced
this pull request
Oct 18, 2026
…FSharp.Core `List` module Follows the reworked ADR 0001 (D2, D6, D14) and the review of #31: - `StackCE.fsi` carries the signature and the XML documentation of the computation expression, compiled right before `StackCE.fs`. `stack` has its `val` and its documentation in `ComputationExpressions.fsi`, after `indexedList`; only its binding is left in `ComputationExpressions.fs`. - `stack { yield! ... }` rejects a `null` sequence, array or stack with `ArgumentNullException` for the parameter `items`. Before, `List<'T>.AddRange` threw for the parameter `collection`. - `sort`, `sortByDescending`, `sortDescending` and `sortWith` of `Stack` and `IStack` sort with the `Seq` functions of FSharp.Core, which share the stable sort of `List`, so that a comparison or a projection that throws throws what `List` throws. LINQ already wrapped the exception of a comparison in an `InvalidOperationException`, as `Array.Sort` does. `sortByDescending` differed: `OrderByDescending` computes the keys before it sorts, while `List.sortByDescending` calls the projection in the comparison. `sortBy` and `sortWithComparer` keep LINQ. - At 10 000 elements, `Stack.sortWith` takes about 0.83 ms instead of 1.16 ms and allocates 651 704 bytes per call instead of 440 384: the sorted sequence is copied into an array that the stack is pushed from. Ending with `Enumerable.ToArray` allocates less than `Seq.toArray` (783 072 bytes). - The remarks of `IStack.append` state the rule of D14: the collection that receives the elements keeps its implementation, here the second stack. They refer to `IIndexedList.append`, which keeps the implementation of the first list, with a cref. - 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 Sonnet 5.5 <noreply@anthropic.com>
xperiandri
added a commit
that referenced
this pull request
Oct 18, 2026
…FSharp.Core `List` module Follows the reworked ADR 0001 (D2, D6, D14) and the review of #31: - `StackCE.fsi` carries the signature and the XML documentation of the computation expression, compiled right before `StackCE.fs`. `stack` has its `val` and its documentation in `ComputationExpressions.fsi`, after `indexedList`; only its binding is left in `ComputationExpressions.fs`. - `stack { yield! ... }` rejects a `null` sequence, array or stack with `ArgumentNullException` for the parameter `items`. Before, `List<'T>.AddRange` threw for the parameter `collection`. - `sort`, `sortByDescending`, `sortDescending` and `sortWith` of `Stack` and `IStack` sort with the `Seq` functions of FSharp.Core, which share the stable sort of `List`, so that a comparison or a projection that throws throws what `List` throws. LINQ already wrapped the exception of a comparison in an `InvalidOperationException`, as `Array.Sort` does. `sortByDescending` differed: `OrderByDescending` computes the keys before it sorts, while `List.sortByDescending` calls the projection in the comparison. `sortBy` and `sortWithComparer` keep LINQ. - At 10 000 elements, `Stack.sortWith` takes about 0.83 ms instead of 1.16 ms and allocates 651 704 bytes per call instead of 440 384: the sorted sequence is copied into an array that the stack is pushed from. Ending with `Enumerable.ToArray` allocates less than `Seq.toArray` (783 072 bytes). - The remarks of `IStack.append` state the rule of D14: the collection that receives the elements keeps its implementation, here the second stack. They refer to `IIndexedList.append`, which keeps the implementation of the first list, with a cref. - 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 Sonnet 5.5 <noreply@anthropic.com>
xperiandri
added a commit
that referenced
this pull request
Oct 18, 2026
…lary of the FSharp.Core `List` module Follows the reworked ADR 0001 (D2, D5) and the review of #31: - `IndexedList`, `IIndexedList`, their `Option` and `Tuple` modules and `IndexedListBuilder` are the root modules of their files. The `IndexedList<'T>` abbreviation moves to `IndexedList/IndexedListTypes.fs`, which needs no signature file. - `binarySearch`, the sorts of both modules and the sort of the builder compare with the cached `DefaultComparer<'T>.Instance`, so a call no longer creates a comparer. - `IndexedList.rev` reverses a copy of the elements with `Array.Reverse` and builds the result with `ImmutableList.CreateRange`. At 100 000 elements, this is about 2.5 times faster than `ImmutableList.Reverse`, with the same allocation. `IIndexedList.rev` reverses its copy in place too and keeps the implementation of the input. - `removeManyAt` of both modules removes nothing when `index + count` overflows, as `List.removeManyAt` does. Before, the range went to `RemoveRange`, which threw. - `Helper.fs` gets the internal `Lengths` module for `Queue`, `Stack` and their interface modules: `queueUpTo` and `stackUpTo` count a queue or a stack only up to a limit. - Two crefs name the right members: `FastGenericComparer` is a method, and the cref of `ofBuilder` writes the nested generic builder as the documentation ID does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri
added a commit
that referenced
this pull request
Oct 18, 2026
…table sorting contract Follows the reworked ADR 0001 (D2, D8) and the review of #31: - Test methods carry no categories: the classes carry them. The classes with the tests of the `Cons` and `Nil` patterns carry `PatternMatchingTestCategory`. - `IndexedListOps` and `IIndexedListOps` are the root modules of their own files. - `Inputs.manyPairs` has 20 pairs with repeated keys. The sort of .NET orders up to 16 elements by insertion, which keeps equal keys in order, so the six `Inputs.pairs` could not tell a stable sort from an unstable one. The stable sorting contract and the stability test of the builder sorts use the new input, and an unstable sort such as `ImmutableList.Sort` fails them. - A contract test checks `removeManyAt` against FSharp.Core for ranges whose end overflows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri
force-pushed
the
feat/indexed-list
branch
from
October 18, 2026 20:12
430f3fb to
53a3555
Compare
xperiandri
added a commit
that referenced
this pull request
Oct 18, 2026
…FSharp.Core `List` module Follows the reworked ADR 0001 (D2, D6, D14) and the review of #31: - `StackCE.fsi` carries the signature and the XML documentation of the computation expression, compiled right before `StackCE.fs`. `stack` has its `val` and its documentation in `ComputationExpressions.fsi`, after `indexedList`; only its binding is left in `ComputationExpressions.fs`. - `stack { yield! ... }` rejects a `null` sequence, array or stack with `ArgumentNullException` for the parameter `items`. Before, `List<'T>.AddRange` threw for the parameter `collection`. - `sort`, `sortByDescending`, `sortDescending` and `sortWith` of `Stack` and `IStack` sort with the `Seq` functions of FSharp.Core, which share the stable sort of `List`, so that a comparison or a projection that throws throws what `List` throws. LINQ already wrapped the exception of a comparison in an `InvalidOperationException`, as `Array.Sort` does. `sortByDescending` differed: `OrderByDescending` computes the keys before it sorts, while `List.sortByDescending` calls the projection in the comparison. `sortBy` and `sortWithComparer` keep LINQ. - At 10 000 elements, `Stack.sortWith` takes about 0.83 ms instead of 1.16 ms and allocates 651 704 bytes per call instead of 440 384: the sorted sequence is copied into an array that the stack is pushed from. Ending with `Enumerable.ToArray` allocates less than `Seq.toArray` (783 072 bytes). - The remarks of `IStack.append` state the rule of D14: the collection that receives the elements keeps its implementation, here the second stack. They refer to `IIndexedList.append`, which keeps the implementation of the first list, with a cref. - 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 Sonnet 5.5 <noreply@anthropic.com>
xperiandri
added a commit
that referenced
this pull request
Oct 18, 2026
…lary of the FSharp.Core `List` module Follows the reworked ADR 0001 (D2, D5) and the review of #31: - `IndexedList`, `IIndexedList`, their `Option` and `Tuple` modules and `IndexedListBuilder` are the root modules of their files. The `IndexedList<'T>` abbreviation moves to `IndexedList/IndexedListTypes.fs`, which needs no signature file. - `binarySearch`, the sorts of both modules and the sort of the builder compare with the cached `DefaultComparer<'T>.Instance`, so a call no longer creates a comparer. - `IndexedList.rev` reverses a copy of the elements with `Array.Reverse` and builds the result with `ImmutableList.CreateRange`. At 100 000 elements, this is about 2.5 times faster than `ImmutableList.Reverse`, with the same allocation. `IIndexedList.rev` reverses its copy in place too and keeps the implementation of the input. - `removeManyAt` of both modules removes nothing when `index + count` overflows, as `List.removeManyAt` does. Before, the range went to `RemoveRange`, which threw. - `Helper.fs` gets the internal `Lengths` module for `Queue`, `Stack` and their interface modules: `queueUpTo` and `stackUpTo` count a queue or a stack only up to a limit. - Two crefs name the right members: `FastGenericComparer` is a method, and the cref of `ofBuilder` writes the nested generic builder as the documentation ID does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri
added a commit
that referenced
this pull request
Oct 19, 2026
…FSharp.Core `List` module Follows the reworked ADR 0001 (D2, D6, D14) and the review of #31: - `StackCE.fsi` carries the signature and the XML documentation of the computation expression, compiled right before `StackCE.fs`. `stack` has its `val` and its documentation in `ComputationExpressions.fsi`, after `indexedList`; only its binding is left in `ComputationExpressions.fs`. - `stack { yield! ... }` rejects a `null` sequence, array or stack with `ArgumentNullException` for the parameter `items`. Before, `List<'T>.AddRange` threw for the parameter `collection`. - `sort`, `sortByDescending`, `sortDescending` and `sortWith` of `Stack` and `IStack` sort with the `Seq` functions of FSharp.Core, which share the stable sort of `List`, so that a comparison or a projection that throws throws what `List` throws. LINQ already wrapped the exception of a comparison in an `InvalidOperationException`, as `Array.Sort` does. `sortByDescending` differed: `OrderByDescending` computes the keys before it sorts, while `List.sortByDescending` calls the projection in the comparison. `sortBy` and `sortWithComparer` keep LINQ. - At 10 000 elements, `Stack.sortWith` takes about 0.83 ms instead of 1.16 ms and allocates 651 704 bytes per call instead of 440 384: the sorted sequence is copied into an array that the stack is pushed from. Ending with `Enumerable.ToArray` allocates less than `Seq.toArray` (783 072 bytes). - The remarks of `IStack.append` state the rule of D14: the collection that receives the elements keeps its implementation, here the second stack. They refer to `IIndexedList.append`, which keeps the implementation of the first list, with a cref. - 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 Sonnet 5.5 <noreply@anthropic.com>
xperiandri
added a commit
that referenced
this pull request
Oct 19, 2026
…lary of the FSharp.Core `List` module Follows the reworked ADR 0001 (D2, D5) and the review of #31: - `IndexedList`, `IIndexedList`, their `Option` and `Tuple` modules and `IndexedListBuilder` are the root modules of their files. The `IndexedList<'T>` abbreviation moves to `IndexedList/IndexedListTypes.fs`, which needs no signature file. - `binarySearch`, the sorts of both modules and the sort of the builder compare with the cached `DefaultComparer<'T>.Instance`, so a call no longer creates a comparer. - `IndexedList.rev` reverses a copy of the elements with `Array.Reverse` and builds the result with `ImmutableList.CreateRange`. At 100 000 elements, this is about 2.5 times faster than `ImmutableList.Reverse`, with the same allocation. `IIndexedList.rev` reverses its copy in place too and keeps the implementation of the input. - `removeManyAt` of both modules removes nothing when `index + count` overflows, as `List.removeManyAt` does. Before, the range went to `RemoveRange`, which threw. - `Helper.fs` gets the internal `Lengths` module for `Queue`, `Stack` and their interface modules: `queueUpTo` and `stackUpTo` count a queue or a stack only up to a limit. - Two crefs name the right members: `FastGenericComparer` is a method, and the cref of `ofBuilder` writes the nested generic builder as the documentation ID does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri
added a commit
that referenced
this pull request
Oct 19, 2026
…table sorting contract Follows the reworked ADR 0001 (D2, D8) and the review of #31: - Test methods carry no categories: the classes carry them. The classes with the tests of the `Cons` and `Nil` patterns carry `PatternMatchingTestCategory`. - `IndexedListOps` and `IIndexedListOps` are the root modules of their own files. - `Inputs.manyPairs` has 20 pairs with repeated keys. The sort of .NET orders up to 16 elements by insertion, which keeps equal keys in order, so the six `Inputs.pairs` could not tell a stable sort from an unstable one. The stable sorting contract and the stability test of the builder sorts use the new input, and an unstable sort such as `ImmutableList.Sort` fails them. - A contract test checks `removeManyAt` against FSharp.Core for ranges whose end overflows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri
force-pushed
the
feat/indexed-list
branch
from
October 19, 2026 14:49
67c2c1c to
4a66e46
Compare
xperiandri
added a commit
that referenced
this pull request
Oct 19, 2026
…FSharp.Core `List` module Follows the reworked ADR 0001 (D2, D6, D14) and the review of #31: - `StackCE.fsi` carries the signature and the XML documentation of the computation expression, compiled right before `StackCE.fs`. `stack` has its `val` and its documentation in `ComputationExpressions.fsi`, after `indexedList`; only its binding is left in `ComputationExpressions.fs`. - `stack { yield! ... }` rejects a `null` sequence, array or stack with `ArgumentNullException` for the parameter `items`. Before, `List<'T>.AddRange` threw for the parameter `collection`. - `sort`, `sortByDescending`, `sortDescending` and `sortWith` of `Stack` and `IStack` sort with the `Seq` functions of FSharp.Core, which share the stable sort of `List`, so that a comparison or a projection that throws throws what `List` throws. LINQ already wrapped the exception of a comparison in an `InvalidOperationException`, as `Array.Sort` does. `sortByDescending` differed: `OrderByDescending` computes the keys before it sorts, while `List.sortByDescending` calls the projection in the comparison. `sortBy` and `sortWithComparer` keep LINQ. - At 10 000 elements, `Stack.sortWith` takes about 0.83 ms instead of 1.16 ms and allocates 651 704 bytes per call instead of 440 384: the sorted sequence is copied into an array that the stack is pushed from. Ending with `Enumerable.ToArray` allocates less than `Seq.toArray` (783 072 bytes). - The remarks of `IStack.append` state the rule of D14: the collection that receives the elements keeps its implementation, here the second stack. They refer to `IIndexedList.append`, which keeps the implementation of the first list, with a cref. - 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 Sonnet 5.5 <noreply@anthropic.com>
xperiandri
added a commit
that referenced
this pull request
Oct 19, 2026
…lary of the FSharp.Core `List` module Follows the reworked ADR 0001 (D2, D5) and the review of #31: - `IndexedList`, `IIndexedList`, their `Option` and `Tuple` modules and `IndexedListBuilder` are the root modules of their files. The `IndexedList<'T>` abbreviation moves to `IndexedList/IndexedListTypes.fs`, which needs no signature file. - `binarySearch`, the sorts of both modules and the sort of the builder compare with the cached `DefaultComparer<'T>.Instance`, so a call no longer creates a comparer. - `IndexedList.rev` reverses a copy of the elements with `Array.Reverse` and builds the result with `ImmutableList.CreateRange`. At 100 000 elements, this is about 2.5 times faster than `ImmutableList.Reverse`, with the same allocation. `IIndexedList.rev` reverses its copy in place too and keeps the implementation of the input. - `removeManyAt` of both modules removes nothing when `index + count` overflows, as `List.removeManyAt` does. Before, the range went to `RemoveRange`, which threw. - `Helper.fs` gets the internal `Lengths` module for `Queue`, `Stack` and their interface modules: `queueUpTo` and `stackUpTo` count a queue or a stack only up to a limit. - Two crefs name the right members: `FastGenericComparer` is a method, and the cref of `ofBuilder` writes the nested generic builder as the documentation ID does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri
force-pushed
the
feat/indexed-list
branch
from
October 19, 2026 15:55
4a66e46 to
aa81bb5
Compare
xperiandri
added a commit
that referenced
this pull request
Oct 19, 2026
…table sorting contract Follows the reworked ADR 0001 (D2, D8) and the review of #31: - Test methods carry no categories: the classes carry them. The classes with the tests of the `Cons` and `Nil` patterns carry `PatternMatchingTestCategory`. - `IndexedListOps` and `IIndexedListOps` are the root modules of their own files. - `Inputs.manyPairs` has 20 pairs with repeated keys. The sort of .NET orders up to 16 elements by insertion, which keeps equal keys in order, so the six `Inputs.pairs` could not tell a stable sort from an unstable one. The stable sorting contract and the stability test of the builder sorts use the new input, and an unstable sort such as `ImmutableList.Sort` fails them. - A contract test checks `removeManyAt` against FSharp.Core for ranges whose end overflows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri
added a commit
that referenced
this pull request
Oct 19, 2026
…FSharp.Core `List` module Follows the reworked ADR 0001 (D2, D6, D14) and the review of #31: - `StackCE.fsi` carries the signature and the XML documentation of the computation expression, compiled right before `StackCE.fs`. `stack` has its `val` and its documentation in `ComputationExpressions.fsi`, after `indexedList`; only its binding is left in `ComputationExpressions.fs`. - `stack { yield! ... }` rejects a `null` sequence, array or stack with `ArgumentNullException` for the parameter `items`. Before, `List<'T>.AddRange` threw for the parameter `collection`. - `sort`, `sortByDescending`, `sortDescending` and `sortWith` of `Stack` and `IStack` sort with the `Seq` functions of FSharp.Core, which share the stable sort of `List`, so that a comparison or a projection that throws throws what `List` throws. LINQ already wrapped the exception of a comparison in an `InvalidOperationException`, as `Array.Sort` does. `sortByDescending` differed: `OrderByDescending` computes the keys before it sorts, while `List.sortByDescending` calls the projection in the comparison. `sortBy` and `sortWithComparer` keep LINQ. - At 10 000 elements, `Stack.sortWith` takes about 0.83 ms instead of 1.16 ms and allocates 651 704 bytes per call instead of 440 384: the sorted sequence is copied into an array that the stack is pushed from. Ending with `Enumerable.ToArray` allocates less than `Seq.toArray` (783 072 bytes). - The remarks of `IStack.append` state the rule of D14: the collection that receives the elements keeps its implementation, here the second stack. They refer to `IIndexedList.append`, which keeps the implementation of the first list, with a cref. - 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 Sonnet 5.5 <noreply@anthropic.com>
xperiandri
added a commit
that referenced
this pull request
Oct 19, 2026
…lary of the FSharp.Core `List` module Follows the reworked ADR 0001 (D2, D5) and the review of #31: - `IndexedList`, `IIndexedList`, their `Option` and `Tuple` modules and `IndexedListBuilder` are the root modules of their files. The `IndexedList<'T>` abbreviation moves to `IndexedList/IndexedListTypes.fs`, which needs no signature file. - `binarySearch`, the sorts of both modules and the sort of the builder compare with the cached `DefaultComparer<'T>.Instance`, so a call no longer creates a comparer. - `IndexedList.rev` reverses a copy of the elements with `Array.Reverse` and builds the result with `ImmutableList.CreateRange`. At 100 000 elements, this is about 2.5 times faster than `ImmutableList.Reverse`, with the same allocation. `IIndexedList.rev` reverses its copy in place too and keeps the implementation of the input. - `removeManyAt` of both modules removes nothing when `index + count` overflows, as `List.removeManyAt` does. Before, the range went to `RemoveRange`, which threw. - `Helper.fs` gets the internal `Lengths` module for `Queue`, `Stack` and their interface modules: `queueUpTo` and `stackUpTo` count a queue or a stack only up to a limit. - Two crefs name the right members: `FastGenericComparer` is a method, and the cref of `ofBuilder` writes the nested generic builder as the documentation ID does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri
force-pushed
the
feat/indexed-list
branch
from
October 19, 2026 17:31
aa81bb5 to
6ce859c
Compare
xperiandri
added a commit
that referenced
this pull request
Oct 19, 2026
…table sorting contract Follows the reworked ADR 0001 (D2, D8) and the review of #31: - Test methods carry no categories: the classes carry them. The classes with the tests of the `Cons` and `Nil` patterns carry `PatternMatchingTestCategory`. - `IndexedListOps` and `IIndexedListOps` are the root modules of their own files. - `Inputs.manyPairs` has 20 pairs with repeated keys. The sort of .NET orders up to 16 elements by insertion, which keeps equal keys in order, so the six `Inputs.pairs` could not tell a stable sort from an unstable one. The stable sorting contract and the stability test of the builder sorts use the new input, and an unstable sort such as `ImmutableList.Sort` fails them. - A contract test checks `removeManyAt` against FSharp.Core for ranges whose end overflows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri
added a commit
that referenced
this pull request
Oct 19, 2026
…FSharp.Core `List` module Follows the reworked ADR 0001 (D2, D6, D14) and the review of #31: - `StackCE.fsi` carries the signature and the XML documentation of the computation expression, compiled right before `StackCE.fs`. `stack` has its `val` and its documentation in `ComputationExpressions.fsi`, after `indexedList`; only its binding is left in `ComputationExpressions.fs`. - `stack { yield! ... }` rejects a `null` sequence, array or stack with `ArgumentNullException` for the parameter `items`. Before, `List<'T>.AddRange` threw for the parameter `collection`. - `sort`, `sortByDescending`, `sortDescending` and `sortWith` of `Stack` and `IStack` sort with the `Seq` functions of FSharp.Core, which share the stable sort of `List`, so that a comparison or a projection that throws throws what `List` throws. LINQ already wrapped the exception of a comparison in an `InvalidOperationException`, as `Array.Sort` does. `sortByDescending` differed: `OrderByDescending` computes the keys before it sorts, while `List.sortByDescending` calls the projection in the comparison. `sortBy` and `sortWithComparer` keep LINQ. - At 10 000 elements, `Stack.sortWith` takes about 0.83 ms instead of 1.16 ms and allocates 651 704 bytes per call instead of 440 384: the sorted sequence is copied into an array that the stack is pushed from. Ending with `Enumerable.ToArray` allocates less than `Seq.toArray` (783 072 bytes). - The remarks of `IStack.append` state the rule of D14: the collection that receives the elements keeps its implementation, here the second stack. They refer to `IIndexedList.append`, which keeps the implementation of the first list, with a cref. - 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 Sonnet 5.5 <noreply@anthropic.com>
xperiandri
added a commit
that referenced
this pull request
Oct 19, 2026
…lary of the FSharp.Core `List` module Follows the reworked ADR 0001 (D2, D5) and the review of #31: - `IndexedList`, `IIndexedList`, their `Option` and `Tuple` modules and `IndexedListBuilder` are the root modules of their files. The `IndexedList<'T>` abbreviation moves to `IndexedList/IndexedListTypes.fs`, which needs no signature file. - `binarySearch`, the sorts of both modules and the sort of the builder compare with the cached `DefaultComparer<'T>.Instance`, so a call no longer creates a comparer. - `IndexedList.rev` reverses a copy of the elements with `Array.Reverse` and builds the result with `ImmutableList.CreateRange`. At 100 000 elements, this is about 2.5 times faster than `ImmutableList.Reverse`, with the same allocation. `IIndexedList.rev` reverses its copy in place too and keeps the implementation of the input. - `removeManyAt` of both modules removes nothing when `index + count` overflows, as `List.removeManyAt` does. Before, the range went to `RemoveRange`, which threw. - `Helper.fs` gets the internal `Lengths` module for `Queue`, `Stack` and their interface modules: `queueUpTo` and `stackUpTo` count a queue or a stack only up to a limit. - Two crefs name the right members: `FastGenericComparer` is a method, and the cref of `ofBuilder` writes the nested generic builder as the documentation ID does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri
added a commit
that referenced
this pull request
Oct 19, 2026
…table sorting contract Follows the reworked ADR 0001 (D2, D8) and the review of #31: - Test methods carry no categories: the classes carry them. The classes with the tests of the `Cons` and `Nil` patterns carry `PatternMatchingTestCategory`. - `IndexedListOps` and `IIndexedListOps` are the root modules of their own files. - `Inputs.manyPairs` has 20 pairs with repeated keys. The sort of .NET orders up to 16 elements by insertion, which keeps equal keys in order, so the six `Inputs.pairs` could not tell a stable sort from an unstable one. The stable sorting contract and the stability test of the builder sorts use the new input, and an unstable sort such as `ImmutableList.Sort` fails them. - A contract test checks `removeManyAt` against FSharp.Core for ranges whose end overflows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri
force-pushed
the
feat/indexed-list
branch
from
October 19, 2026 17:40
6ce859c to
d416a34
Compare
xperiandri
added a commit
that referenced
this pull request
Oct 19, 2026
…FSharp.Core `List` module Follows the reworked ADR 0001 (D2, D6, D14) and the review of #31: - `StackCE.fsi` carries the signature and the XML documentation of the computation expression, compiled right before `StackCE.fs`. `stack` has its `val` and its documentation in `ComputationExpressions.fsi`, after `indexedList`; only its binding is left in `ComputationExpressions.fs`. - `stack { yield! ... }` rejects a `null` sequence, array or stack with `ArgumentNullException` for the parameter `items`. Before, `List<'T>.AddRange` threw for the parameter `collection`. - `sort`, `sortByDescending`, `sortDescending` and `sortWith` of `Stack` and `IStack` sort with the `Seq` functions of FSharp.Core, which share the stable sort of `List`, so that a comparison or a projection that throws throws what `List` throws. LINQ already wrapped the exception of a comparison in an `InvalidOperationException`, as `Array.Sort` does. `sortByDescending` differed: `OrderByDescending` computes the keys before it sorts, while `List.sortByDescending` calls the projection in the comparison. `sortBy` and `sortWithComparer` keep LINQ. - At 10 000 elements, `Stack.sortWith` takes about 0.83 ms instead of 1.16 ms and allocates 651 704 bytes per call instead of 440 384: the sorted sequence is copied into an array that the stack is pushed from. Ending with `Enumerable.ToArray` allocates less than `Seq.toArray` (783 072 bytes). - The remarks of `IStack.append` state the rule of D14: the collection that receives the elements keeps its implementation, here the second stack. They refer to `IIndexedList.append`, which keeps the implementation of the first list, with a cref. - 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 Sonnet 5.5 <noreply@anthropic.com>
xperiandri
commented
Oct 19, 2026
xperiandri
added a commit
that referenced
this pull request
Oct 19, 2026
…FSharp.Core `List` module Follows the reworked ADR 0001 (D2, D6, D14) and the review of #31: - `StackCE.fsi` carries the signature and the XML documentation of the computation expression, compiled right before `StackCE.fs`. `stack` has its `val` and its documentation in `ComputationExpressions.fsi`, after `indexedList`; only its binding is left in `ComputationExpressions.fs`. - `stack { yield! ... }` rejects a `null` sequence, array or stack with `ArgumentNullException` for the parameter `items`. Before, `List<'T>.AddRange` threw for the parameter `collection`. - `sort`, `sortByDescending`, `sortDescending` and `sortWith` of `Stack` and `IStack` sort with the `Seq` functions of FSharp.Core, which share the stable sort of `List`, so that a comparison or a projection that throws throws what `List` throws. LINQ already wrapped the exception of a comparison in an `InvalidOperationException`, as `Array.Sort` does. `sortByDescending` differed: `OrderByDescending` computes the keys before it sorts, while `List.sortByDescending` calls the projection in the comparison. `sortBy` and `sortWithComparer` keep LINQ. - At 10 000 elements, `Stack.sortWith` takes about 0.83 ms instead of 1.16 ms and allocates 651 704 bytes per call instead of 440 384: the sorted sequence is copied into an array that the stack is pushed from. Ending with `Enumerable.ToArray` allocates less than `Seq.toArray` (783 072 bytes). - The remarks of `IStack.append` state the rule of D14: the collection that receives the elements keeps its implementation, here the second stack. They refer to `IIndexedList.append`, which keeps the implementation of the first list, with a cref. - 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 Sonnet 5.5 <noreply@anthropic.com>
3 of 6 tasks
… the FSharp.Core `List` module
`IndexedList` (`ImmutableList<'T>`) replaces the `ImmutableList` module. It has every function of `List` that applies to
an immutable list, with the parameter names, the checks and the exceptions of `List` and with stable sorts, plus the
extras of ADR 0001: `add`, `binarySearch`, `blit`, `cons`, `uncons`, `tryUncons`, the struct partial patterns `Cons` and
`Nil`, `indexOf`, `lastIndexOf`, `skipLast`, `takeLast`, `tryTail` and `sortWithComparer`. `IIndexedList` has the same
functions for `IImmutableList<'T>` without creation, builder, `binarySearch` and `blit`, plus `clear`; a result with the
element type of the input keeps the implementation of the input. The `Option` and `Tuple` namespaces repeat the
functions that use value options or struct tuples, `IndexedListBuilder` works on the builder of an indexed list, and
`indexedList { }` builds one.
`Helper.fs` gains three internal modules for the modules that mirror `List`: `ListChecks` throws the exceptions of `List`
for lengths and indices with the parameter names that the caller passes, `ListArrays` runs the functions of two and three
lists over arrays and checks the lengths after the common prefix, as `List` does, and `Snapshot` copies a collection into
an array.
The `ImmutableList` and `IndexedSeq` modules, the `ReadOnlyList` alias and the `IIndexedSeq<'T>` abbreviation are removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…orting contract
`StableSortContract` checks that `sort`, `sortBy`, `sortByDescending`, `sortDescending`, `sortWith` and
`sortWithComparer` keep the order of equal elements, as the `List` functions do. The pairs of `Inputs.pairs` show it for
the sorts by key; `sort` and `sortDescending` run on `KeyedItem`, whose comparison looks at a key only.
The sequence-like, creation, index-based and stable sorting contracts run for `IndexedList`, and all of them but the
creation contract for `IIndexedList`. Further tests cover null inputs, shared trees, `cons`, `uncons` and the patterns,
`binarySearch`, `blit`, the order in which the functions of two lists call their callbacks before they throw, the complete
messages of the `List` exceptions, the implementation that `IIndexedList` keeps, the builder functions,
`indexedList { }` and the `Option` and `Tuple` namespaces.
Two shared contracts now serve the modules that mirror `List` as well. `ofArray` of a null array is compared with
`Array.toList`, because `List.ofArray` reads a null array without a check. `removeManyAt` of a count that is not positive
is compared with what the mirrored function returns or throws, because `List.removeManyAt` removes nothing where
`Array.removeManyAt` copies by the count.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ction table
The changelog lists the new functions, the renamed `ImmutableList` module and functions, the breaking changes of the
`List` semantics and the removed functions and modules. The collection tables name `IndexedList` with `indexedList { }`
and `IIndexedList` instead of `ImmutableList` and `IIndexedSeq`, and the tutorial creates an `IndexedList`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri
force-pushed
the
feat/indexed-list
branch
from
October 20, 2026 16:27
178bba1 to
53b8a6b
Compare
4 of 6 tasks
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Proposed Changes
Rewrites the legacy
ImmutableListmodule asIndexedList(ImmutableList<'T>) and adds the interface moduleIIndexedList(IImmutableList<'T>) after ADR 0001 (#23). It builds on #26 (FlatList), which is merged intomain.IndexedList,IIndexedList, theirOptionandTuplemodules andIndexedListBuilderare the root modules of their files, such asmodule FSharp.Collections.Immutable.IndexedList. TheIndexedList<'T>andIIndexedList<'T>abbreviations live inIndexedList/IndexedListTypes.fs, which needs no signature file.IndexedListBuilder.fsiandIndexedListCE.fsiincluded.indexedListhas itsvalinComputationExpressions.fsi, afterflatListunder theCreationbanner.IndexedList/IndexedList.fsiandIndexedList.fs: 162 functions, in the canonical category order. They are theIndexedListcolumn of ADR Appendix A, with exactly the parameter names of the FSharp.CoreListmodule, so the exceptions name the same parameters asList.ImmutableListcome first (D5):ConvertAll,FindAll,FindIndex,FindLastIndex,TrueForAll,Exists,GetRange,Insert,InsertRange,RemoveAt,RemoveRange,SetItem,IndexOf,LastIndexOf,BinarySearch, and nowRemove,RemoveRange (items),ReplaceandReverse (index, count).Arrayfunction and builds a balanced tree withImmutableList.CreateRange. The section on cost below lists what takes another way and why:exists2,forall2andcompareWithwalk the lists and stop at the pair that decides,max,maxBy,minandminByare inline,collectandconcatjoin long indexed lists to the result instead of copying them, and the range functions copy a range withCopyTo.revtakes the faster path that the maintainer chose overImmutableList.Reverse(D5). It copies the elements to an array, reverses it withArray.Reverseand builds the result withImmutableList.CreateRange. At 100 000 integers it takes about 3 ms whereImmutableList.Reverse, which the 1.0.0reversecalled, takes about 23 ms (about 8 times faster; 40 ms against 265 ms at 1 000 000), and it allocates 5.2 MB whereReverseallocates 4.8 MB, because of the copy of the elements. The numbers are the median of 15 rounds in Release on .NET 10, for a list fromCreateRangeand for one built withAdd, run on the 1.0.0 assembly and on this branch.List.sort*is.ImmutableList.Sortis unstable, so it is not used.sortandsortWithsort with the FSharp.CoreSeqsorts, which share the stable sort ofList: they make the same comparisons, in the sameArray.Sort.sortDescendingandsortByDescendingcallSeq.sortWiththrough the internalDescendingSortsmodule (Helper/DescendingSorts.fs), with the cached comparer reversed.Seq.sortDescendingandSeq.sortByDescendingare inline: inside a generic function they compare with the generic operator, which boxes the elements, or the keys, in every comparison and allocates 7.5 MB at 10 000 integers. The comparisons, their order and the exceptions stay those ofList; only the boxes go.sortByandsortWithComparersort with LINQOrderBy/Order, which compute the keys first, asList.sortBydoes.sortRange,sortRangeWithandsortRangeWithComparersort a copy of the range with LINQOrderand put it back withRemoveRangeandInsertRange, so they are stable too.ImmutableList.Sort (index, count, comparer)is unstable and copies the whole list.binarySearchcompare with the cachedDefaultComparer<'T>.Instanceof feat:FlatListmodule with the vocabulary of the FSharp.CoreArraymodule #26, so a call does not create a comparer.add,cons,uncons,tryUncons,tryTail,skipLast,takeLast,build,update,blit,binarySearch,indexOf/indexOfWith/lastIndexOf/lastIndexOfWith,sortWithComparer, the span constructors and theCons/Nilpatterns.ImmutableList, from the API gap analysis of the modules (20 functions):EqualityComparer<'T>.Defaultor a given equality comparer:remove,removeWith,removeMany,removeManyWith,replace,replaceWith. Nothing to remove returns the input itself, andreplacethrowsArgumentExceptionfor a value that no element equals, as the BCL does.sub(it throws whatArray.subthrows, asFlatList.subdoes),revRange,indexOfRange,indexOfRangeWith,lastIndexOfRange,lastIndexOfRangeWith,tryFindIndexRangeandtryFindIndexBackRange. The searches return indices of the whole indexed list.binarySearchWithComparer,binarySearchRangeandbinarySearchRangeWithComparer, and the stablesortRange*.IIndexedList.fsiandIIndexedList.fs: 136 functions overIImmutableList<'T>.Clear ()plusAddRange, or the member of the interface for an edit of one element.revreverses its copy in place, asIndexedList.revdoes, before it rebuilds.IndexedList. So areconcatandtranspose, because their inputs can have different implementations.IndexedList.exists2,forall2andcompareWithhand two indexed lists toIndexedList, the searches from the end search an indexed list from its end, andskip,takeand the other range functions copy the range of an indexed list. The sequence-like contract runs over an implementation that is not an indexed list as well, to cover the general way.remove,removeWith,removeMany,removeManyWith,replaceandreplaceWithcall the members of the interface, so they keep the implementation too. The four searches in a range call the members of the interface after the module has checked the range.IImmutableListimplementation proves that the module keeps it.Listfunction throws: the type, the message and the parameter name. This includes the quirks ofList:exists2andforall2can return before they check the lengths.removeManyAtremoves nothing for a count that is not positive, and nothing whenindex + countoverflows, becauseListwalks the firstindex + countelements. The Copilot review found the overflow: such a range used to reachRemoveRange, which threw.sort,sortDescending,sortWithorsortByDescendingthrows anInvalidOperationExceptionwith the original exception inside, asArray.Sortdoes inList.List.sortByDescendingcalls the projection in the comparison, and so does this module now.sortBylets an exception of the projection escape, asList.sortBydoes.Listreports some unusual parameter names, and the module reports them too:"source"forexactlyOne,averageandaverageBy;"count"forinit;"list.[j]"fortranspose.nullinput throwsArgumentNullException.seq<'T>rejectsnulland a default flat list given as a sequence, a boxed struct that fails withInvalidOperationExceptionas soon as the BCL enumerates it, withArgumentNullExceptionthat names the parameter of the function.checkNotNullOrDefaultSeqdoes it inIndexedList(ofSeq,concat,insertManyAt,removeMany,removeManyWith,transpose,except), inIIndexedList(the same functions butofSeq;concatandtransposereject a default flat list as an element too, because it implementsIImmutableList<'T>), inIndexedListBuilder(addRange,insertManyAt,removeMany,removeManyWith) and inindexedList { }(yield!of a sequence andfor). TheOptionandTuplemodules have no sequence parameter.ImmutableListthatListlacks throw what the member throws, with the names of their own parameters (D6). Where the member contradicts its own documentation or answers with a wrong index, the module follows the documentation (D14). The internal moduleListRanges(Helper/ListRanges.fs) checks the ranges first, comparing the count with the difference between the length and the index:Int32.MaxValue:IndexOf (item, Int32.MaxValue, 1)returns -1, andReverse (Int32.MaxValue, 1)returns the list, for a list of five elements. The module throwsArgumentOutOfRangeException.BinarySearch (index, count, item, comparer)does not check that the range lies in the list, although its documentation promisesArgumentException.binarySearchRange*throwsArgumentException.LastIndexOfandFindLastIndexaccept a last element at or after the end of the list and answer with the index of another element:LastIndexOf (20, 5, 2)on[10; 20; 30; 20; 10]returns 4, and element 4 is 10.FindLastIndexalso takes a negative count for "all".lastIndexOfRange*andtryFindIndexBackRangethrowArgumentOutOfRangeException, also whereLastIndexOfthrows a plainArgumentException.IndexedListandIIndexedListmodules repeat the functions withvoptionor struct tuples (D3).Option.IndexedListhastryFindIndexRangeandtryFindIndexBackRangetoo.IndexedListBuilderworks onImmutableList<'T>.Builder(37 functions), and its sorts are stable too. It getsinsertManyAt,removeAll,removeMany,removeManyAt,removeManyWith,removeWith,replace,replaceWith,revRange,sortRange*, the four searches in a range,binarySearch,binarySearchWithComparer,binarySearchRangeWithComparerandblit.replacethrowsArgumentExceptionfor a value that no element equals, as the builder of the BCL does, andaddRangenamesvaluesin itsArgumentNullException, where the BCL namesitems.indexedList { }builds an indexed list.yield!of anullsequence, array or indexed list throwsArgumentNullExceptionforitems.Helper/gets a sharedListlayer thatQueueandStackwill reuse, one root module per file:ListChecksholds the checks of theListfunctions, with their exact exceptions.ListArraysholds the two- and three-collection algorithms with the behaviour ofList.Snapshotcopies a sequence to an array.Collectorsholds the accumulator ofcollectandconcat: it copies the lists of fewer than 16 elements into a buffer and joins the longer indexed lists to a builder withBuilder.AddRange, which shares their nodes (see the cost of the whole-list functions below).DescendingSortsholdssortDescendingandsortByDescending: the stable sorts ofListwithout the boxes of the inlineSeqsorts, for the descending sorts ofQueue,Stackand their interface modules too.Lengthscounts a queue or a stack only up to a limit, for the checks that tell an empty input, one element and more apart.queueUpToreadsIsEmptyand callsDequeueat most limit − 1 times;stackUpTopops without an allocation. No function of this pull request calls it, so the tests ofQueueandStackwill cover it.ListRangesholds the checks of the functions that take a range of an indexed list or of its builder:forward,backward,searchandblit.IndexedList.blituses it too.Raise.fsgetsinvalidArg2ListsDifferent,invalidArgRandomizerOutOfRangeandinvalidOpListEmpty, written with interpolated strings like the rest of the module. The message that the index functions ofList(insertAt,insertManyAt,removeAt,removeManyAtandupdateAt) write as a literal is in no resource, so it is a private literal ofListChecksand not a constant ofErrorStrings.Added,ChangedandRemovedentries compare with the published 1.0.0 assembly, where the list module isImmutableListModulewith 75 functions andIndexedSeqhas 3. They were checked by reflection, by compiling against the assembly and by running its functions next to the new ones, not againstmain.Removednames the 23 functions that 1.0.0 had andIndexedListlacks, each with its replacement. Thirteen are renames with the same arguments, in one entry:lastIndex,lastIndexWith,indexRange,indexRangeWith,lastIndexRange,lastIndexRangeWith,insert,insertRange,removeRange,exceptWith,reverse,reverseRangeandwithItem. The other ten areindex,indexWith,indexFromandindexFromWith(they returned a function that took the list a second time),lastIndexFromandlastIndexFromWith(with the formulas),clear,skipUntil,takeUntilandfilterFold(no replacement). TheIndexedSeqmodule (check,item,length) and theIIndexedSeq<'T>abbreviation are listed withIReadOnlyList<'T>as the replacement.Addedno longer names what 1.0.0 had:add,cons,remove,removeWith,replace,replaceWithandtryTail.Changednames what running both showed. The module is compiled asIndexedListwith PascalCase names, whereImmutableListModulehad the F# names. The functions takeImmutableList<'T>, where 1.0.0 took anyIImmutableList<'T>;ofList,collect,concatandappendtake indexed lists or an F# list, where they took any sequences.tryFind,tryFindIndex,tryHead,tryItem,tryLastandtryTailreturnvoption.head,last,tail,item,removeAt,take,skip,truncate,splitAt,forall2,containsandofBuilderthrow other exceptions or return other results.skipWhileandtakeWhiledo what their names say,exceptfollowsList.except, and the equality ofcontains,remove,replaceand theindexOffamily differs.revis faster.Removedentry of theIndexedSeqmodule does not list theReadOnlyListalias of 1.0.0: a module abbreviation cannot be used from another assembly. A project that references the 1.0.0 package compilesIndexedSeq.lengthandIIndexedSeq<int>, and does not compileReadOnlyList.length.ImmutableList.fsandIndexedSeq.fs.docsSrc/Tutorials/Getting_Started.mdno longer calls the removedImmutableList.ofSeq.Tests: 8315 per target framework, 5555 of them new (
mainhas 2760).DataRowandDynamicDatarows, so every case runs and reports as a test of its own.Listfunctions whereArraybehaves differently (the stable sorts, the walk of the common prefix,exists2/forall2,removeManyAt, the exceptions) and as the list case ofyield!.IndexedListderives the sequence-like, creation, index-based, stable-sort and cost contracts.IIndexedListderives the sequence-like, index-based, stable-sort and cost contracts, over indexed lists upcast to the interface. Its sequence-like contract runs again overTaggedList, an implementation ofIImmutableList<'T>that is not an indexed list, which covers the general way of the functions that take a shortcut for indexed lists.Contracts/CostContract.fsis new. It bounds whatmax,maxBy,minandminByallocate for 10 000 integers (they must not box), whatexists2,forall2andcompareWithallocate for two lists of 100 000 integers when the first pair decides (they must not copy the lists), and whatconcatandcollectallocate for 100 indexed lists of 1000 elements (they must join the lists, not copy their elements), forIndexedListandIIndexedList. With the sources of the previous head in place of the new ones, exactly these 18 tests fail and the other 8 297 pass. The four tests of the boxes are inconclusive in a Debug build: with the optimizer of the compiler off, the comparison of two integers in a function that is inline at the call site goes through the generic operator, which boxes them, as it does for the inline functions of FSharp.Core.Contracts/AllocationAssert.fsmeasures what an action allocates, and the stable sorting contract uses it too.Contracts/OptionAssert.fshasWantSomeandWantValueSome, which return the value of an option or a value option and fail the test with a message when there is none; they replace the matches that failed on nothing,ValueOption.getandValuein the tests ofIndexedList,IIndexedListand the oracles ofFlatList.SeqLikeTransformationContracthas a test thatcollectandconcatkeep the order of the elements across collections shorter and longer than 16 elements, also with the same long list twice.Contracts/StableSortContract.fsis new.sort*keeps the order of equal keys, on the 20 pairs of the newInputs.ManyPairs, whose keys repeat. The sort of .NET orders up to 16 elements by insertion, which keeps equal keys in order. As the Copilot review noted, the sixInputs.Pairstherefore could not tell a stable sort from an unstable one.ImmutableList.Sortfor a moment, the 12 stable-sort contract tests and the stability test of the builder sorts fail.sort,sortDescending,sortWith) and a projection that throws (sortBy,sortByDescending) throw whatListthrows, as the review of feat:QueueandIQueuemodules with the vocabulary of the FSharp.CoreListmodule #33 asked, and thatsortByDescendingcalls the projection in the comparisons, in the same order, asList.sortByDescendingdoes.sortDescendingandsortByDescendingallocate less than 3 MB for 10 000 integers: 1.2 MB withDescendingSorts, 8 MB with the inlineSeqsorts, in Debug and in Release. With the inline sorts back, exactly these four tests fail.IndexedList/ListMembersContract.fsis new.IndexedListandIIndexedListderive it, and no other module has these functions, which is why it is not part of the shared contracts thatFlatListand the queues run.replace; the four searches in every range of eight strings; and the exception of every range that is rejected, with the ranges that the members ofImmutableListaccept (D14).IndexedList/ListMemberCases.fsis new too. It holds the strings and the rows that the contract,IndexedListRangeTestsandIndexedListBuilderTestsshare, and the oracles that feat:FlatListmodule with the vocabulary of the FSharp.CoreArraymodule #26 does not have: the change of a range, the stable sort, the removal of several values and the search from the back.FlatList/FlatListOracles.fsof feat:FlatListmodule with the vocabulary of the FSharp.CoreArraymodule #26 and use itsremoveFirst,replaceFirst,firstInRange,equalIgnoringCase,descendingandbyAbsolute. The tests ofnullinputs callFlatListAssert.RejectsNullof feat:FlatListmodule with the vocabulary of the FSharp.CoreArraymodule #26 too. The oracle of the search from the back answers -1 for a range of no elements before it callslastInRange, because the bounds of a range that starts atInt32.MaxValueoverflow, and the members ofImmutableListaccept such a start index for an empty range.IndexedListRangeTestscoverssub,revRange,sortRange*, the binary searches andtryFindIndexRange/tryFindIndexBackRange: every range of the inputs against the arrays, the stability of the range sorts onInputs.ManyPairs, and the parameter that each function names for a range that it rejects. Its rows of the ranges are the ones ofFlatListTests.FlatListruns them too and the queues and the stacks will:SeqCreationContract:ofSeqrejects a default flat list given as a sequence with what it throws for a null sequence.SeqLikeTransformationContract:concatandtranspose, one data-row test each, reject a null sequence of collections withArgumentNullExceptionthat names the parameter of FSharp.Core (arraysorlists, mapped to the name of the module), and reject a default flat list with what they throw for a null one.List.concatthrowsNullReferenceExceptionfornull, so the test asserts the exception of the module.SeqLikePartitioningContract:exceptrejects a default flat list given as the items to exclude like null items, also for an empty input.IndexedContract:insertManyAtrejects null values and a default flat list withArgumentNullExceptionforvalues, and checks the index before the values.ListMembersContract(IndexedListandIIndexedList):removeManyandremoveManyWithreject null items and a default flat list, on an empty and on a non-empty collection, and check the collection before the items. The tables of each module add what a contract over integers cannot say: a default flat list of strings given as a sequence of objects, the order of the checks, a default flat list as an element, andyield!andforof the expression.Inputs.DefaultFlatListAsSeqis the one small addition to the contracts of chore: lay the foundation for the per-collection modules #25.FlatList:nullarray case ofofArrayusesArray.toListas itsListreference, becauseList.ofArraythrowsNullReferenceException.removeManyAtwith a count that is not positive moved to its own test, and a new test covers ranges whose end overflowsInt32.MaxValue. Both expect exactly what the mirrored function does: its result or its exception.ConsandNilpatterns also carryPatternMatchingTestCategory.IndexedListOpsandIIndexedListOpsare the root modules of their own files.nullinputs of every function, shared trees,cons/unconsand the patterns,binarySearch,blit, the builder, the computation expression,yield!ofnulland both namespaces.IndexedList.blit, which the function makes anyway (a control that has to stay green);IndexedListBuilder.sortRange*that sorts a range of one element instead of skipping it, which leaves the builder as it was;ListRanges.searchwithout the check of a negative count, whichBinarySearchmakes again and answers with the same exception.Types of changes
What types of changes does your code introduce to FSharp.Collections.Immutable?
Put an
xin the boxes that applyThese changes break existing code:
ImmutableListis renamed toIndexedList.try*functions returnvoption, and tuples are struct tuples.skipWhileandtakeWhilereturn what their names say. In 1.0.0 each of them returned the result of the other one.IndexedSeqmodule and the legacy functions in the Removed section of the changelog are gone.Checklist
Put an
xin 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.Further comments
skipWhile,takeWhile,skipUntilandtakeUntilreturned each other's results; thatindex,indexWith,indexFromandindexFromWithreturned a function that took the list a second time; thatlastIndexRangeaccepted a start index at the end of the list and answered with an index that the list does not have, andlastIndexof an empty list threw; and thatforall2returnedtruefor lists of different lengths when the common pairs satisfied the predicate.Orderalready wraps the exception of a comparison inInvalidOperationException, asArray.Sortdoes. With the old LINQ sorts, only thesortByDescendingtest fails, becauseOrderByDescendingcomputes the keys before it sorts. The four sorts moved toSeqanyway: they now make exactly the comparisons thatListmakes.Seqsorts: the inlineSeq.sortDescendingandSeq.sortByDescendingboxed every integer in every comparison; the work onQueuefound it. The numbers are those of the library itself, in Release on .NET 10 with FSharp.Core 10.1.301, for 10 000 integers with 1000 distinct values: the median of eight rounds of 100 calls, run alternately before and after the fix, and the best single call in brackets.IndexedList.sortDescendingtook 7.22 ms (3.46 ms) and allocated 7 508 744 bytes per call. It takes 1.67 ms (0.90 ms) and allocates 680 336 bytes.IndexedList.sortByDescendingby the integer itself took 7.64 ms (3.74 ms) and allocated 7 508 768 bytes. It takes 2.20 ms (1.11 ms) and allocates 680 344 bytes.IIndexedList.sortDescendingandsortByDescendingtook 7.20 ms and 7.96 ms with the same allocation, and take 1.68 ms and 2.04 ms.sortWithtakes 1.76 ms (0.82 ms) and allocates 680 312 bytes,sort0.80 ms and 560 208 bytes,sortBy1.40 ms and 640 416 bytes,sortWithComparer0.73 ms and 520 104 bytes. Most of the allocation is the tree of 10 000 nodes and the arrays of the sort.sortandsortDescendingof a value type without a primitive comparer, such asstruct (int * int), allocate 17 MB for 10 000 elements. F# comparison boxes such a value in every comparison, and FSharp.Core does so inList.sort(17.2 MB),Seq.sortandArray.sort(15.3 MB) too; the cached comparer is that comparison. Integers and strings do not box.sub list (n / 2) 1000takes 80 µs, 93 µs and 69 µs for n = 10 000, 100 000 and 1 000 000, and allocates 48 024 bytes each time.IndexedList.skipfollowed bytaketakes 0.59 ms, 5.5 ms and 56 ms and allocates 0.29 MB, 2.4 MB and 24 MB, because each of them builds a tree. Since the range is copied withCopyToinstead of read withGetRange,sub list (n / 2) 1000of a list of 1 000 000 takes 0.05 to 0.07 ms instead of 0.07 to 0.09 ms, andskipfollowed bytake17 ms instead of 36 to 40 ms (measured on the same machine, one after the other).sortRange (n / 2) 1000takes 0.23 ms, 0.27 ms and 0.27 ms for the same three lengths, and sorting 1 000, 10 000 and 100 000 elements of a list of 1 000 000 takes 0.27 ms, 4.0 ms and 35 ms. The BCLSort (400 000, 10 000, comparer)takes 68 ms and allocates 52 MB, because it copies the whole list into an array and builds a new tree;sortRangetook 2.3 ms and allocated 1.6 MB when it read the range withGetRange, and takes about 2 ms and allocates 1.1 MB since it copies the range withCopyTo. Writing the sorted elements back through a builder is not faster (the same 2.9 ms for 10 000 elements, 132 ms against 104 ms for 500 000), soRemoveRangeandInsertRangestay.revRange (n / 2) 1000takes 0.13 ms, 0.25 ms and 0.25 ms; reversing 1 000, 10 000 and 100 000 elements of a list of 1 000 000 takes 0.26 ms, 2.9 ms and 27 ms.indexOfRangeat the end of a list: 1 000 elements take 25 µs, 27 µs and 15 µs for the same three lengths, and 100 000 elements of a list of 1 000 000 take 1.5 ms (lastIndexOfRange: 1.1 ms). The work depends on the length of the range, as the O(log n + count) of the documentation says.ResizeArrayare the best way to build the results): Release on .NET 10 with FSharp.Core 10.1.301, 100 000 integers unless said otherwise, the median of 7 to 31 runs after a warm-up of 400 ms and a pause that lets the tiered compiler install the optimized code. Each range holds two runs of the previous head and of this one, made alternately, because the machine drifts by up to 30 % between runs.max,maxBy,minandminByof both modules are inline, as the ones ofListare. Compiled into a generic function,Seq.maxand the others compare through the generic operator and box every integer twice: 4.1 to 9.4 ms and 4.8 MB before, 2.1 to 2.9 ms and 40 bytes after (112 forIIndexedList, which allocates the enumerator).exists2,forall2andcompareWithofIndexedListwalk the two lists with their enumerators and stop at the pair that decides. When the first pair decides,exists2takes 0.006 to 0.007 ms and 40 bytes where it took 3.3 to 4.9 ms and 800 KB. A walk to the end takes 2.5 to 2.8 ms and 40 bytes whereforall2andcompareWithtook 4.7 to 6.3 ms and 4.8 to 4.9 ms and 800 KB. An enumerator reached through an interface takes more than twice as long as the snapshots on a walk to the end (8.6 to 9.1 ms against 3.6 to 4.0 ms), soIIndexedListhands two indexed lists toIndexedListand copies the other implementations.findBack,tryFindBack,findIndexBackandtryFindIndexBackofIIndexedListsearch an indexed list from its end withFindLastIndex: 0.005 ms and 120 bytes where the copy took 2.4 to 2.5 ms and 400 KB when the last element matches, and 0.6 to 0.8 ms where it took 2.4 ms when none does. Another implementation is still copied, because its indexer can take O(n) for an element.collectandconcat(Helper/Collectors.fs):Builder.AddRangeof an indexed list shares its nodes, which takes O(log n) whatever its length, where a copy takes O(length). A builder alone is 1.2 to 1.8 times slower than the buffer for lists of 1, 3 and 10 elements and 1.4 times faster for 30, 2.4 times for 100 and 1000 (fresh lists, 100 000 elements in all), so the collector copies the lists of fewer than 16 elements and joins the others.concatof 100 indexed lists of 1000 elements takes 0.09 to 0.13 ms and 76 KB where it took 2.8 to 3.5 ms and 5.8 MB, one indexed list of 100 000 elements 0.004 to 0.005 ms where it took 3.2 to 3.4 ms, andcollectof 100 results that are one list of 1000 elements 0.07 to 0.08 ms and 76 KB where it took 2.2 ms and 5.8 MB.collectof 10 000 fresh lists of 3 elements does not change (1.3 to 1.7 ms against 1.4 to 1.5 ms).skip,take,sub,splitAt,takeLast,truncateand the range sorts copy the range withCopyTo, O(log n + count), whereGetRangeread the elements one by one, O(count log n). Half a million elements of a list of a million take 16 to 19 ms and 26 MB where they took 35 to 36 ms and 24 MB, andIIndexedList.skipof the same range takes 17 ms where it took 37 to 39 ms.sortRangeof 10 000 and of 500 000 elements takes 1.9 to 2.2 ms and 113 to 120 ms where it took 2.6 to 2.8 ms and 164 to 171 ms, and allocates 1.1 MB and 56 MB where it allocated 1.6 MB and 78 MB.chunkBySizeandwindowedofIndexedListcopy the list once and build a tree for each chunk or window, asIIndexedListdoes. Chunks of 2, 100 and 10 000 elements of a list of a million take 44 to 48, 32 to 34 and 31 to 49 ms where they took 76 to 87, 71 to 73 and 75 to 76 ms; windows of 3 and 100 elements of 100 000 take 9 to 9.5 and 94 to 102 ms where they took 18 to 19 and 430 to 514 ms. They allocate 9 to 27 % more, for the copy and the arrays of the chunks. The outer list ofchunkBySize,windowed,splitIntoandtransposeis built fromSeq.mapandofSeqin both modules, not from an array of the inner lists: the BCL copies a sequence into a buffer anyway, and the time and the allocation are the same.allPairs: two snapshots take 1.4 to 2.5 times less time than an enumerator of the second list for each element of the first (300 x 300, 1000 x 1000, 10 x 100 000 and 100 000 x 10 elements). A snapshot of the second list alone is between 2.3 times slower and 17 % faster, so theArrays.allPairsthatFlatList,QueueandStackshare stays.chooseandunfold: aResizeArrayandCreateRangetake 3.4 ms whereBuilder.Addtakes 7.7 ms (2.3 times as long).IIndexedList.chunkBySize: spans cut from one snapshot, instead of the arrays ofArray.chunkBySize, are between 1.2 times faster and as fast for chunks of 2, 100 and 10 000 elements of a list of a million, so the array function stays.contains: a version that is inline takes as long as the current one (2.7 ms), andImmutableList.Containstakes 1.6 ms but compares withEqualityComparer<'T>.Default, whereList.containscompares in F# equality.iter: the loop takes 2.1 ms whereForEachtook 2.2 to 2.6 ms; it does not allocate the delegate.ArrayPool<T>.Shared(asked in the review): the functions that copy a list into an array, work on it and build the tree from the result allocate an array that nobody sees again, because the tree copies the elements.revwas measured,ToArray+Array.Reverse+CreateRangeagainstRent+CopyTo+Array.Reverse+ImmutableList.Create (ReadOnlySpan)+Return; the figures are the mean of a loop of 2 s with the allocation and the collections counted. Renting the array pays when it is large, and only then:Arraytake the length of the array they get, and the pool returns a longer one, sofoldBack,scanBack,windowed,chunkBySize,splitInto,permute,partitionand the other functions that call anArrayfunction on a snapshot, and the algorithms ofArraysandListArraysthatFlatList,QueueandStackshare and that return arrays, would first need versions that take a span.rev,mapi,mapFold,scanand the random functions work on the snapshot or on their result in their own loop, and could rent it today.ResizeArrayofchoose,unfoldandconcatallocates a new array at every doubling, the large ones in the large object heap; a buffer from the pool would avoid that for long results.ArgumentOutOfRangeExceptionwhereLastIndexOfthrows a plainArgumentException, because the first is anArgumentExceptiontoo and it names the parameter.binarySearchRange*throws a plainArgumentExceptionfor a range beyond the end, as the documentation ofBinarySearchsays.findIndexRangeandfindIndexBackRangeis not added; the analysis calls it optional.IndexedListBuilderhasbinarySearchRangeWithCompareronly, as the analysis proposes;binarySearchRangeofIndexedListis the same call with the default comparer.ArrayPool(see above). It needs a rented snapshot (a struct that returns the array when it is disposed) and span versions ofArraysandListArrays, whichFlatList,QueueandStackshare, so it is a change of its own.🤖 Generated with Claude Code