Repository navigation
feat: Stack and IStack modules with the vocabulary of the FSharp.Core List module - #34
Open
xperiandri wants to merge 3 commits into
Open
xperiandri wants to merge 3 commits into
xperiandri wants to merge 3 commits into
Conversation
There was a problem hiding this comment.
🔵 Needs a closer look
The extensive breaking API rewrite needs final human verification of compatibility and List-equivalent behavior across target frameworks.
No open issues 🎉
What changed in this PR
Replaces the legacy stack bindings with List-style Stack and IStack APIs under ADR 0001, continuing the collection-module rewrite from #31.
Changes:
- Establishes top-to-bottom ordering, shared-node operations, stable sorting, and List-compatible exceptions.
- Adds Option/Tuple variants and the
stack { }computation expression. - Adds contract and collection-specific tests and documents breaking changes.
| File | Description |
|---|---|
| tests/FSharp.Collections.Immutable.Tests/Stack/StackTupleTests.fs | Tests reference-tuple overrides. |
| tests/FSharp.Collections.Immutable.Tests/Stack/StackTests.fs | Tests ordering, sharing, and exceptions. |
| tests/FSharp.Collections.Immutable.Tests/Stack/StackOptionTests.fs | Tests option overrides. |
| tests/FSharp.Collections.Immutable.Tests/Stack/StackOps.fs | Connects Stack to shared contracts. |
| tests/FSharp.Collections.Immutable.Tests/Stack/StackContractTests.fs | Instantiates both modules’ contracts. |
| tests/FSharp.Collections.Immutable.Tests/Stack/StackComputationExpressionTests.fs | Tests expression ordering and control flow. |
| tests/FSharp.Collections.Immutable.Tests/Stack/IStackTupleTests.fs | Tests interface tuple overrides. |
| tests/FSharp.Collections.Immutable.Tests/Stack/IStackTests.fs | Tests implementation preservation and interface behavior. |
| tests/FSharp.Collections.Immutable.Tests/Stack/IStackOptionTests.fs | Tests interface option overrides. |
| tests/FSharp.Collections.Immutable.Tests/Stack/IStackOps.fs | Connects IStack to shared contracts. |
| tests/FSharp.Collections.Immutable.Tests/FSharp.Collections.Immutable.Tests.fsproj | Includes new test files. |
| src/FSharp.Collections.Immutable/Stack/StackTypes.fs | Defines stack type abbreviations. |
| src/FSharp.Collections.Immutable/Stack/StackTuple.fsi | Declares reference-tuple APIs. |
| src/FSharp.Collections.Immutable/Stack/StackTuple.fs | Implements reference-tuple overrides. |
| src/FSharp.Collections.Immutable/Stack/StackOption.fsi | Declares option APIs. |
| src/FSharp.Collections.Immutable/Stack/StackOption.fs | Implements option overrides. |
| src/FSharp.Collections.Immutable/Stack/StackCE.fs | Implements the stack expression builder. |
| src/FSharp.Collections.Immutable/Stack/IStackTuple.fsi | Declares interface tuple APIs. |
| src/FSharp.Collections.Immutable/Stack/IStackTuple.fs | Implements interface tuple overrides. |
| src/FSharp.Collections.Immutable/Stack/IStackOption.fsi | Declares interface option APIs. |
| src/FSharp.Collections.Immutable/Stack/IStackOption.fs | Implements interface option overrides. |
| src/FSharp.Collections.Immutable/Stack/IStack.fs | Implements List-style interface operations. |
| src/FSharp.Collections.Immutable/Stack.fs | Removes legacy stack bindings. |
| src/FSharp.Collections.Immutable/Helper.fs | Shares order-preserving stack construction. |
| src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj | Registers replacement stack files. |
| src/FSharp.Collections.Immutable/ComputationExpressions.fs | Exposes stack { }. |
| README.md | Lists IStack and stack expressions. |
| docsSrc/index.md | Updates the collection overview. |
| docsSrc/Explanations/Background.md | Updates the collection reference table. |
| CHANGELOG.md | Records additions and breaking changes. |
Review effort: Balanced
xperiandri
force-pushed
the
feat/indexed-list
branch
from
October 18, 2026 18:49
56e9ddf to
e7a6257
Compare
xperiandri
force-pushed
the
feat/stack
branch
from
October 18, 2026 19:01
4ea17f7 to
0f1976d
Compare
xperiandri
force-pushed
the
feat/stack
branch
from
October 18, 2026 19:34
0f1976d to
738ffaa
Compare
xperiandri
force-pushed
the
feat/indexed-list
branch
from
October 18, 2026 20:12
430f3fb to
53a3555
Compare
xperiandri
force-pushed
the
feat/stack
branch
from
October 18, 2026 20:16
738ffaa to
d515088
Compare
xperiandri
force-pushed
the
feat/indexed-list
branch
from
October 18, 2026 23:05
53a3555 to
5c8b1f5
Compare
xperiandri
force-pushed
the
feat/stack
branch
from
October 18, 2026 23:16
d515088 to
090a0fa
Compare
xperiandri
force-pushed
the
feat/stack
branch
from
October 19, 2026 11:50
090a0fa to
cf7f578
Compare
xperiandri
force-pushed
the
feat/indexed-list
branch
from
October 19, 2026 14:49
67c2c1c to
4a66e46
Compare
xperiandri
force-pushed
the
feat/stack
branch
from
October 19, 2026 14:49
cf7f578 to
d0ee981
Compare
xperiandri
force-pushed
the
feat/indexed-list
branch
from
October 19, 2026 15:55
4a66e46 to
aa81bb5
Compare
xperiandri
force-pushed
the
feat/stack
branch
from
October 19, 2026 15:55
d0ee981 to
ca1052f
Compare
xperiandri
force-pushed
the
feat/indexed-list
branch
from
October 19, 2026 17:31
aa81bb5 to
6ce859c
Compare
xperiandri
force-pushed
the
feat/stack
branch
from
October 19, 2026 17:31
ca1052f to
9de058d
Compare
xperiandri
force-pushed
the
feat/indexed-list
branch
from
October 19, 2026 17:40
6ce859c to
d416a34
Compare
xperiandri
force-pushed
the
feat/stack
branch
2 times, most recently
from
October 19, 2026 18:10
d27df0a to
ebb8a2d
Compare
…Core `List` module
`Stack` (`ImmutableStack<'T>`) replaces the module of the same name. It has every function of `List` that applies to an
immutable stack, with the checks and the exceptions of `List` and with stable sorts, plus the extras of ADR 0001:
`consMany`, `ofReadOnlySpan`, `ofSpan`, `skipLast`, `sortWithComparer`, `takeLast`, `tryTail`, `tryUncons`, `uncons` and
the struct partial patterns `Cons` and `Nil`. Its functions name their stacks `stack`, `stack1` and `stacks` where `List`
names its lists, and the exceptions report these names. `cons`, `consMany`, `head`, `tail` and `uncons` compile to `Push`,
`PushMany`, `Peek`, `Pop` and `PeekPop`, and their `try` variants to `TryPeek`, `TryPop` and `TryPeekPop`.
A stack enumerates its elements from the top, and every function treats it as a list in that order. The creation
functions push the elements from the last one, so that `ofSeq` puts the first element on top and is the inverse of
`toList`; `ImmutableStack.CreateRange` would leave the last element on top. The transformations copy the elements to an
array, compute the result and push it back from its last element, and `rev` reverses the order. `tail`, `skip`,
`skipWhile`, `takeLast` and the second part of `splitAt` are the stack below the removed elements, without a copy, and
`cons`, `consMany` and `append` push onto the stack that the result shares.
`IStack` has the same functions for `IImmutableStack<'T>` without the creation functions, plus `clear`; a result with the
element type of the input keeps the implementation of the input, through `Pop`, or `Clear` followed by `Push`. The
`Option` and `Tuple` namespaces repeat the functions that use value options or struct tuples, and `stack { }` builds a
stack whose first element is on top. One internal loop, `StackConstruction.pushArray`, does the pushing for both modules
and the expression.
The legacy `Stack.fs` and its `ImmutableStack` module alias are removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sequence-like, creation and stable sorting contracts run for `Stack`, and the sequence-like and stable sorting
contracts for `IStack`, whose stacks are created by `Stack.ofSeq` and seen through the interface. The contracts compare
with `List` over the order in which a stack enumerates its elements, and their `ParamName` maps the names of the lists
that `List` reports, such as `list1` and `list.[1]`, to the names of the stacks.
Further tests cover the order of the elements: `ofSeq` and `toList` undo each other, every creation function and
`stack { }` put the first element on top, `cons` and `consMany` push on top, `rev` reverses, and `append` puts the first
stack on top of the second one. They also cover the nodes that `tail`, `skip`, `skipWhile`, `takeLast`, `splitAt`,
`cons`, `consMany` and `append` share, the inputs that the functions return themselves, null inputs, `uncons` and the
patterns, the complete messages of the `List` exceptions with the names of the stacks, the implementation that `IStack`
keeps for a custom `IImmutableStack<'T>`, `stack { }` and the `Option` and `Tuple` namespaces.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The changelog lists the new functions and modules, the order of the elements, which puts the first element on top and
which `rev` reverses, the breaking changes of the types, the exceptions and the patterns, and the removed functions and
module alias. The collection tables name `stack { }` for `Stack` and add `IStack`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri
force-pushed
the
feat/indexed-list
branch
from
October 20, 2026 16:27
178bba1 to
53b8a6b
Compare
xperiandri
force-pushed
the
feat/stack
branch
from
October 20, 2026 16:27
ebb8a2d to
e005058
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Proposed Changes
Rewrites the legacy
Stackmodule forImmutableStack<'T>and adds the interface moduleIStack(IImmutableStack<'T>) after ADR 0001 (#23). Both have the vocabulary of the FSharp.CoreListmodule. This pull request is stacked on #31 (feat/indexed-list).Stack,IStackand theirOptionandTuplemodules are root modules, such asmodule FSharp.Collections.Immutable.Stack. TheStack<'T>andIStack<'T>abbreviations live inStack/StackTypes.fs, which needs no signature file.StackCE.fsiincluded.stackhas itsvalinComputationExpressions.fsi, afterindexedListunder theCreationbanner.Stack.fsiandStack.fs: 124 functions, in the canonical category order. They are theStackcolumn of ADR Appendix A.cons,head,tailandunconscallPush,PeekandPop. They take O(1), and a result shares the nodes of its input.tail,skip,skipWhile,takeLastand the second part ofsplitAt. A function that keeps every element returns its input.Arrayfunction and push the result onto a new stack.List.sort*is, and compares in F# generic comparison, so strings sort ordinally.sortandsortWithsort with the FSharp.CoreSeqsorts, which share the stable sort ofList: they make the same comparisons, in the sameArray.Sort.sortDescendingandsortByDescendingcallDescendingSortsof feat:IndexedListandIIndexedListmodules with the vocabulary of the FSharp.CoreListmodule #31, which runsSeq.sortWithwith the comparer thatDefaultComparerof feat:FlatListmodule with the vocabulary of the FSharp.CoreArraymodule #26 caches, reversed. The inlineSeq.sortDescendingandSeq.sortByDescendingbox every element, or key, in every comparison once they are compiled into a generic function.sortByDescendingcalls the projection in the comparison, asList.sortByDescendingdoes.sortByandsortWithComparersort with LINQOrderBy/Order, which compute the keys first, asList.sortBydoes.sortBycompares the keys with the cachedDefaultComparer<'Key>.Instance.lengthis O(n), because a stack does not store its length. The checks count a stack withLengths.stackUpToof feat:IndexedListandIIndexedListmodules with the vocabulary of the FSharp.CoreListmodule #31, only as far as they need.cons,consMany,uncons,tryUncons,tryTail,skipLast,takeLast,sortWithComparer, the span constructors and the struct partial active patternsConsandNil.ofSeq,ofList,ofArray,ofSpan,ofReadOnlySpan,init,unfoldandstack { }put the first element on top, soofSeqis the inverse oftoList.ImmutableStack.CreateRangeandCreateput the last element on top, so the module does not wrap them.conspushes a value, which becomes the first element.consManypushes its values so that the first one ends on top.ofSeqof anImmutableStackreturns it as it is.IStack.fsiandIStack.fs: 115 functions overIImmutableStack<'T>: the functions ofStackwithout the creation functions, plusclear.Pop, orClear ()plusPushfrom the last element to the first one.Stack. So arecollect,concatandtranspose, because their stacks can have different implementations.Stack.appendkeeps the implementation and the nodes of the second stack (D14). The collection that receives the elements keeps its implementation.IStack.appendpushes the first stack onto the second one, asList.appendshares its second list, so the second stack receives the elements.IIndexedList.appendadds the second list to the first one, which keeps its implementation.Listfunction throws: the type, the message and the parameter name. The namesstack,stack1,stack2,stack3andstacksreplace the list names ofList.head,tailandunconsof an empty stack throw theArgumentExceptionofList.headandList.tail, not theInvalidOperationExceptionofImmutableStack.Liststay. The functions of two and three stacks call their callback for the same elements asListbefore they throw.exists2andforall2can return before they check the lengths.exceptdoes not read the excluded items for an empty stack.sort,sortDescending,sortWithorsortByDescendingthrows anInvalidOperationExceptionwith the original exception inside, asArray.Sortdoes inList.List.sortByDescendingcalls the projection in the comparison, and so do these modules now.sortBylets an exception of the projection escape, asList.sortBydoes.nullinput throwsArgumentNullException.ofSeq,concat,consMany,transposeandexceptofStack, the same withoutofSeqofIStack, andyield!andforofstack { }) throwsArgumentNullExceptionfor its own parameter when the sequence isnulland when it is a defaultFlatList, which would fail withInvalidOperationExceptionwhen the function enumerates it.StackandIStackmodules ofFSharp.Collections.Immutable.OptionandFSharp.Collections.Immutable.Tuplerepeat the functions withvoptionor struct tuples (D3).cons,consMany,head,tailandunconscompile toPush,PushMany,Peek,PopandPeekPop.tryHead,tryTailandtryUnconscompile toTryPeek,TryPopandTryPeekPop. TheIStack,OptionandTuplemodules compile the same way.stack { }collects the elements in aList<'T>and pushes them from the last one, so the first element of the expression ends on top.yield!takes a sequence, an array, a stack or another expression, and anullsequence, array or stack, or a defaultFlatListgiven as a sequence, throwsArgumentNullExceptionforitems;forthrows it forsource. The expression checks when it calls the member, asflatList { }does.Helper/StackConstruction.fsholds the internalStackConstructionmodule. ItspushArraypushes an array onto a stack from the last element. One inline function servesImmutableStack<'T>andIImmutableStack<'T>, so a result keeps the implementation of its input.Stack,IStack, theirTuplemodules and the computation expression build their stacks with it.Stack.fs.README.md,docsSrc/index.mdandBackground.md, listStackwithstack { }and the newIStack.Tests: 9302 per target framework, 1470 of them new.
DataRowandDynamicDatarows, so every case runs and reports as a test of its own.partitionandsplitAt.StackTeststakes 169 cases as rows,IStackTests156 andStackComputationExpressionTests15.Listfunctions whereArraybehaves differently (the walk of the common prefix,exists2/forall2,containsofNaN, the exceptions), as the function under test (ofList,toList) and as the list case ofyield!.Stackderives the sequence-like, creation and stable-sort contracts.IStackderives the sequence-like and stable-sort contracts, over stacks upcast to the interface. Their expectations come fromListover the elements from the top to the bottom.Inputs.ManyPairs, so an unstable sort fails it. It checks that a comparison or a projection that throws makes the sorts throw whatListthrows, and thatsortByDescendingcalls the projection in the comparisons, in the order ofList.sortByDescending.sortDescendingandsortByDescendingallocate less than 3 MB for 10 000 integers. The pipeline of the test (create the stack, sort it, read it) allocates 1.0 MB withDescendingSortsand 7.8 MB with the inlineSeqsorts. With the inline sorts put back, exactly the four allocation tests fail, two inStackand two inIStack.IndexedListandIIndexedListmodules with the vocabulary of the FSharp.CoreListmodule #31 (ofSeq,concat,transposeandexcept, inStackandIStack) and by a table inStackTests,IStackTestsandStackComputationExpressionTests:consMany, a default flat list of strings given as a sequence of objects, the order of the checks ofexcept, and the members of the builder called on their own. Breaking each fix once in a copy of the sources (27 changes) fails the tests in every case.IImmutableStackimplementation proves that 35 functions ofIStackkeep the implementation of their input. Data rows check thatIStack.appendkeeps the implementation and the nodes of the second stack, also when either stack is empty.CompiledNamefails the row of its function.ConsandNilpatterns also carryPatternMatchingTestCategory.StackOpsandIStackOpsare the root modules of their own files.nullinputs of every function, the complete messages of theListexceptions with the names of the stacks, the ordinal order of strings in the four sorts, the patterns, the computation expression,yield!andforofnulland of a defaultFlatList, and both namespaces.Types of changes
What types of changes does your code introduce to FSharp.Collections.Immutable?
Put an
xin the boxes that applyThese changes break existing code:
The list compares with the published 1.0.0, whose
Stackmodule hascons,empty,head,ofArray,ofSeq,peek,pop,push,tailand the complete active patternCons | Nil.Stack.cons,headandtailtook and returnedIImmutableStack<'T>. They take and returnImmutableStack<'T>, and theIStackmodule serves the interface. They compile toPush,PeekandPop, where 1.0.0 compiled them under their F# names.Stack.ofSeqandStack.ofArrayput the first element on top. Before, they wrappedImmutableStack.CreateRange, which puts the last element on top.Stack.ofSeqof a defaultFlatListgiven as a sequence throwsArgumentNullException, as it does fornull, instead ofInvalidOperationException.Stack.push,peekandpopare removed: usecons,headanduncons.popreturned a reference tuple, andunconsreturns a struct tuple.Cons | Nil, which matched a reference tuple, is replaced by the struct partial active patternsCons, which matches a struct tuple, andNil.headandtailof an empty stack throwArgumentExceptioninstead ofInvalidOperationException.Checklist
Put an
xin the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code.Further comments
Orderalready wraps the exception of a comparison inInvalidOperationException, asArray.Sortdoes. With the old LINQ sorts, only thesortByDescendingtest of the stable-sort contract fails, inStackand inIStack, becauseOrderByDescendingcomputes the keys before it sorts. The four sorts moved toSeqanyway, the two descending ones throughDescendingSorts: they now make exactly the comparisons thatListmakes.Seqsorts: measured in Release on .NET 10 with a stack of 10 000 integers, as the median of 75 calls in alternating rounds with the old build, in two runs.Seq.sortDescendingandSeq.sortByDescendingallocated 7.5 MB per call. WithDescendingSorts, both allocate 0.65 MB, assortWithdoes.sortDescendingtakes 1.0 ms instead of 2.1 to 2.5 ms, andsortByDescending1.1 to 2.2 ms instead of 2.2 to 4.6 ms. The two runs differ most forsortByDescending.sortandsortWithstay onSeqand allocate 0.53 and 0.65 MB. Against LINQOrder,Stack.sortWithtakes 0.83 ms instead of 1.16 ms and allocates 651 704 bytes instead of 440 384.Enumerable.ToArrayallocates less thanSeq.toArray, which allocates 783 072 bytes.lengthis O(n): an immutable stack does not store its length. The argument checks count only as far as they need, sohead,exactlyOneand the sorts do not count the whole stack.List:Listreports some unusual names, and the module reports them too:"source"forcontains,exactlyOne,average,averageByand therandom*functions;"count"forinit;"stack.[j]"fortranspose, whereListreports"list.[j]".🤖 Generated with Claude Code