Skip to content

feat: IndexedList and IIndexedList modules with the vocabulary of the FSharp.Core List module - #31

Open
xperiandri wants to merge 23 commits into
mainfrom
feat/indexed-list
Open

xperiandri wants to merge 23 commits into
mainfrom
feat/indexed-list

Conversation

@xperiandri

@xperiandri xperiandri commented Oct 18, 2026 •

Copy link
Copy Markdown
Collaborator

Proposed Changes

Rewrites the legacy ImmutableList module as IndexedList (ImmutableList<'T>) and adds the interface module IIndexedList (IImmutableList<'T>) after ADR 0001 (#23). It builds on #26 (FlatList), which is merged into main.

  • Layout (D2): IndexedList, IIndexedList, their Option and Tuple modules and IndexedListBuilder are the root modules of their files, such as module FSharp.Collections.Immutable.IndexedList. The IndexedList<'T> abbreviation lives in IndexedList/IndexedListTypes.fs, which needs no signature file.
    • Every other file with public bindings has a signature file that carries its XML documentation, compiled right before it: IndexedListBuilder.fsi and IndexedListCE.fsi included.
    • indexedList has its val in ComputationExpressions.fsi, after flatList under the Creation banner.
  • IndexedList/IndexedList.fsi and IndexedList.fs: 162 functions, in the canonical category order. They are the IndexedList column of ADR Appendix A, with exactly the parameter names of the FSharp.Core List module, so the exceptions name the same parameters as List.
    • The members of ImmutableList come first (D5): ConvertAll, FindAll, FindIndex, FindLastIndex, TrueForAll, Exists, GetRange, Insert, InsertRange, RemoveAt, RemoveRange, SetItem, IndexOf, LastIndexOf, BinarySearch, and now Remove, RemoveRange (items), Replace and Reverse (index, count).
    • Everything else copies to an array, calls the Array function and builds a balanced tree with ImmutableList.CreateRange.
    • rev takes the faster path that the maintainer chose over ImmutableList.Reverse (D5). It copies the elements to an array, reverses it with Array.Reverse and builds the result with ImmutableList.CreateRange. At 100 000 integers it takes about 3 ms where ImmutableList.Reverse, which the 1.0.0 reverse called, takes about 23 ms (about 8 times faster; 40 ms against 265 ms at 1 000 000), and it allocates 5.2 MB where Reverse allocates 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 from CreateRange and for one built with Add, run on the 1.0.0 assembly and on this branch.
    • Every sort is stable, as List.sort* is. ImmutableList.Sort is unstable, so it is not used.
      • sort and sortWith sort with the FSharp.Core Seq sorts, which share the stable sort of List: they make the same comparisons, in the same Array.Sort.
      • sortDescending and sortByDescending call Seq.sortWith through the internal DescendingSorts module (Helper/DescendingSorts.fs), with the cached comparer reversed. Seq.sortDescending and Seq.sortByDescending are 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 of List; only the boxes go.
      • sortBy and sortWithComparer sort with LINQ OrderBy/Order, which compute the keys first, as List.sortBy does.
      • sortRange, sortRangeWith and sortRangeWithComparer sort a copy of the range with LINQ Order and put it back with RemoveRange and InsertRange, so they are stable too. ImmutableList.Sort (index, count, comparer) is unstable and copies the whole list.
    • The sorts and binarySearch compare with the cached DefaultComparer<'T>.Instance of feat: FlatList module with the vocabulary of the FSharp.Core Array module #26, so a call does not create a comparer.
    • The extras are add, cons, uncons, tryUncons, tryTail, skipLast, takeLast, build, update, blit, binarySearch, indexOf/indexOfWith/lastIndexOf/lastIndexOfWith, sortWithComparer, the span constructors and the Cons/Nil patterns.
    • Functions that wrap more members of ImmutableList, from the API gap analysis of the modules (20 functions):
      • Removal and replacement by value, in EqualityComparer<'T>.Default or a given equality comparer: remove, removeWith, removeMany, removeManyWith, replace, replaceWith. Nothing to remove returns the input itself, and replace throws ArgumentException for a value that no element equals, as the BCL does.
      • The range: sub (it throws what Array.sub throws, as FlatList.sub does), revRange, indexOfRange, indexOfRangeWith, lastIndexOfRange, lastIndexOfRangeWith, tryFindIndexRange and tryFindIndexBackRange. The searches return indices of the whole indexed list.
      • Binary searches with a comparer, binarySearchWithComparer, binarySearchRange and binarySearchRangeWithComparer, and the stable sortRange*.
  • IIndexedList.fsi and IIndexedList.fs: 136 functions over IImmutableList<'T>.
    • Results of the same element type keep the implementation of the input: Clear () plus AddRange, or the member of the interface for an edit of one element. rev reverses its copy in place, as IndexedList.rev does, before it rebuilds.
    • Results of another element type are an IndexedList. So are concat and transpose, because their inputs can have different implementations.
    • The sorts are the same as in IndexedList.
    • remove, removeWith, removeMany, removeManyWith, replace and replaceWith call 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.
    • A test with a custom IImmutableList implementation proves that the module keeps it.
  • Exceptions (D6, D14): every function throws what the FSharp.Core List function throws: the type, the message and the parameter name. This includes the quirks of List:
    • The functions of two and three lists call their callback on the common prefix before they throw.
    • exists2 and forall2 can return before they check the lengths.
    • removeManyAt removes nothing for a count that is not positive, and nothing when index + count overflows, because List walks the first index + count elements. The Copilot review found the overflow: such a range used to reach RemoveRange, which threw.
    • A comparison or a projection that throws in sort, sortDescending, sortWith or sortByDescending throws an InvalidOperationException with the original exception inside, as Array.Sort does in List. List.sortByDescending calls the projection in the comparison, and so does this module now. sortBy lets an exception of the projection escape, as List.sortBy does.
    • List reports some unusual parameter names, and the module reports them too: "source" for exactlyOne, average and averageBy; "count" for init; "list.[j]" for transpose.
    • A null input throws ArgumentNullException.
    • A parameter of type seq<'T> rejects null and a default flat list given as a sequence, a boxed struct that fails with InvalidOperationException as soon as the BCL enumerates it, with ArgumentNullException that names the parameter of the function. checkNotNullOrDefaultSeq does it in IndexedList (ofSeq, concat, insertManyAt, removeMany, removeManyWith, transpose, except), in IIndexedList (the same functions but ofSeq; concat and transpose reject a default flat list as an element too, because it implements IImmutableList<'T>), in IndexedListBuilder (addRange, insertManyAt, removeMany, removeManyWith) and in indexedList { } (yield! of a sequence and for). The Option and Tuple modules have no sequence parameter.
    • The functions that wrap a member of ImmutableList that List lacks 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 module ListRanges (Helper/ListRanges.fs) checks the ranges first, comparing the count with the difference between the length and the index:
      • The members add the index and the count, which wraps around Int32.MaxValue: IndexOf (item, Int32.MaxValue, 1) returns -1, and Reverse (Int32.MaxValue, 1) returns the list, for a list of five elements. The module throws ArgumentOutOfRangeException.
      • BinarySearch (index, count, item, comparer) does not check that the range lies in the list, although its documentation promises ArgumentException. binarySearchRange* throws ArgumentException.
      • LastIndexOf and FindLastIndex accept 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. FindLastIndex also takes a negative count for "all". lastIndexOfRange* and tryFindIndexBackRange throw ArgumentOutOfRangeException, also where LastIndexOf throws a plain ArgumentException.
  • Option and Tuple namespaces: the IndexedList and IIndexedList modules repeat the functions with voption or struct tuples (D3). Option.IndexedList has tryFindIndexRange and tryFindIndexBackRange too.
  • Builder and computation expression (D7): IndexedListBuilder works on ImmutableList<'T>.Builder (37 functions), and its sorts are stable too. It gets insertManyAt, removeAll, removeMany, removeManyAt, removeManyWith, removeWith, replace, replaceWith, revRange, sortRange*, the four searches in a range, binarySearch, binarySearchWithComparer, binarySearchRangeWithComparer and blit. replace throws ArgumentException for a value that no element equals, as the builder of the BCL does, and addRange names values in its ArgumentNullException, where the BCL names items. indexedList { } builds an indexed list. yield! of a null sequence, array or indexed list throws ArgumentNullException for items.
  • Helper/ gets a shared List layer that Queue and Stack will reuse, one root module per file:
    • ListChecks holds the checks of the List functions, with their exact exceptions.
    • ListArrays holds the two- and three-collection algorithms with the behaviour of List.
    • Snapshot copies a sequence to an array.
    • DescendingSorts holds sortDescending and sortByDescending: the stable sorts of List without the boxes of the inline Seq sorts, for the descending sorts of Queue, Stack and their interface modules too.
    • Lengths counts a queue or a stack only up to a limit, for the checks that tell an empty input, one element and more apart. queueUpTo reads IsEmpty and calls Dequeue at most limit − 1 times; stackUpTo pops without an allocation. No function of this pull request calls it, so the tests of Queue and Stack will cover it.
    • ListRanges holds the checks of the functions that take a range of an indexed list or of its builder: forward, backward, search and blit. IndexedList.blit uses it too.
    • Raise.fs gets invalidArg2ListsDifferent, invalidArgRandomizerOutOfRange and invalidOpListEmpty, written with interpolated strings like the rest of the module. The message that the index functions of List (insertAt, insertManyAt, removeAt, removeManyAt and updateAt) write as a literal is in no resource, so it is a private literal of ListChecks and not a constant of ErrorStrings.
  • Changelog: the new functions are listed, and the Added, Changed and Removed entries compare with the published 1.0.0 assembly, where the list module is ImmutableListModule with 75 functions and IndexedSeq has 3. They were checked by reflection, by compiling against the assembly and by running its functions next to the new ones, not against main.
    • Removed names the 23 functions that 1.0.0 had and IndexedList lacks, 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, reverseRange and withItem. The other ten are index, indexWith, indexFrom and indexFromWith (they returned a function that took the list a second time), lastIndexFrom and lastIndexFromWith (with the formulas), clear, skipUntil, takeUntil and filterFold (no replacement). The IndexedSeq module (check, item, length) and the IIndexedSeq<'T> abbreviation are listed with IReadOnlyList<'T> as the replacement.
    • Added no longer names what 1.0.0 had: add, cons, remove, removeWith, replace, replaceWith and tryTail.
    • Changed names what running both showed. The module is compiled as IndexedList with PascalCase names, where ImmutableListModule had the F# names. The functions take ImmutableList<'T>, where 1.0.0 took any IImmutableList<'T>; ofList, collect, concat and append take indexed lists or an F# list, where they took any sequences. tryFind, tryFindIndex, tryHead, tryItem, tryLast and tryTail return voption. head, last, tail, item, removeAt, take, skip, truncate, splitAt, forall2, contains and ofBuilder throw other exceptions or return other results. skipWhile and takeWhile do what their names say, except follows List.except, and the equality of contains, remove, replace and the indexOf family differs. rev is faster.
    • The Removed entry of the IndexedSeq module does not list the ReadOnlyList alias of 1.0.0: a module abbreviation cannot be used from another assembly. A project that references the 1.0.0 package compiles IndexedSeq.length and IIndexedSeq<int>, and does not compile ReadOnlyList.length.
  • Removed files: the legacy ImmutableList.fs and IndexedSeq.fs. docsSrc/Tutorials/Getting_Started.md no longer calls the removed ImmutableList.ofSeq.

Tests: 7832 per target framework, 5072 of them new (main has 2760).

  • The tests compare arrays and take their cases as DataRow and DynamicData rows, so every case runs and reports as a test of its own.
    • The case tables are maps whose names are the rows.
    • Lists remain only as the oracles of the List functions where Array behaves differently (the stable sorts, the walk of the common prefix, exists2/forall2, removeManyAt, the exceptions) and as the list case of yield!.
  • IndexedList derives the sequence-like, creation, index-based and stable-sort contracts.
  • IIndexedList derives the sequence-like, index-based and stable-sort contracts, over indexed lists upcast to the interface.
  • Contracts/StableSortContract.fs is new.
    • It checks that every sort* keeps the order of equal keys, on the 20 pairs of the new Inputs.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 six Inputs.Pairs therefore could not tell a stable sort from an unstable one.
    • With every sort switched to the unstable algorithm of ImmutableList.Sort for a moment, the 12 stable-sort contract tests and the stability test of the builder sorts fail.
    • It checks that a comparison that throws (sort, sortDescending, sortWith) and a projection that throws (sortBy, sortByDescending) throw what List throws, as the review of feat: Queue and IQueue modules with the vocabulary of the FSharp.Core List module #33 asked, and that sortByDescending calls the projection in the comparisons, in the same order, as List.sortByDescending does.
    • It checks that sortDescending and sortByDescending allocate less than 3 MB for 10 000 integers: 1.2 MB with DescendingSorts, 8 MB with the inline Seq sorts, in Debug and in Release. With the inline sorts back, exactly these four tests fail.
  • IndexedList/ListMembersContract.fs is new. IndexedList and IIndexedList derive it, and no other module has these functions, which is why it is not part of the shared contracts that FlatList and the queues run.
    • It checks the removal and the replacement by value, in the default equality and with a comparer that ignores the case, on strings; the input itself when nothing is removed; the exception and the parameter of 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 of ImmutableList accept (D14).
    • IndexedList/ListMemberCases.fs is new too. It holds the strings and the rows that the contract, IndexedListRangeTests and IndexedListBuilderTests share, and the oracles that feat: FlatList module with the vocabulary of the FSharp.Core Array module #26 does not have: the change of a range, the stable sort, the removal of several values and the search from the back.
    • Both files come after FlatList/FlatListOracles.fs of feat: FlatList module with the vocabulary of the FSharp.Core Array module #26 and use its removeFirst, replaceFirst, firstInRange, equalIgnoringCase, descending and byAbsolute. The tests of null inputs call FlatListAssert.RejectsNull of feat: FlatList module with the vocabulary of the FSharp.Core Array module #26 too. The oracle of the search from the back answers -1 for a range of no elements before it calls lastInRange, because the bounds of a range that starts at Int32.MaxValue overflow, and the members of ImmutableList accept such a start index for an empty range.
  • IndexedListRangeTests covers sub, revRange, sortRange*, the binary searches and tryFindIndexRange/tryFindIndexBackRange: every range of the inputs against the arrays, the stability of the range sorts on Inputs.ManyPairs, and the parameter that each function names for a range that it rejects. Its rows of the ranges are the ones of FlatListTests.
  • The sequence inputs are in the shared contracts wherever every implementer has the function, so FlatList runs them too and the queues and the stacks will:
    • SeqCreationContract: ofSeq rejects a default flat list given as a sequence with what it throws for a null sequence.
    • SeqLikeTransformationContract: concat and transpose, one data-row test each, reject a null sequence of collections with ArgumentNullException that names the parameter of FSharp.Core (arrays or lists, mapped to the name of the module), and reject a default flat list with what they throw for a null one. List.concat throws NullReferenceException for null, so the test asserts the exception of the module.
    • SeqLikePartitioningContract: except rejects a default flat list given as the items to exclude like null items, also for an empty input.
    • IndexedContract: insertManyAt rejects null values and a default flat list with ArgumentNullException for values, and checks the index before the values.
    • ListMembersContract (IndexedList and IIndexedList): removeMany and removeManyWith reject 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, and yield! and for of the expression. Inputs.DefaultFlatListAsSeq is the one small addition to the contracts of chore: lay the foundation for the per-collection modules #25.
  • Shared contracts, without weakening them for FlatList:
    • The null array case of ofArray uses Array.toList as its List reference, because List.ofArray throws NullReferenceException.
    • removeManyAt with a count that is not positive moved to its own test, and a new test covers ranges whose end overflows Int32.MaxValue. Both expect exactly what the mirrored function does: its result or its exception.
  • Categories are on the test classes only (D8): the collection and the area that a class examines. The classes with the tests of the Cons and Nil patterns also carry PatternMatchingTestCategory.
  • IndexedListOps and IIndexedListOps are the root modules of their own files.
  • The per-collection tests cover null inputs of every function, shared trees, cons/uncons and the patterns, binarySearch, blit, the builder, the computation expression, yield! of null and both namespaces.
  • Breaking each new behaviour once, in a copy of the sources (81 changes: a call dropped, a comparer ignored, the arguments swapped, a check removed, the stable sort replaced by the BCL sort, the range check replaced by the sum of the index and the count), fails the tests in every case that changes a result: 78 of the 81 changes. The other three change no result:
    • an additional check in 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.search without the check of a negative count, which BinarySearch makes again and answers with the same exception.
  • Breaking each fix of the sequence inputs once (28 changes: the check back to a null check, the check removed, the checks in the old order, the check of an element removed) fails the tests in every case.

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)

These changes break existing code:

  • ImmutableList is renamed to IndexedList.
  • The try* functions return voption, and tuples are struct tuples.
  • skipWhile and takeWhile return what their names say. In 1.0.0 each of them returned the result of the other one.
  • The IndexedSeq module and the legacy functions in the Removed section of the changelog are gone.

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

  • Checked against 1.0.0: running the published assembly next to this branch showed, besides the differences in the changelog, that the 1.0.0 skipWhile, takeWhile, skipUntil and takeUntil returned each other's results; that index, indexWith, indexFrom and indexFromWith returned a function that took the list a second time; that lastIndexRange accepted a start index at the end of the list and answered with an index that the list does not have, and lastIndex of an empty list threw; and that forall2 returned true for lists of different lengths when the common pairs satisfied the predicate.
  • LINQ and the exceptions of a sort: LINQ Order already wraps the exception of a comparison in InvalidOperationException, as Array.Sort does. With the old LINQ sorts, only the sortByDescending test fails, because OrderByDescending computes the keys before it sorts. The four sorts moved to Seq anyway: they now make exactly the comparisons that List makes.
  • Cost of the Seq sorts: the inline Seq.sortDescending and Seq.sortByDescending boxed every integer in every comparison; the work on Queue found 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.sortDescending took 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.sortByDescending by 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.sortDescending and sortByDescending took 7.20 ms and 7.96 ms with the same allocation, and take 1.68 ms and 2.04 ms.
    • The other sorts do not change: sortWith takes 1.76 ms (0.82 ms) and allocates 680 312 bytes, sort 0.80 ms and 560 208 bytes, sortBy 1.40 ms and 640 416 bytes, sortWithComparer 0.73 ms and 520 104 bytes. Most of the allocation is the tree of 10 000 nodes and the arrays of the sort.
  • Boxes that remain: sort and sortDescending of a value type without a primitive comparer, such as struct (int * int), allocate 17 MB for 10 000 elements. F# comparison boxes such a value in every comparison, and FSharp.Core does so in List.sort (17.2 MB), Seq.sort and Array.sort (15.3 MB) too; the cached comparer is that comparison. Integers and strings do not box.
  • Cost of the range functions: the numbers are those of the library, in Release on .NET 10, as the median of 9 to 31 runs after a warm-up, for integers.
    • The cost of a range depends on its length, not on the length of the list. sub list (n / 2) 1000 takes 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.skip followed by take takes 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.
    • sortRange (n / 2) 1000 takes 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 BCL Sort (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; sortRange takes 2.3 ms and allocates 1.6 MB. The alternatives that copy the range into an array (2.3 ms, 1.0 MB) or write the sorted elements through a builder (3.5 ms, 1.0 MB) are not faster, so the plan of GetRange, Order, RemoveRange and InsertRange stays.
    • revRange (n / 2) 1000 takes 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.
    • indexOfRange at 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.
  • Decisions for the maintainers:
    • The module throws ArgumentOutOfRangeException where LastIndexOf throws a plain ArgumentException, because the first is an ArgumentException too and it names the parameter. binarySearchRange* throws a plain ArgumentException for a range beyond the end, as the documentation of BinarySearch says.
    • The throwing pair findIndexRange and findIndexBackRange is not added; the analysis calls it optional.
    • IndexedListBuilder has binarySearchRangeWithComparer only, as the analysis proposes; binarySearchRange of IndexedList is the same call with the default comparer.

🤖 Generated with Claude Code

Copilot AI balanced review requested due to automatic review settings October 18, 2026 11:24

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

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

Comment thread src/FSharp.Collections.Immutable/Helper.fs Outdated
Comment thread tests/FSharp.Collections.Immutable.Tests/Contracts/StableSortContract.fs Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Needs a closer look

The broad breaking API migration and exact List-compatibility requirements need human validation across both target frameworks.

No open issues 🎉

2 resolved since last review

Review effort: Balanced

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
xperiandri requested a balanced review from Copilot October 18, 2026 18:50

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 and exact FSharp.Core compatibility guarantees require final human validation.

No open issues 🎉

Review effort: Balanced

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
xperiandri requested a balanced review from Copilot October 18, 2026 19:34

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 and exact FSharp.Core compatibility requirements warrant final human validation.

No open issues 🎉

Review effort: Balanced

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 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>
Base automatically changed from feat/flat-list to main October 18, 2026 22:34
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
…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 and others added 22 commits October 19, 2026 19:39
… 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>
…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>
…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>
…e collection table

The changelog says that `IndexedList.rev` is about 2.5 times faster than `ImmutableList.Reverse`,
which the old `ImmutableList.reverse` called.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…table sorting contract

The tests compare arrays and take their cases as data rows, as the converted contracts of
`FlatList` do (2292 tests per target framework instead of 771):

- `IndexedListOps` and `IIndexedListOps` observe a collection with its `toArray`, and results
  of another element type are arrays; `ToList` is the `toList` under test. `StableSortOps`
  observes with `ToArray`, and its `Sort` and `SortDescending` take arrays.
- `Inputs.ManyPairs` is a property of the `Inputs` class that builds new pairs on each read.
- The case tables of `IndexedListTests` and `IIndexedListTests` are maps whose names are the
  rows. The names tell apart the cases that repeated a function or a parameter: `blit`,
  `foldBack2` and the two parts of `partition` and `splitAt`.
- `sortWith` of the stable sorting contract takes the direction of its comparison as a
  `SortDirection` row, and `tryItem` of the `Option` tests takes its index as a `DataRow`.
- The `removeManyAt` tests of a count that is not positive and of a range whose end overflows
  take their ranges as rows of the index-based contract.
- Expectations come from the `Array` functions, except where `List` behaves differently: the
  stable sorts, the walk of the common prefix before a length check, `exists2` and `forall2`,
  `contains` of `NaN`, `removeManyAt` and the exceptions keep their `List` oracles.
- Tuples that hold arrays are compared by their parts.
- Every case of the old loops is a row: 190 in `IndexedListTests`, 175 in `IIndexedListTests`,
  19 and 17 in the `Option` tests, 7 in each stable sorting class.
- The doc comments of the stable sorting contract and of `TaggedList` follow the layout rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lary of the FSharp.Core `List` 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. The documentation of `Lengths` and
`ListChecks` refers to `IImmutableQueue<'T>`, `IImmutableStack<'T>` and `IndexedList` with
crefs, instead of naming the `Queue` and `Stack` modules, which this branch does not have.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lary of the FSharp.Core `List` module

- `IndexedListBuilder.fsi` and `IndexedListCE.fsi` carry the signatures and the XML
  documentation of their files, compiled right before them. `indexedList` has its `val` in
  `ComputationExpressions.fsi`, after `flatList` under the `Creation` banner.
- `sort`, `sortByDescending`, `sortDescending` and `sortWith` of `IndexedList` and
  `IIndexedList` 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, `sortWith` takes about the same time as before and allocates 680 312
  bytes per call instead of 600 328.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…table sorting contract

- The stable sorting contract checks that `sort`, `sortDescending` and `sortWith` throw what
  `List` throws when a comparison throws, and that `sortBy` and `sortByDescending` do when the
  projection throws. A `KeyedItem` with the key `UncomparableKey` throws when it is compared.
- `indexedList { yield! ... }` rejects a null sequence, array or indexed list with
  `ArgumentNullException` for the parameter `items`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…table sorting contract

The assertion helpers of `main` are static classes: the tests call `ExceptionAssert.Same`
and `OverrideAssert.OptionMatches` with tupled arguments, in place of the curried functions
of the former modules (31 calls in 7 files).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…lary of the FSharp.Core `List` module

`sortDescending` and `sortByDescending` of `IndexedList` and `IIndexedList` call the new
internal module `DescendingSorts` of `Helper.fs`. `Seq.sortDescending` and
`Seq.sortByDescending` are inline: in a generic function they compare with the generic
operator, which boxes the elements, or the keys, in every comparison. At 10 000 integers, a
call allocated 7.5 MB and took 7.2 ms; it allocates 0.68 MB and takes 1.7 ms, as `sortWith`
does.

`DescendingSorts.sortDescending` and `sortByDescending` run `Seq.sortWith` with the comparer
that `DefaultComparer` caches, reversed. The comparisons, their order and the exceptions are
those of `List.sortDescending` and `List.sortByDescending`, so `Queue` and `Stack` can call
the same helpers.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…table sorting contract

- The stable sorting contract checks that `sortDescending` and `sortByDescending` allocate
  less than 3 MB for 10 000 integers: 1.2 MB with the cached comparer, 8 MB with the inline
  `Seq` sorts. The second run of a call is measured, after the first has compiled the code.
  `StableSortOps` gets the functions `SortIntegersDescending` and `SortIntegersByDescending`
  for it.
- It checks that `sortByDescending` calls the projection in the comparisons, in the order of
  `List.sortByDescending`; a sort that computes the keys first fails it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…lary of the FSharp.Core `List` module

`IndexedList` gets the functions of `ImmutableList<T>` that the module lacked, with the
names and signatures of the API gap analysis:

- `remove`, `removeWith`, `removeMany`, `removeManyWith`, `replace`, `replaceWith`: removal
  and replacement by value, in `EqualityComparer<'T>.Default` or a given equality comparer.
  Nothing to remove returns the input itself, and `replace` throws `ArgumentException` naming
  `oldValue` when no element equals it, as the BCL does.
- `indexOfRange`, `indexOfRangeWith`, `lastIndexOfRange`, `lastIndexOfRangeWith`,
  `tryFindIndexRange`, `tryFindIndexBackRange`: searches in a range, which return indices of
  the whole indexed list. The two `tryFind` functions have their `Option.IndexedList`
  counterparts.
- `sub`, `revRange`: a range of the elements, and the reversal of a range. `sub` throws what
  `Array.sub` throws, as `FlatList.sub` does.
- `sortRange`, `sortRangeWith`, `sortRangeWithComparer`: stable, like every sort of the
  module. A copy of the range is sorted with LINQ `Order`, and put back with `RemoveRange` and
  `InsertRange`; `ImmutableList.Sort (index, count, comparer)` is unstable. The cost depends on
  the range: 10 000 elements of 1 000 000 take 2.3 ms and allocate 1.6 MB, where the BCL member
  takes 68 ms and allocates 52 MB, because it copies the whole list.
- `binarySearchWithComparer`, `binarySearchRange`, `binarySearchRangeWithComparer`.

`IIndexedList` gets `remove`, `removeWith`, `removeMany`, `removeManyWith`, `replace`,
`replaceWith` and the four searches in a range. They call the members of the interface, so the
results keep the implementation of the input. `IndexedListBuilder` gets `insertManyAt`,
`removeAll`, `removeMany`, `removeManyAt`, `removeManyWith`, `removeWith`, `replace`,
`replaceWith`, `revRange`, `sortRange` (stable, like the sorts of the module), `sortRangeWith`,
`sortRangeWithComparer`, the four searches in a range, `binarySearch`,
`binarySearchWithComparer`, `binarySearchRangeWithComparer` and `blit`. `replace` of the
builder throws when no element equals the old value, as the builder of the BCL does.
`addRange` names `values` in its `ArgumentNullException`: the BCL names it `items`.

The functions that take a range check it first (ADR 0001, D14). The internal module
`ListRanges` of `Helper.fs` holds the checks, which compare the count with the difference
between the length and the index. The members of `ImmutableList` add the index and the count
and accept a sum that wraps around `Int32.MaxValue`. `BinarySearch` accepts a range beyond the
end, although its documentation promises `ArgumentException`, which the module throws.
`LastIndexOf` and `FindLastIndex` accept a last element at or after the end and answer with
the index of another element, and `FindLastIndex` takes a negative count for "all": the module
throws `ArgumentOutOfRangeException` for them. A range of no elements searches nothing, for
any start index. `blit` of `IndexedList` uses the same checks.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…table sorting contract

Tests of the new functions of `IndexedList`, `IIndexedList` and `IndexedListBuilder`, in rows:

- `Contracts/ListMemberCases.fs` is new. It holds the strings that tell the default equality
  from an equality that ignores the case, the oracles of the removal, the replacement and the
  searches in a range, and the rows: every range of eight strings, the ranges that are
  rejected with the fault of each (`RangeFault`), and the sorted inputs of the binary
  searches. `IndexedList`, `IIndexedList` and the builder share them.
- `Contracts/ListMembersContract.fs` is new. `IndexedList` and `IIndexedList` derive it: the
  removal and the replacement by value, the input itself when nothing is removed, the
  exception and the parameter of `replace`, the four searches in every range, and the
  exceptions of every range that is rejected, with the ranges that the members of
  `ImmutableList` accept (ADR 0001, D14). No other module has these functions, which is why
  it is not part of the contracts that `FlatList` and the queues run.
- `IndexedListRangeTests` covers `sub`, `revRange`, `sortRange`, `sortRangeWith`,
  `sortRangeWithComparer`, the three binary searches, `tryFindIndexRange` and
  `tryFindIndexBackRange` of `IndexedList`: every range of the inputs against the arrays, the
  stability on `Inputs.ManyPairs`, the parameter that each function names for a range that it
  rejects, the input itself for a range of at most one element, and a comparison that does
  not run for such a range.
- `IIndexedListTests` proves that the six functions of removal and replacement keep the
  implementation of their input, that the searches in a range work on an implementation that
  is not an indexed list, and that `removeMany` and `removeManyWith` reject null values even
  for a list that would accept them (`TaggedList` gets that mode). `IndexedListOptionTests`
  covers the two `tryFind` functions.
- `IndexedListBuilderTests` covers the new functions of the builder against the same oracles
  and rows, including the result of the functions that change a builder, the `bool` of
  `removeWith`, the parameter names of `addRange` and `insertManyAt`, and `blit`.
- `KeyedItem` gets `OfManyPairs` and `KeysAndTags`, in place of the copies in the stable
  sorting contract and the builder tests. The rows of the rejected copies of `blit` are shared
  with the builder.

Each new behaviour was broken once in a copy of the sources (a call dropped, a comparer
ignored, the arguments swapped, a check removed, the stable sort replaced by the BCL sort, the
range check replaced by the sum of the index and the count): the tests fail for every change
that alters a result.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…e collection table

The changelog lists the new functions of `IndexedList`, `IIndexedList` and `IndexedListBuilder`
and corrects the `Removed` entries of the 1.0.0 `ImmutableList` functions:

- `indexFrom`, `indexRange` and their `lastIndex` counterparts are replaced by `indexOfRange`,
  `indexOfRangeWith`, `lastIndexOfRange` and `lastIndexOfRangeWith`, with the formulas for
  `indexFrom` and `lastIndexFrom`. They no longer send their users to `indexOf`, which searches
  the whole list.
- `remove`, `removeWith`, `replace` and `replaceWith` are no longer removed: `IndexedList` has
  them again. `reverseRange` is `revRange`, with two arguments instead of a tuple, and
  `exceptWith` is `removeManyWith`. The entry no longer sends users to "the members of
  `ImmutableList<'T>`".
- `skipUntil` and `takeUntil` are `skipWhile` and `takeWhile` with the negated predicate.
- `filterFold` has no replacement.
- The 1.0.0 `append list items` took a sequence: `insertManyAt (length list) items list`, or
  `append list (ofSeq items)`. The 1.0.0 `except` removed one element for each item, which
  `removeMany` does.
- The 1.0.0 functions compared in F# structural equality (`HashIdentity.Structural`), and the
  new ones compare in `EqualityComparer<'T>.Default`.
- The entry of the `IndexedSeq` module no longer lists the `ReadOnlyList` alias: a module
  abbreviation is not in the IL of the assembly, and a project that references the 1.0.0 package
  cannot compile `ReadOnlyList.length`, where `IndexedSeq.length` and the `IIndexedSeq<'T>`
  abbreviation compile.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…table sorting contract

The tests of the new functions use what the tests of `FlatList` share, in place of their own
copies:

- `ListMemberCases.fs` and `ListMembersContract.fs` move from `Contracts/` to `IndexedList/`
  and come after `FlatList/FlatListOracles.fs` in the compile order, because they use it.
- The oracles of the removal of the first equal element, the replacement, the search in a range
  from the front, the equality that ignores the case and the descending and absolute orders are
  `FlatListOracles.removeFirst`, `replaceFirst`, `firstInRange`, `equalIgnoringCase`,
  `descending` and `byAbsolute`. `ListOracles` keeps the oracles that only the indexed list has:
  the change of a range, the stable sorts, the removal of several values and the search from
  the back. That one answers -1 for a range of no elements before it calls
  `FlatListOracles.lastInRange`, because the bounds of a range that starts at `Int32.MaxValue`
  overflow, and the members of `ImmutableList` accept such a start index for an empty range.
- The assertions that a function rejects a `null` input and names its parameter call
  `FlatListAssert.RejectsNull` in the tests of `IndexedList`, `IIndexedList`, the builder and
  the computation expression.

The tests are the same: 7421 per target framework. The 81 changes of the sources of the package
fail the same tests as before. 78 are caught, and the three that remain change no result: an
additional check in `IndexedList.blit` that is made anyway (a control), `sortRange*` of the
builder for a range of one element, and `ListRanges.search` without the check of a negative
count, which `BinarySearch` repeats.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…e collection table

The `Added`, `Changed` and `Removed` entries of `IndexedList` compare with the published 1.0.0, not
with `main`. They were checked in the 1.0.0 assembly by reflection (its list module is the class
`ImmutableListModule` with 75 functions, and `IndexedSeq` has 3), by compiling against it and by
running its functions next to the new ones.

- `Removed` names the 23 functions that 1.0.0 had and `IndexedList` lacks, each with its replacement.
  Thirteen renames that keep their arguments are one entry. `index`, `indexWith`, `indexFrom` and
  `indexFromWith` returned a function that took the list a second time, so their entry says that the
  replacements return the index. `skipUntil` and `takeUntil` returned each other's result, so their
  entry says which call returns what. `IndexedSeq` and `IIndexedSeq<'T>` point to `IReadOnlyList<'T>`,
  and `IndexedSeq.check` needs no replacement.
- `Changed` lists what running both showed: the compiled names (PascalCase, in the class
  `IndexedList`), the `ImmutableList<'T>` parameters where 1.0.0 took any `IImmutableList<'T>`, the
  arguments of `ofList`, `collect`, `concat` and `append`, the `voption` results, the exceptions and
  results of `head`, `last`, `tail`, `item`, `removeAt`, `take`, `skip`, `truncate`, `splitAt`,
  `forall2`, `contains` and `ofBuilder`, the swapped `skipWhile` and `takeWhile` of 1.0.0, `except`,
  the equality of `contains`, `remove`, `replace` and the `indexOf` family, the differences of the
  functions that replace renamed ones, and the speed of `rev`. It no longer says that a `null` list
  throws `ArgumentNullException` (it did in 1.0.0, except in `contains`, `reverse` and `reverseRange`),
  and it no longer lists the sorts and `tryPick`, which 1.0.0 did not have.
- `Added` no longer names `add`, `cons`, `remove`, `removeWith`, `replace`, `replaceWith` and
  `tryTail`, which 1.0.0 had, and its sentence about the wrapped members no longer describes the
  equality of `remove` and `replace`, which moved to `Changed`.
- The speed of `rev` is the measurement against the 1.0.0 `reverse` at 100 000 integers: about 3 ms
  against 23 ms, and 5.2 MB against 4.8 MB. The entry said about 2.5 times faster with the same
  allocation.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…lary of the FSharp.Core `List` module

The remark of `IndexedList.rev` and the comment in its body said that the function is about 2.5
times faster than `ImmutableList.Reverse` at 100 000 elements, with the same allocation. Measured
against the 1.0.0 `reverse`, which calls `Reverse`, at 100 000 integers (Release, .NET 10, the
median of 15 rounds), `rev` takes about 3 ms and `Reverse` about 23 ms, and `rev` allocates the copy
of the elements in addition (5.2 MB against 4.8 MB). Only the remark and the comment change.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…lary of the FSharp.Core `List` module

A parameter of type `seq<'T>` rejects a default flat list that is given as a sequence, as it rejects
`null` (ADR 0001, D6). The default flat list is a boxed struct, so `checkNotNull` lets it through, and the
call fails with `InvalidOperationException` when it enumerates the sequence. A call of
`checkNotNullOrDefaultSeq` does both checks and names the parameter of the function, before anything else
runs:

- `IndexedList`: `ofSeq`, `concat`, `insertManyAt`, `removeMany`, `removeManyWith`, `transpose` and
  `except`. `removeMany` and `removeManyWith` did not check the items at all and left it to the member,
  which names a null sequence `items` but fails while it enumerates a default flat list.
- `IIndexedList`: the same functions except `ofSeq`. `concat` and `transpose` reject a default flat list
  as an element too, because a default flat list implements `IImmutableList<'T>` and can be one.
- `IndexedListBuilder`: `addRange`, `insertManyAt`, `removeMany` and `removeManyWith`. `insertManyAt`
  checks the index before the values, as the member and `FlatListBuilder.insertManyAt` do, so an index
  out of range wins over a missing sequence.
- `indexedList { }`: `yield!` of a sequence and `for`. The check runs when the expression calls the
  member, as in `flatList { }`; `for` did not check the sequence at all and failed with
  `NullReferenceException` for `null`.

The documentation of the affected parameters says "`null` or a default flat list", as `FlatList.fsi` does,
and the remarks of the modules say that a function that takes a sequence rejects a default flat list given
as a sequence. The audit covered every `seq<_>` and `IEnumerable<_>` parameter of the branch; the `Option`
and `Tuple` modules have none.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…table sorting contract

Tests of the sequence inputs, shared where every implementer has the function. The shared tests run for
`FlatList`, `IndexedList` and `IIndexedList`, and for `Queue`, `IQueue`, `Stack` and `IStack` once they
derive the contracts:

- `SeqCreationContract`: `ofSeq` rejects a default flat list given as a sequence with what it throws for
  a null sequence (`FlatList`, `IndexedList`, `Queue`, `Stack`).
- `SeqLikeTransformationContract`: `concat` and `transpose`, in one data-row test each, reject a null
  sequence of collections with `ArgumentNullException` that names the parameter of FSharp.Core (`arrays`
  for `Array`, `lists` for `List`, mapped to the name of the module), and reject a default flat list given
  as the sequence with what they throw for a null one. `List.concat` fails with `NullReferenceException`
  for `null`, so the test asserts the exception of the module and not the one of the oracle.
- `SeqLikePartitioningContract`: `except` rejects a default flat list given as the items to exclude with
  what it throws for null items, on an empty and on a non-empty input.
- `IndexedContract`: `insertManyAt` rejects null values and a default flat list with `ArgumentNullException`
  for `values`, and checks the index before the values as `Array.insertManyAt` and `List.insertManyAt` do.
  `Inputs.DefaultFlatListAsSeq` is the input of these tests.

Only the indexed list and its interface module have the next ones: `ListMembersContract` checks that
`removeMany` and `removeManyWith` reject null items and a default flat list, on an empty and on a
non-empty collection, and that they check the collection before the items.

The rest is per module, for what a contract over integers cannot say: the tables of `IndexedListTests`,
`IIndexedListTests` and `IndexedListBuilderTests` have the rows of a default flat list of strings given as
a sequence of objects, the order of the checks, and, for `IIndexedList`, a default flat list as an element
of the sequence of lists of `concat` and `transpose`. `IndexedListBuilderTests` replaces the table of null
values by one with a row for each function and each input, and checks that `insertManyAt` checks the index
before the values. `IndexedListComputationExpressionTests` has a table for `yield!` and `for`. The tests
that a flat list that is not default still passes as a sequence of objects are in the same classes.
`IIndexedListTests.insertManyAt rejects null values with the name of the parameter` is gone: the contract
of `insertManyAt` covers it for every module.

Breaking each fix once in a copy of the sources (28 changes: the check back to a null check, the check
removed, the checks in the old order, the check of an element removed) fails the tests in every case.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…e collection table

The changelog says that a function which takes a sequence rejects a default flat list given as a sequence
with `ArgumentNullException` that names the parameter (`IndexedList`, `IIndexedList`, `IndexedListBuilder`
and the `indexedList { }` expression), and that the 1.0.0 `ofSeq`, `concat` and `except` threw
`InvalidOperationException` for it, and returned for an empty list in `except`. Both statements were checked
by running the functions of the published 1.0.0 assembly and of this branch.

Co-Authored-By: Claude Sonnet 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>
Comment thread src/FSharp.Collections.Immutable/Helper/ListChecks.fs Outdated

[<CompiledName "AllPairs">]
let allPairs (list1 : IImmutableList<'T1>) (list2 : IImmutableList<'T2>) =
Arrays.allPairs structPair (Snapshot.ofSeq (nameof list1) list1) (Snapshot.ofSeq (nameof list2) list2)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Have you compared to non-snapshot implementations?

[<CompiledName "Choose">]
let choose (chooser : 'T -> 'U voption) (list : IImmutableList<'T>) =
checkNotNull (nameof list) list
let chosen = ResizeArray<'U>()

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

why not builder?

Comment on lines +359 to +361
Array.chunkBySize chunkSize (Snapshot.ofSeq (nameof list) list)
|> Array.map IndexedList.ofArray
|> IndexedList.ofArray

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

is it the most optimal implementation?

[<CompiledName "Collect">]
let collect (mapping : 'T -> IImmutableList<'U>) (list : IImmutableList<'T>) =
checkNotNull (nameof list) list
let collected = ResizeArray<'U>()

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

why not builder?

Comment on lines +13 to +14
|> IndexedList.toArray
|> Array.map IndexedList.toArray

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

looks stupid

/// <remarks>
/// The list is a balanced binary tree: reading, inserting, replacing or removing the element at an index takes O(log n).
/// </remarks>
type IndexedList<'T> = ImmutableList<'T>

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

what about IIndexedList interface alias?

let found : (int * IndexedList<int>) option = IndexedList.tryUncons list

// A tuple compares an array by reference, so the first element and the other elements are compared apart
match found with

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

use WantSome for all such cases in all tests

…lary of the FSharp.Core `List` module

The helpers of the branch get the documentation of `Arrays` and `ParallelArrays`: an explicit `<summary>`,
`<param>`, `<returns>`, `<exception>` and `<remarks>` for every function, and `<see cref>` instead of `<c>` for
every function and type of FSharp.Core. The remarks say why the function exists.

- `ListChecks`: every check names the functions of `List` that it serves, the exception that it throws (the
  type, the message and the parameter name) and, where a sibling check exists, why the two are separate
  (`item` and `removeAt` differ in the message, `replicate` and `nonNegative` in the value, `exists2` and
  `sameLengths` in the names, `zip3` and `map3` in the lengths, `notEmpty` and `notEmptySequence` in the
  word). `foldBack2` says why it has two branches, and `transpose` which pair of lists it reports, checked
  against `local.fs` of FSharp.Core. The private literal `IndexOutsideListBounds` says which five functions
  of `List` write it and why it is not in `ErrorStrings`.
- `ListArrays`: the summaries, the descriptions of the parameters and of the results are those of the
  functions of `List`. Each remark says why the function is not `Array.iter2` and its siblings: they compare
  the lengths before they call the callback and report arrays, where the functions of `List` call the
  callback for the common prefix first and report lists.
- `Snapshot`, `Lengths` (with its private `countUpTo`), `DescendingSorts` and `ListRanges`: parameters,
  results and exceptions, the reason for each function, and the functions that call it. The private literal
  of `ListRanges` said that its message is the one of System.Collections.Immutable; no resource of that
  assembly holds it. The runtime throws the message from ``List`1.BinarySearch`` with "array" and "source
  collection", and the literal says "list", so the comment says that.
- The three functions that the branch adds to `Raise` (`invalidArg2ListsDifferent`,
  `invalidArgRandomizerOutOfRange` and `invalidOpListEmpty`) replace one-line comments with backticks.

No code changed: without the doc comments, every file is identical to the file before. The build with
`--warnon:3390`, `dotnet fantomas --check` and a check of every `cref` against the generated XML
documentation, FSharp.Core and the reference assemblies are clean.

Co-Authored-By: Claude Sonnet 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>

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants