Skip to content

feat: Queue and IQueue modules with the vocabulary of the FSharp.Core List module - #33

Open
xperiandri wants to merge 16 commits into
feat/indexed-listfrom
feat/queue
Open

xperiandri wants to merge 16 commits into
feat/indexed-listfrom
feat/queue

Conversation

@xperiandri

@xperiandri xperiandri commented Oct 18, 2026 •

Copy link
Copy Markdown
Collaborator

Proposed Changes

Rewrites the legacy Queue module for ImmutableQueue<'T> and adds the interface module IQueue (IImmutableQueue<'T>) after ADR 0001 (#23). This pull request is stacked on #31 (feat/indexed-list).

  • Queue/Queue.fsi and Queue.fs: 124 functions, in the canonical category order. They are the Queue column of ADR Appendix A, over the elements in the order in which they leave the queue. Where List names a list, the parameters name the queue: queue, queue1, queue2, queue3, queues.
    • The members of ImmutableQueue come first (D5): Peek, Dequeue, Enqueue, IsEmpty. head, tail, uncons, skip, skipWhile, splitAt, take and truncate read only the elements that they need.
    • The searches from the front, the folds and the iterations walk the struct enumerator of the queue.
    • Everything else copies to an array, calls the Array function or a shared algorithm and creates the queue with ImmutableQueue.Create. The elements go to the front stack, so the queue dequeues them without a reversal.
    • Every sort is stable, as List.sort* is. sort, sortDescending, sortWith and sortByDescending use the Seq sorts of FSharp.Core, which share the stable sort of List, so a comparison or a projection that throws throws what List throws. sortBy and sortWithComparer use LINQ, which computes the keys first, as List.sortBy does. A queue of one element is returned without a call of the projection or the comparer.
    • The queue does not store its length, so length is O(n). A function that checks a count against the length reads the elements first.
    • Results that keep every element return the input itself. ofSeq returns an ImmutableQueue without a copy.
    • The extras that 1.0.0 did not have are enqueueMany, uncons, tryUncons, tryTail, skipLast, takeLast, sortWithComparer, ofReadOnlySpan and ofSpan. The struct partial patterns Cons and Nil replace the complete pattern Cons | Nil of 1.0.0.
  • IQueue/IQueue.fsi and IQueue.fs: 115 functions over IImmutableQueue<'T>: the functions of Queue without the creation functions, plus clear.
    • Results of the same element type keep the implementation of the input: Clear () plus Enqueue, or Dequeue.
    • append enqueues the second queue into the first one, so the collection that receives the elements keeps its implementation (D14), also when the first queue is empty. Queue.append returns the second queue then, because both are ImmutableQueues.
    • Results of another element type are a Queue. So are concat and transpose, because their inputs can have different implementations.
  • Queue/QueueTypes.fs holds the Queue<'T> and IQueue<'T> abbreviations, because the modules are root modules (D2). Queue.fsi and Queue.fs open FSharp.Collections.Immutable after System.Collections.Generic, whose Queue<T> would shadow the abbreviation.
  • Exceptions (D6): every function throws what the FSharp.Core List function throws: the type, the message and the parameter name of this module. This includes the quirks of List:
    • The functions of two and three queues call their callback on the common prefix before they throw.
    • exists2 and forall2 can return before they check the lengths.
    • except does not read the items to exclude for an empty queue.
    • take, skip and splitAt throw for a count beyond the length.
    • A comparison that throws in a sort gives an InvalidOperationException, and so does the projection of sortByDescending, which List calls inside the comparison. The projection of sortBy throws as it is.
    • A null input throws ArgumentNullException.
  • Sequence inputs (D6): every seq parameter rejects null and a default flat list given as a sequence, a boxed struct that the BCL would fail on while it enumerates it, with ArgumentNullException that names the parameter. checkNotNullOrDefaultSeq does it as the first statement of ofSeq, concat, transpose, enqueueMany and except of Queue and IQueue (which has no ofSeq) and of yield! and for in queue { }. The shared contracts of feat: IndexedList and IIndexedList modules with the vocabulary of the FSharp.Core List module #31 cover ofSeq, concat, transpose and except; the tests here add enqueueMany, the covariant sequences (a default flat list of strings as objects) and the expression.
  • Option and Tuple namespaces (D3): Option.Queue (14 functions) and Option.IQueue (13) repeat the functions with voption. Tuple.Queue (18) and Tuple.IQueue (17) repeat the functions with struct tuples. The per-element tuple producers share one algorithm with the default module; the other functions delegate and repack.
  • Computation expression (D7): queue { } builds a queue. An immutable queue has no builder, so the expression collects its elements in a ResizeArray and creates the queue at the end. The first element of the expression leaves the queue first. yield! rejects a null sequence, array or queue and a default flat list given as a sequence with ArgumentNullException for the parameter items, and for rejects a null sequence and a default flat list for the parameter source; the checks run when yield! or for is evaluated. QueueCE.fsi carries the signatures and the documentation, and queue has its val in ComputationExpressions.fsi.
  • Compiled names: the functions of Queue compile under PascalCase names, where 1.0.0 compiled the F# names, such as head and isEmpty. The queue operations use the .NET vocabulary in compiled code, in all six modules. head, tail and uncons compile as Peek, Dequeue and PeekDequeue; tryHead, tryTail and tryUncons as TryPeek, TryDequeue and TryPeekDequeue; enqueue and enqueueMany as Enqueue and EnqueueMany. The other functions use the compiled names of FSharp.Core.
  • Helper/Queues.fs:
  • Measured allocations (Release, net10.0, bytes per call):
    • Queue.tryFind, tryPick and findIndex: 0 B.
    • IQueue.tryFind, tryPick and findIndex of an ImmutableQueue: 0 B. The interface enumerator of ImmutableQueue would allocate 48 B.
    • The descending sorts of 10 000 integers with the inline Seq.sortDescending and Seq.sortByDescending, and then with DescendingSorts of feat: IndexedList and IIndexedList modules with the vocabulary of the FSharp.Core List module #31, which the modules call (median of 200 calls, five alternating processes, one process for each sort):
      • Queue.sortDescending: 7.6 MB and 2.2 ms, then 0.74 MB and 0.90 ms.
      • Queue.sortByDescending: 7.6 MB and 2.3 ms, then 0.74 MB and 1.2 ms.
      • IQueue.sortDescending: 7.8 MB and 1.9 ms, then 1.0 MB and 1.0 ms.
      • IQueue.sortByDescending: 7.8 MB and 2.2 ms, then 1.0 MB and 1.2 ms.
      • The control, Queue.sortWith, is the same in both builds: 0.74 MB, 0.91 and 0.89 ms.
  • Removed files: the legacy Queue.fs.
  • Documentation: the changelog, and the collection tables of README.md, docsSrc/index.md and docsSrc/Explanations/Background.md: queue { } for Queue and a new row for IQueue.

Tests: 9320 per target framework, 1488 of them new (768 for Queue, 720 for IQueue; #31 has 7832).

  • The tests compare arrays and take their cases as data rows. A row is a name or a value that prints in the name of the test, so a failing case does not stop the others.
  • Queue derives the sequence-like (461 tests), creation (31) and stable-sort (15) contracts. IQueue derives the sequence-like (461) and stable-sort (15) contracts, over immutable queues upcast to the interface. The sequence-like and creation contracts of feat: IndexedList and IIndexedList modules with the vocabulary of the FSharp.Core List module #31 include the sequence inputs: a null sequence and a default flat list given as a sequence to ofSeq, concat, transpose and except. The stable-sort contract also compares the exceptions of the sorts with List, checks the order in which sortByDescending calls the projection, and checks that the descending sorts of 10 000 integers allocate less than 3 MB, which the inline Seq functions do not meet.
  • QueueOps.paramName maps the parameter names of the List exceptions to the names of the queue modules, also in "list1, list2" and in the list.[i] names of List.transpose.
  • The tests of Queue (199) and IQueue (206) have one row for each case of a table: 114 and 115 functions that reject a null queue, 12 and 10 results that are the input itself, 38 functions of IQueue that keep the implementation, and 9 and 9 functions that reject a default flat list given as a sequence (also a default flat list of strings as objects, which a type test misses, and an empty queue for except). The tests of Queue also have 6 sorts of one element, 4 functions of two queues that walk a common prefix and 14 complete messages. They also cover:
    • the first-in, first-out order and queues whose elements lie in both stacks of ImmutableQueue;
    • the order of the callbacks before an exception;
    • the patterns, queue { } (20 tests, with yield! and for over a null sequence and a default flat list) and both namespaces (19, 17, 23 and 21 tests).
  • A custom IImmutableQueue implementation proves that IQueue keeps it. A second one counts its enumerators: the searches, take and truncate read it only as far as they need and dispose the enumerator.
  • Reflection proves the compiled names of all six modules. A test with strings proves that the sorts use F# generic comparison, which is ordinal.

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:

  • Queue works on Queue<'T> (ImmutableQueue<'T>) instead of IQueue<'T>, and enqueue, tail and filter return a Queue<'T>. The functions for any IImmutableQueue<'T> are in the new IQueue module.
  • tryFind, tryFindIndex and tryPick return voption, and pick and tryPick take functions that return voption.
  • The complete active pattern Cons | Nil, whose Cons case matched a reference tuple of the head and the tail, is replaced by the struct partial patterns Cons, which matches a struct tuple, and Nil.
  • head and tail of an empty queue throw ArgumentException, as List.head and List.tail do, where they threw InvalidOperationException. iter2 of queues of different lengths throws ArgumentException, as List.iter2 does, where it stopped at the shorter queue.
  • forall and averageBy take the queue. They returned a function of a sequence before.
  • Queue.clear is removed: use Queue.empty, or IQueue.clear to keep the implementation of an interface value.
  • C# sees Enqueue, Peek and Dequeue and the other PascalCase compiled names, where it saw the F# names of 1.0.0, such as enqueue, head and tail.

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

  • The walks of Queues: they need an internal struct over the struct enumerator of ImmutableQueue. An inline loop with member constraints cannot use it: a constraint resolves only to members that are public everywhere, and IEnumerator<'T> has two Current properties. One loop over the interface enumerator would be simpler, at 48 B per call.
  • Shared code: only the helpers are shared. The bodies of exists, forall, takeWhile, skip and a few others look alike in both modules. Queue walks the struct enumerator and builds an ImmutableQueue; IQueue keeps the implementation of its input.
  • take and truncate read one element beyond the count. It tells them whether to return the input itself.
  • Parameter names of List: the modules report the unusual names of List exactly: "source" for exactlyOne, average, averageBy, contains and the random* functions; "count" for init.

🤖 Generated with Claude Code

Copilot AI balanced review requested due to automatic review settings October 18, 2026 15:38

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Needs a closer look

The broad breaking API rewrite needs human compatibility and cross-framework validation, with exception parity still unresolved.

1 open issue
What changed in this PR

Rewrites the queue bindings around FSharp.Core List vocabulary, continuing ADR 0001’s per-collection module design.

Changes:

  • Adds concrete and interface queue APIs with List-compatible semantics.
  • Adds Option/Tuple variants, shared enumeration helpers, and queue { }.
  • Expands contract tests and documents breaking changes.
File Description
tests/​FSharp.Collections.Immutable.Tests/​Queue/​QueueTupleTests.fs Tests reference-tuple operations.
tests/​FSharp.Collections.Immutable.Tests/​Queue/​QueueTests.fs Tests queue behavior and exceptions.
tests/​FSharp.Collections.Immutable.Tests/​Queue/​QueueOptionTests.fs Tests option-returning operations.
tests/​FSharp.Collections.Immutable.Tests/​Queue/​QueueOps.fs Connects Queue to shared contracts.
tests/​FSharp.Collections.Immutable.Tests/​Queue/​QueueContractTests.fs Instantiates queue contract suites.
tests/​FSharp.Collections.Immutable.Tests/​Queue/​QueueComputationExpressionTests.fs Tests computation expression behavior.
tests/​FSharp.Collections.Immutable.Tests/​Queue/​IQueueTupleTests.fs Tests interface tuple variants.
tests/​FSharp.Collections.Immutable.Tests/​Queue/​IQueueTests.fs Tests implementation preservation and enumeration.
tests/​FSharp.Collections.Immutable.Tests/​Queue/​IQueueOptionTests.fs Tests interface option variants.
tests/​FSharp.Collections.Immutable.Tests/​Queue/​IQueueOps.fs Connects IQueue to shared contracts.
tests/​FSharp.Collections.Immutable.Tests/​FSharp.Collections.Immutable.Tests.fsproj Registers queue tests.
src/​FSharp.Collections.Immutable/​Queue/​QueueTypes.fs Defines queue type abbreviations.
src/​FSharp.Collections.Immutable/​Queue/​QueueTuple.fsi Declares tuple-variant API.
src/​FSharp.Collections.Immutable/​Queue/​QueueTuple.fs Implements tuple variants.
src/​FSharp.Collections.Immutable/​Queue/​QueueOption.fsi Declares option-variant API.
src/​FSharp.Collections.Immutable/​Queue/​QueueOption.fs Implements option variants.
src/​FSharp.Collections.Immutable/​Queue/​QueueCE.fs Implements the queue expression builder.
src/​FSharp.Collections.Immutable/​Queue/​IQueueTuple.fsi Declares interface tuple variants.
src/​FSharp.Collections.Immutable/​Queue/​IQueueTuple.fs Implements interface tuple variants.
src/​FSharp.Collections.Immutable/​Queue/​IQueueOption.fsi Declares interface option variants.
src/​FSharp.Collections.Immutable/​Queue/​IQueueOption.fs Implements interface option variants.
src/​FSharp.Collections.Immutable/​Queue/​IQueue.fs Implements interface queue operations.
src/​FSharp.Collections.Immutable/​Queue.fs Removes the legacy module.
src/​FSharp.Collections.Immutable/​Helper.fs Adds shared queue construction and traversal.
src/​FSharp.Collections.Immutable/​FSharp.Collections.Immutable.fsproj Registers the new queue files.
src/​FSharp.Collections.Immutable/​ComputationExpressions.fs Exposes queue { }.
README.md Updates supported collection table.
docsSrc/​index.md Lists interface and expression support.
docsSrc/​Explanations/​Background.md Updates queue support documentation.
CHANGELOG.md Records additions and breaking changes.

Review effort: Balanced

Comment thread src/FSharp.Collections.Immutable/Queue/IQueue.fs Outdated
xperiandri added a commit that referenced this pull request Oct 18, 2026
…FSharp.Core `List` module

Follows the review of #33 and ADR 0001 (D6, D7, D14):

- `sort`, `sortByDescending`, `sortDescending` and `sortWith` of `Queue` and `IQueue` 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.
  `OrderByDescending` differed for the projection, which `List.sortByDescending` calls inside the comparison. `sortBy`
  and `sortWithComparer` keep LINQ.
- `sortDescending` and `sortByDescending` call `Seq.sortWith` with the cached comparer of F# generic comparison, through
  the new `Queues.sortDescending` and `Queues.sortByDescending`. `Seq.sortDescending` and `Seq.sortByDescending` are
  inline: inside a generic function they compare with the generic operator, which boxes the elements of a value type in
  every comparison. For 10 000 integers, they allocate 7.8 MB per call instead of 0.74 MB.
- At 10 000 integers, `sortWith` takes about the same time as before and allocates 743 KB per call instead of 440 KB.
- `QueueCE.fsi` carries the signatures and the documentation of `QueueCE`, compiled right before it. `queue` has its
  `val` and its example in `ComputationExpressions.fsi`, after `indexedList`, and only the binding stays in the `.fs`.
- `queue { yield! ... }` rejects a null sequence, array or queue with `ArgumentNullException` for the parameter `items`.
- The remarks of `IQueue.append` say that the result keeps the implementation of the first queue, which receives the
  elements (D14), also when that queue is empty.

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 review of #33 and ADR 0001 (D6, D7, D14):

- `sort`, `sortByDescending`, `sortDescending` and `sortWith` of `Queue` and `IQueue` 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.
  `OrderByDescending` differed for the projection, which `List.sortByDescending` calls inside the comparison. `sortBy`
  and `sortWithComparer` keep LINQ.
- `sortDescending` and `sortByDescending` call `Seq.sortWith` with the cached comparer of F# generic comparison, through
  the new `Queues.sortDescending` and `Queues.sortByDescending`. `Seq.sortDescending` and `Seq.sortByDescending` are
  inline: inside a generic function they compare with the generic operator, which boxes the elements of a value type in
  every comparison. For 10 000 integers, they allocate 7.8 MB per call instead of 0.74 MB.
- At 10 000 integers, `sortWith` takes about the same time as before and allocates 743 KB per call instead of 440 KB.
- `QueueCE.fsi` carries the signatures and the documentation of `QueueCE`, compiled right before it. `queue` has its
  `val` and its example in `ComputationExpressions.fsi`, after `indexedList`, and only the binding stays in the `.fs`.
- `queue { yield! ... }` rejects a null sequence, array or queue with `ArgumentNullException` for the parameter `items`.
- The remarks of `IQueue.append` say that the result keeps the implementation of the first queue, which receives the
  elements (D14), also when that queue is empty.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@xperiandri
xperiandri requested a balanced review from Copilot October 18, 2026 19:40

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Needs a closer look

The broad breaking API rewrite and specialized traversal paths warrant human validation of behavior and test results.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

xperiandri added a commit that referenced this pull request Oct 18, 2026
…FSharp.Core `List` module

Follows the review of #33 and ADR 0001 (D6, D7, D14):

- `sort`, `sortByDescending`, `sortDescending` and `sortWith` of `Queue` and `IQueue` 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.
  `OrderByDescending` differed for the projection, which `List.sortByDescending` calls inside the comparison. `sortBy`
  and `sortWithComparer` keep LINQ.
- `sortDescending` and `sortByDescending` call `Seq.sortWith` with the cached comparer of F# generic comparison, through
  the new `Queues.sortDescending` and `Queues.sortByDescending`. `Seq.sortDescending` and `Seq.sortByDescending` are
  inline: inside a generic function they compare with the generic operator, which boxes the elements of a value type in
  every comparison. For 10 000 integers, they allocate 7.8 MB per call instead of 0.74 MB.
- At 10 000 integers, `sortWith` takes about the same time as before and allocates 743 KB per call instead of 440 KB.
- `QueueCE.fsi` carries the signatures and the documentation of `QueueCE`, compiled right before it. `queue` has its
  `val` and its example in `ComputationExpressions.fsi`, after `indexedList`, and only the binding stays in the `.fs`.
- `queue { yield! ... }` rejects a null sequence, array or queue with `ArgumentNullException` for the parameter `items`.
- The remarks of `IQueue.append` say that the result keeps the implementation of the first queue, which receives the
  elements (D14), also when that queue is empty.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@xperiandri
xperiandri requested a balanced review from Copilot October 18, 2026 20:19

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Needs a closer look

The broad breaking API rewrite and strict FSharp.Core compatibility guarantees require final maintainer validation across target frameworks.

No open issues 🎉

Review effort: Balanced

xperiandri added a commit that referenced this pull request Oct 19, 2026
…FSharp.Core `List` module

Follows the review of #33 and ADR 0001 (D6, D7, D14):

- `sort`, `sortByDescending`, `sortDescending` and `sortWith` of `Queue` and `IQueue` 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.
  `OrderByDescending` differed for the projection, which `List.sortByDescending` calls inside the comparison. `sortBy`
  and `sortWithComparer` keep LINQ.
- `sortDescending` and `sortByDescending` call `Seq.sortWith` with the cached comparer of F# generic comparison, through
  the new `Queues.sortDescending` and `Queues.sortByDescending`. `Seq.sortDescending` and `Seq.sortByDescending` are
  inline: inside a generic function they compare with the generic operator, which boxes the elements of a value type in
  every comparison. For 10 000 integers, they allocate 7.8 MB per call instead of 0.74 MB.
- At 10 000 integers, `sortWith` takes about the same time as before and allocates 743 KB per call instead of 440 KB.
- `QueueCE.fsi` carries the signatures and the documentation of `QueueCE`, compiled right before it. `queue` has its
  `val` and its example in `ComputationExpressions.fsi`, after `indexedList`, and only the binding stays in the `.fs`.
- `queue { yield! ... }` rejects a null sequence, array or queue with `ArgumentNullException` for the parameter `items`.
- The remarks of `IQueue.append` say that the result keeps the implementation of the first queue, which receives the
  elements (D14), also when that queue is empty.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@xperiandri
xperiandri requested a balanced review from Copilot October 19, 2026 10:37

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Needs a closer look

The broad breaking API rewrite, exception contracts, and custom enumerator paths warrant human review and build/test validation.

No open issues 🎉

Review effort: Balanced

xperiandri added a commit that referenced this pull request Oct 19, 2026
…FSharp.Core `List` module

Follows the review of #33 and ADR 0001 (D6, D7, D14):

- `sort`, `sortByDescending`, `sortDescending` and `sortWith` of `Queue` and `IQueue` 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.
  `OrderByDescending` differed for the projection, which `List.sortByDescending` calls inside the comparison. `sortBy`
  and `sortWithComparer` keep LINQ.
- `sortDescending` and `sortByDescending` call `Seq.sortWith` with the cached comparer of F# generic comparison, through
  the new `Queues.sortDescending` and `Queues.sortByDescending`. `Seq.sortDescending` and `Seq.sortByDescending` are
  inline: inside a generic function they compare with the generic operator, which boxes the elements of a value type in
  every comparison. For 10 000 integers, they allocate 7.8 MB per call instead of 0.74 MB.
- At 10 000 integers, `sortWith` takes about the same time as before and allocates 743 KB per call instead of 440 KB.
- `QueueCE.fsi` carries the signatures and the documentation of `QueueCE`, compiled right before it. `queue` has its
  `val` and its example in `ComputationExpressions.fsi`, after `indexedList`, and only the binding stays in the `.fs`.
- `queue { yield! ... }` rejects a null sequence, array or queue with `ArgumentNullException` for the parameter `items`.
- The remarks of `IQueue.append` say that the result keeps the implementation of the first queue, which receives the
  elements (D14), also when that queue is empty.

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 review of #33 and ADR 0001 (D6, D7, D14):

- `sort`, `sortByDescending`, `sortDescending` and `sortWith` of `Queue` and `IQueue` 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.
  `OrderByDescending` differed for the projection, which `List.sortByDescending` calls inside the comparison. `sortBy`
  and `sortWithComparer` keep LINQ.
- `sortDescending` and `sortByDescending` call `Seq.sortWith` with the cached comparer of F# generic comparison, through
  the new `Queues.sortDescending` and `Queues.sortByDescending`. `Seq.sortDescending` and `Seq.sortByDescending` are
  inline: inside a generic function they compare with the generic operator, which boxes the elements of a value type in
  every comparison. For 10 000 integers, they allocate 7.8 MB per call instead of 0.74 MB.
- At 10 000 integers, `sortWith` takes about the same time as before and allocates 743 KB per call instead of 440 KB.
- `QueueCE.fsi` carries the signatures and the documentation of `QueueCE`, compiled right before it. `queue` has its
  `val` and its example in `ComputationExpressions.fsi`, after `indexedList`, and only the binding stays in the `.fs`.
- `queue { yield! ... }` rejects a null sequence, array or queue with `ArgumentNullException` for the parameter `items`.
- The remarks of `IQueue.append` say that the result keeps the implementation of the first queue, which receives the
  elements (D14), also when that queue is empty.

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 review of #33 and ADR 0001 (D6, D7, D14):

- `sort`, `sortByDescending`, `sortDescending` and `sortWith` of `Queue` and `IQueue` 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.
  `OrderByDescending` differed for the projection, which `List.sortByDescending` calls inside the comparison. `sortBy`
  and `sortWithComparer` keep LINQ.
- `sortDescending` and `sortByDescending` call `Seq.sortWith` with the cached comparer of F# generic comparison, through
  the new `Queues.sortDescending` and `Queues.sortByDescending`. `Seq.sortDescending` and `Seq.sortByDescending` are
  inline: inside a generic function they compare with the generic operator, which boxes the elements of a value type in
  every comparison. For 10 000 integers, they allocate 7.8 MB per call instead of 0.74 MB.
- At 10 000 integers, `sortWith` takes about the same time as before and allocates 743 KB per call instead of 440 KB.
- `QueueCE.fsi` carries the signatures and the documentation of `QueueCE`, compiled right before it. `queue` has its
  `val` and its example in `ComputationExpressions.fsi`, after `indexedList`, and only the binding stays in the `.fs`.
- `queue { yield! ... }` rejects a null sequence, array or queue with `ArgumentNullException` for the parameter `items`.
- The remarks of `IQueue.append` say that the result keeps the implementation of the first queue, which receives the
  elements (D14), also when that queue is empty.

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 review of #33 and ADR 0001 (D6, D7, D14):

- `sort`, `sortByDescending`, `sortDescending` and `sortWith` of `Queue` and `IQueue` 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.
  `OrderByDescending` differed for the projection, which `List.sortByDescending` calls inside the comparison. `sortBy`
  and `sortWithComparer` keep LINQ.
- `sortDescending` and `sortByDescending` call `Seq.sortWith` with the cached comparer of F# generic comparison, through
  the new `Queues.sortDescending` and `Queues.sortByDescending`. `Seq.sortDescending` and `Seq.sortByDescending` are
  inline: inside a generic function they compare with the generic operator, which boxes the elements of a value type in
  every comparison. For 10 000 integers, they allocate 7.8 MB per call instead of 0.74 MB.
- At 10 000 integers, `sortWith` takes about the same time as before and allocates 743 KB per call instead of 440 KB.
- `QueueCE.fsi` carries the signatures and the documentation of `QueueCE`, compiled right before it. `queue` has its
  `val` and its example in `ComputationExpressions.fsi`, after `indexedList`, and only the binding stays in the `.fs`.
- `queue { yield! ... }` rejects a null sequence, array or queue with `ArgumentNullException` for the parameter `items`.
- The remarks of `IQueue.append` say that the result keeps the implementation of the first queue, which receives the
  elements (D14), also when that queue is empty.

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 review of #33 and ADR 0001 (D6, D7, D14):

- `sort`, `sortByDescending`, `sortDescending` and `sortWith` of `Queue` and `IQueue` 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.
  `OrderByDescending` differed for the projection, which `List.sortByDescending` calls inside the comparison. `sortBy`
  and `sortWithComparer` keep LINQ.
- `sortDescending` and `sortByDescending` call `Seq.sortWith` with the cached comparer of F# generic comparison, through
  the new `Queues.sortDescending` and `Queues.sortByDescending`. `Seq.sortDescending` and `Seq.sortByDescending` are
  inline: inside a generic function they compare with the generic operator, which boxes the elements of a value type in
  every comparison. For 10 000 integers, they allocate 7.8 MB per call instead of 0.74 MB.
- At 10 000 integers, `sortWith` takes about the same time as before and allocates 743 KB per call instead of 440 KB.
- `QueueCE.fsi` carries the signatures and the documentation of `QueueCE`, compiled right before it. `queue` has its
  `val` and its example in `ComputationExpressions.fsi`, after `indexedList`, and only the binding stays in the `.fs`.
- `queue { yield! ... }` rejects a null sequence, array or queue with `ArgumentNullException` for the parameter `items`.
- The remarks of `IQueue.append` say that the result keeps the implementation of the first queue, which receives the
  elements (D14), also when that queue is empty.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
xperiandri and others added 16 commits October 19, 2026 20:09
…Core `List` module

`Queue` (`ImmutableQueue<'T>`) replaces the old module, which worked on `IImmutableQueue<'T>`. It has every function of
`List` that applies to an immutable queue, over the elements in the order in which they leave the queue, with the checks
and the exceptions of `List` and with stable sorts. Its parameters and exceptions name the queues `queue`, `queue1`,
`queue2`, `queue3` and `queues` where `List` names its lists. The extras of ADR 0001 are `enqueue`, `enqueueMany`,
`uncons`, `tryUncons`, the struct partial patterns `Cons` and `Nil`, `ofReadOnlySpan`, `ofSpan`, `skipLast`, `takeLast`,
`tryTail` and `sortWithComparer`; `enqueue`, `head`, `tail` and `uncons` compile as `Enqueue`, `Peek`, `Dequeue` and
`PeekDequeue`.

`IQueue` has the same functions for `IImmutableQueue<'T>` without the creation functions, plus `clear`. A result with the
element type of the input keeps the implementation of the input through `Clear` and `Enqueue` or through `Dequeue`; the
other results are immutable queues. The `Option` and `Tuple` namespaces repeat the functions that use value options or
struct tuples, and `queue { }` builds a queue.

A queue does not store its length, so `length` is O(n). `head`, `tail`, `skip`, `skipWhile`, `take`, `truncate` and
`splitAt` walk only the elements that they need, and the functions that check against the length take a snapshot first.
The `ImmutableQueue` module alias of the old module is removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sequence-like, creation and stable sorting contracts run for `Queue`, and the sequence-like and stable sorting
contracts for `IQueue`, over immutable queues seen through the interface. `QueueOps.paramName` maps the names that the
`List` exceptions report to the names of the queue modules, also inside the names of several parameters and the
`list.[i]` names of `List.transpose`.

Further tests cover null inputs, the first-in, first-out order, `enqueue` and `enqueueMany`, `head`, `tail`, `uncons`
and `tryUncons`, the `Cons` and `Nil` patterns, queues whose elements lie in both stacks of `ImmutableQueue`, the results
that are their inputs, the order in which the functions of two queues call their callbacks before they throw, the
complete messages of the `List` exceptions with the names of the queues, the implementation that `IQueue` keeps,
`queue { }` and the `Option` and `Tuple` namespaces.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The changelog lists the new functions of `Queue`, the new `IQueue` module, the `Option` and `Tuple` modules and
`queue { }`, the breaking changes of the `List` semantics, the queue type of the module and the patterns, the fixed
`forall` and `averageBy`, and the removed `clear` and `ImmutableQueue` alias. The collection tables name `queue { }` for
`Queue` and add `IQueue`.

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

Follows the reworked ADR 0001 (D2, D5) and the answers of the maintainer:

- `Queue`, `IQueue` and their `Option` and `Tuple` modules are the root modules of their files. The `Queue<'T>` and
  `IQueue<'T>` abbreviations move to `Queue/QueueTypes.fs`, which needs no signature file. `Queue.fsi` and `Queue.fs`
  open `FSharp.Collections.Immutable` after `System.Collections.Generic`, whose `Queue<T>` would shadow the abbreviation.
- `tryHead`, `tryTail` and `tryUncons` compile as `TryPeek`, `TryDequeue` and `TryPeekDequeue` in all six modules, next
  to `Peek`, `Dequeue` and `PeekDequeue` for `head`, `tail` and `uncons` and `EnqueueMany` for `enqueueMany`.
- The sorts of both modules compare with the cached `DefaultComparer<'T>.Instance`, so a call no longer creates a
  comparer.
- The checks count a queue with `Lengths.queueUpTo` instead of the private `lengthUpToOne` and `lengthUpToTwo`.
- `Helper.fs` gets the internal `Queues` module with the helpers that the modules repeated: `ofFreshArray`, `ofBuffer`,
  `lastMatch` and the walks `firstMatch`, `firstChoice` and `front`. A walk takes the interface and runs one generic loop
  over the struct enumerator of an `ImmutableQueue` or over the enumerator of another queue, which it disposes. The walks
  of `Queue` still allocate nothing, and the ones of `IQueue` no longer allocate an enumerator for an `ImmutableQueue`.
- The remarks of `IQueue.append` say that the result keeps the implementation of the first queue, also when that queue
  is empty.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Follows the reworked ADR 0001 (D2, D8):

- Test methods carry no categories: the classes carry them. The classes with the tests of the `Cons` and `Nil` patterns
  carry `PatternMatchingTestCategory`.
- `QueueOps` and `IQueueOps` are the root modules of their own files.
- The stable sorting contract runs on the 20 pairs of `Inputs.manyPairs` for both modules.
- New tests, with arrays and data rows: the queue functions of all six modules compile with the names of the queue
  operations of the BCL; the sorts of both modules order strings in F# generic comparison; the searches, `take` and
  `truncate` of `IQueue` read a queue of another implementation only as far as they need and dispose its enumerator.

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

Follows the review of #33 and ADR 0001 (D6, D7, D14):

- `sort`, `sortByDescending`, `sortDescending` and `sortWith` of `Queue` and `IQueue` 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.
  `OrderByDescending` differed for the projection, which `List.sortByDescending` calls inside the comparison. `sortBy`
  and `sortWithComparer` keep LINQ.
- `sortDescending` and `sortByDescending` call `Seq.sortWith` with the cached comparer of F# generic comparison, through
  the new `Queues.sortDescending` and `Queues.sortByDescending`. `Seq.sortDescending` and `Seq.sortByDescending` are
  inline: inside a generic function they compare with the generic operator, which boxes the elements of a value type in
  every comparison. For 10 000 integers, they allocate 7.8 MB per call instead of 0.74 MB.
- At 10 000 integers, `sortWith` takes about the same time as before and allocates 743 KB per call instead of 440 KB.
- `QueueCE.fsi` carries the signatures and the documentation of `QueueCE`, compiled right before it. `queue` has its
  `val` and its example in `ComputationExpressions.fsi`, after `indexedList`, and only the binding stays in the `.fs`.
- `queue { yield! ... }` rejects a null sequence, array or queue with `ArgumentNullException` for the parameter `items`.
- The remarks of `IQueue.append` say that the result keeps the implementation of the first queue, which receives the
  elements (D14), also when that queue is empty.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The tests compare arrays and take their cases as data rows, as the converted contracts of `IndexedList` do (3751 tests per
target framework; 742 of them for `Queue` and 702 for `IQueue`, instead of 258 and 229):

- `QueueOps` and `IQueueOps` observe a collection with its `toArray`, and results of another element type are arrays;
  `ToList` is the `toList` under test. The stable sorting contract runs for both modules, with the new tests of the
  exceptions of the sorts against `List`.
- The case tables of `QueueTests` and `IQueueTests` are maps whose names are the rows: 114 and 115 functions that reject a
  null queue, 5 that reject another null input, 12 and 10 results that are the input itself, 38 functions of `IQueue` that
  keep the implementation, 6 sorts of one element, 4 functions of two queues that walk a common prefix and 14 complete
  messages. The names tell apart the cases that repeated a function: the two parts of `partition` and `splitAt`, `append`
  to an empty queue and the two ways to keep every element with `skip`.
- 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` and the exceptions keep their `List`
  oracles. Tuples that hold arrays are compared by their parts.
- `queue { yield! ... }` rejects a null sequence, array or queue.
- The descending sorts of 10 000 integers allocate less than 3 MB. `Seq.sortDescending` and `Seq.sortByDescending` take
  7.8 MB, because they box the integers in every comparison.

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

The changelog names the compiled names of the queue operations, as it does for `Stack`, and says that `IQueue.append`
keeps the implementation of the first queue.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The tests call the assertion helpers of `main`, which are static classes: `ExceptionAssert.Same` replaces
`ExceptionAssert.same`, and `OverrideAssert.OptionMatches`, `PairMatches` and `TripleMatches` replace the module
functions. The calls are tupled and their messages come last (25 calls in 6 files).

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

`sortDescending` and `sortByDescending` of `Queue` and `IQueue` call `DescendingSorts` of `Helper.fs`, which the sorts of
`IndexedList` share, instead of copies in `Queues`. The module runs `Seq.sortWith` with the cached comparer of F# generic
comparison, because the inline `Seq.sortDescending` and `Seq.sortByDescending` box the elements of a value type in every
comparison. For 10 000 integers, the sorts allocate 0.74 MB per call instead of 7.6 MB and take about half the time.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
`QueueOps` and `IQueueOps` give the stable sorting contract the two functions that sort integers, so that it checks for
`Queue` and `IQueue` the allocation of the descending sorts and the order in which `sortByDescending` calls the
projection. The allocation test of `QueueTests` and `IQueueTests` and its helper are removed, because the contract runs
the same check (743 tests for `Queue` and 703 for `IQueue`).

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

The `Added`, `Changed` and `Removed` entries name only what the published 1.0.0 had or did, checked in
the 1.0.0 assembly by reflection and by running its functions: its `Queue` module has `average`,
`averageBy`, `clear`, `empty`, `enqueue`, `exists`, `filter`, `find`, `findIndex`, `fold`, `forall`,
`head`, `isEmpty`, `iter`, `iter2`, `iteri`, `ofSeq`, `pick`, `reduce`, `singleton`, `sum`, `sumBy`,
`tail`, `tryFind`, `tryFindIndex`, `tryPick` and the complete active pattern `Cons | Nil`.

- `Removed` keeps `clear`, with its replacements. The `ImmutableQueue` module alias goes: an
  abbreviation of a module cannot be used from another assembly.
- `Changed` compares with 1.0.0. The entry about `voption` names `tryFind`, `tryFindIndex` and
  `tryPick`, the only `try*` functions of 1.0.0, and the complete pattern `Cons | Nil` is described by
  what its `Cons` case matched, a reference tuple. `iter2` of queues of different lengths stopped at
  the shorter queue in 1.0.0 and now throws `ArgumentException`. A `null` queue threw
  `ArgumentNullException` in 1.0.0 too, so that is no longer listed. The compiled names are a change:
  1.0.0 compiled the F# names, so `enqueue`, `head` and `tail` now compile to `Enqueue`, `Peek` and
  `Dequeue`, and the other functions to PascalCase names.
- `Added` no longer names `enqueue` and the patterns `Cons` and `Nil`, which 1.0.0 had. Its sentence
  about compiled names names only the functions that 1.0.0 did not have: `enqueueMany`, `uncons`,
  `tryHead`, `tryTail` and `tryUncons`.

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

The `Removed` entry of `Queue.clear` names the replacement for an interface value, as the entry of
`ImmutableList.clear` does: `IQueue.clear` keeps the implementation of the queue that it clears, and
`Queue.empty` creates the default one.

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

A function that takes a sequence rejects a default flat list given as the sequence as it rejects a null
sequence: `ArgumentNullException` that names the parameter, from `checkNotNullOrDefaultSeq` as the first
statement (ADR 0001, D6). The default flat list is a boxed struct, not null, so it reached the BCL and
failed with `InvalidOperationException` while the call enumerated it, or was not read at all.

- `Queue.ofSeq` (`source`), `concat` and `transpose` (`queues`), `enqueueMany` (`values`) and `except`
  (`itemsToExclude`), and the same four functions of `IQueue` (it has no `ofSeq`). `except` checks the
  items before it looks at an empty queue, as `List.except` does, so a default flat list is rejected for
  an empty queue too. The checks also see a default flat list of a derived reference type that arrives as
  a sequence of its base type, such as strings as objects.
- `queue { }`: `yield!` of a sequence and `for` reject a default flat list. The checks of `yield!` of a
  sequence, an array and a queue now run when `yield!` is evaluated, before the buffer is touched,
  instead of inside the function that it returns, so that the builder members themselves reject their
  input. The array check stays needed: `AddRange` of a null array converts it to an empty span and adds
  nothing.
- The documentation of these functions says "null or a default flat list" the way `IndexedList` does.

The other parameters of the queue modules are not sequences: the queue parameters are classes or
interfaces that a flat list cannot be, and `ofArray` already checks its array.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The queue modules inherit the shared sequence-input tests: `ofSeq` (`Queue`), `concat`, `transpose` and
`except` reject a default flat list given as the sequence as they reject a null sequence. This adds the
rows that no shared contract has, as data rows with the names of the functions:

- `QueueTests` and `IQueueTests`: each function that takes a sequence with a default flat list given as
  the sequence, with the parameter that names it (9 rows each): `ofSeq`, `concat`, `transpose`,
  `enqueueMany` and `except` for `Queue`; `concat`, `transpose`, `enqueueMany` and `except` for `IQueue`.
  Rows with a default flat list of strings as a sequence of objects, and with a default flat list of
  immutable queues as a sequence of the interface, check the covariant sequences that a type test for the
  flat list of the element type does not see. `except` is also checked on an empty queue.
- A flat list of strings that is not default is still taken as a sequence of objects.
- `QueueComputationExpressionTests`: `yield!` and `for` with a null sequence, a default flat list and a
  default flat list of strings as objects (6 rows), and the same flat list of strings that is not default.

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

The changelog says what the sequence-input contract does for the queue modules: a function that takes a
sequence throws `ArgumentNullException` that names the parameter for a `null` sequence and for a default
`FlatList` given as a sequence, in `Queue`, `IQueue` and `queue { }`. Of these functions only `ofSeq`
exists in 1.0.0, where a default `FlatList` made it throw `InvalidOperationException` (checked in the
1.0.0 assembly), so `Changed` names `ofSeq` only; the other functions are new.

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