Skip to content

feat: add EquatableArray<'T> (step 1 of ADR 0002) - #28

Open
xperiandri wants to merge 4 commits into
mainfrom
feat/equatable-array-type
Open

xperiandri wants to merge 4 commits into
mainfrom
feat/equatable-array-type

Conversation

@xperiandri

Copy link
Copy Markdown
Collaborator

Proposed Changes

Implements step 1 of ADR 0002 (#24): EquatableArray<'T>, a struct over one ImmutableArray<'T> with value semantics, for fields of records and unions, dictionary and set keys, and persisted documents.

  • Equality and the hash code use all elements, compared with EqualityComparer<'T>.Default, for every caller: F# equality, Equals, EqualityComparer.Default, dictionary and set keys, ValueTuple, and C# == and !=.
  • Comparison puts shorter arrays first, then orders by the first different element in the order of F# generic comparison. Strings compare ordinally in every culture.
  • The default value is the empty array. The default state of ImmutableArray is never observable.
  • Conversions to and from ImmutableArray<'T> (constructor, Items, op_Implicit) do not copy. GetSlice has the rules of F# array slices, Slice serves C# ranges, IList<'T> is read-only, CollectionBuilderAttribute serves C# collection expressions, and a System.Text.Json converter factory needs no registration.
  • The module EquatableArray (compiled EquatableArrayModule): empty, ofArray, ofImmutableArray, ofList, ofSeq, singleton, unsafeOfArray, isEmpty, length, map, toImmutableArray.
  • The struct partial active patterns Arr0, Arr1, Arr2, Arr3 and ArrN.

The maintainers accepted the recommendations of ADR 0002 §11, and #24 records them. Step 2 (the mirror of the rewritten FlatList module, the builder functions and equatableArray { }) follows in the chain of ADR 0001 (#23).

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)

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

Tests. 97 F# tests in tests/FSharp.Collections.Immutable.Tests/EquatableArray/ cover one behaviour per class, with the category attributes in the new TestCategories.fs. 19 C# tests in the new tests/FSharp.Collections.Immutable.CSharp.Tests project cover what only the C# compiler binds: collection expressions, indices and ranges, == and !=, record structs and the compiled names of the module. Release builds also check that =, hash, CompareTo, for and a pattern match allocate nothing. Tiered compilation is off in the F# test project for Release, so the counts are exact, and a control test proves that the counter sees an allocation. The full build chain as CI runs it passed locally (CI=true, Release, coverage on), and so did the strict documentation build.

Differences from the wrapper of the first consumer (FSharp.Azure.Cosmos), which can replace it by opening this namespace:

  • ofArray null and unsafeOfArray null throw ArgumentNullException, as FSharp.Core does. The wrapper returned an empty array.
  • The type supports comparison when its element type does. The wrapper was NoComparison, so records and unions that hold the array gain comparison.
  • The patterns take EquatableArray<'T>. The patterns of the wrapper took ImmutableArray<'T>.
  • ToString () and %A of an array wider than one line, or longer than 100 elements, wrap and end in ..., as %A of an F# array does. Short arrays print the same [|1; 2|].

Implementation notes. The code is ported from the prototype that verified the ADR, with Slice, the null checks, unsafeOfArray and the XML documentation added. The signature files carry the documentation. ofSeq shares the array of a boxed ImmutableArray (it cannot change) and throws InvalidOperationException for a default one, as ImmutableArray.CreateRange does.

🤖 Generated with Claude Code

Copilot AI balanced review requested due to automatic review settings October 18, 2026 11:13
xperiandri added a commit that referenced this pull request Oct 18, 2026
The maintainers accepted every recommendation of section 11 on
2026-10-18, and pull request #28 implements step 1.

- Status Accepted; section 11 records the decisions.
- Section 1: the library compiles against the signature, `Slice`
  included.
- Sections 5 and 6: step 1 also has `unsafeOfArray`, which throws for
  `null` like `ofArray`.
- Section 7: the categories as implemented, the exact allocation
  counts, and the C# test project that the maintainers chose.
- Section 9: the four differences the first consumer notices when it
  moves to the library type.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@xperiandri xperiandri mentioned this pull request Oct 18, 2026
1 of 6 tasks

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

Recursive formatting can overflow the stack, and extreme reversed slice bounds can throw instead of returning an empty array.

2 open issues
What changed in this PR

Implements step 1 of ADR 0002: an immutable array wrapper with value semantics for F# and C# consumers.

Changes:

  • Adds EquatableArray<'T>, module functions, active patterns, JSON conversion, and collection interoperability.
  • Adds F# and C# tests for behavior and allocation guarantees.
  • Updates project integration and documentation.
File Description
tests/​FSharp.Collections.Immutable.Tests/​TestCategories.fs Centralizes F# test categories.
tests/​FSharp.Collections.Immutable.Tests/​FSharp.Collections.Immutable.Tests.fsproj Includes tests and allocation settings.
tests/​FSharp.Collections.Immutable.Tests/​EquatableArray/​EquatableArrayTestTypes.fs Adds shared test models.
tests/​FSharp.Collections.Immutable.Tests/​EquatableArray/​EquatableArrayPatternTests.fs Tests active patterns.
tests/​FSharp.Collections.Immutable.Tests/​EquatableArray/​EquatableArrayModuleTests.fs Tests module functions.
tests/​FSharp.Collections.Immutable.Tests/​EquatableArray/​EquatableArrayKeyTests.fs Tests collection-key semantics.
tests/​FSharp.Collections.Immutable.Tests/​EquatableArray/​EquatableArrayJsonTests.fs Tests JSON integration.
tests/​FSharp.Collections.Immutable.Tests/​EquatableArray/​EquatableArrayInteropTests.fs Tests conversions, slices, and interfaces.
tests/​FSharp.Collections.Immutable.Tests/​EquatableArray/​EquatableArrayFormattingTests.fs Tests array formatting.
tests/​FSharp.Collections.Immutable.Tests/​EquatableArray/​EquatableArrayEqualityTests.fs Tests equality and hashing.
tests/​FSharp.Collections.Immutable.Tests/​EquatableArray/​EquatableArrayDefaultValueTests.fs Tests empty-default behavior.
tests/​FSharp.Collections.Immutable.Tests/​EquatableArray/​EquatableArrayComparisonTests.fs Tests ordering semantics.
tests/​FSharp.Collections.Immutable.Tests/​EquatableArray/​EquatableArrayAllocationTests.fs Checks Release allocations.
tests/​FSharp.Collections.Immutable.CSharp.Tests/​TestCategories.cs Centralizes C# test categories.
tests/​FSharp.Collections.Immutable.CSharp.Tests/​MSTestSettings.cs Configures parallel execution.
tests/​FSharp.Collections.Immutable.CSharp.Tests/​JsonTests.cs Tests record-struct serialization.
tests/​FSharp.Collections.Immutable.CSharp.Tests/​IndexAndRangeTests.cs Tests C# indices and ranges.
tests/​FSharp.Collections.Immutable.CSharp.Tests/​FSharp.Collections.Immutable.CSharp.Tests.csproj Adds the C# test project.
tests/​FSharp.Collections.Immutable.CSharp.Tests/​EqualityTests.cs Tests C# equality consumers.
tests/​FSharp.Collections.Immutable.CSharp.Tests/​ConversionTests.cs Tests conversions and compiled names.
tests/​FSharp.Collections.Immutable.CSharp.Tests/​ComparisonTests.cs Tests C# ordering consumers.
tests/​FSharp.Collections.Immutable.CSharp.Tests/​CollectionExpressionTests.cs Tests collection expressions.
src/​FSharp.Collections.Immutable/​FSharp.Collections.Immutable.fsproj Includes implementations and signatures.
src/​FSharp.Collections.Immutable/​EquatableArray/​EquatableArrayPatterns.fsi Documents active-pattern signatures.
src/​FSharp.Collections.Immutable/​EquatableArray/​EquatableArrayPatterns.fs Implements active patterns.
src/​FSharp.Collections.Immutable/​EquatableArray/​EquatableArray.fsi Defines the documented public API.
src/​FSharp.Collections.Immutable/​EquatableArray/​EquatableArray.fs Implements the wrapper and supporting APIs.
README.md Lists the new collection.
FSharp.Collections.Immutable.slnx Includes C# tests.
FSharp.Collections.Immutable.slnf Includes C# tests in the filter.
docsSrc/​index.md Lists the new feature.
docsSrc/​Explanations/​Background.md Updates available collections.
Directory.Packages.props Adds the JSON test dependency.
CHANGELOG.md Records the new API.
.github/​copilot-instructions.md Updates project structure and library references.

Review effort: Balanced

Comment thread src/FSharp.Collections.Immutable/EquatableArray/EquatableArray.fs Outdated
Comment thread src/FSharp.Collections.Immutable/EquatableArray/EquatableArray.fs Outdated
xperiandri added a commit that referenced this pull request Oct 18, 2026
The maintainers accepted every recommendation of section 11 on
2026-10-18, and pull request #28 implements step 1.

- Status Accepted; section 11 records the decisions.
- Section 1: the library compiles against the signature, `Slice`
  included.
- Sections 5 and 6: step 1 also has `unsafeOfArray`, which throws for
  `null` like `ofArray`.
- Section 7: the categories as implemented, the exact allocation
  counts, and the C# test project that the maintainers chose.
- Section 9: the four differences the first consumer notices when it
  moves to the library type.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@xperiandri
xperiandri force-pushed the feat/equatable-array-type branch from e657911 to 33b1a65 Compare October 18, 2026 13:03
xperiandri added a commit that referenced this pull request Oct 18, 2026
Section 2 states the depth guard of the display text: each nested
display starts a new %A formatter, so a guard shared by them shows
"..." at a nesting of 100 (the Copilot review of pull request #28).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xperiandri and others added 4 commits October 20, 2026 18:13
A struct over one `ImmutableArray<'T>` field, as ADR 0002 designs it
(step 1 of its section 6):

- Equality and the hash code read all elements and compare them with
  `EqualityComparer<'T>.Default`, for every caller, C# `==` and `!=`
  included.
- Comparison puts shorter arrays first, then orders by the first
  different element in the order of F# generic comparison; strings
  compare ordinally.
- The default value is the empty array; `Items` never exposes a
  default `ImmutableArray`.
- Conversions to and from `ImmutableArray<'T>` do not copy. `GetSlice`
  has the rules of F# array slices, `Slice` serves C# ranges, `IList<'T>`
  is read-only, a collection builder serves C# collection expressions,
  and a System.Text.Json converter factory needs no registration.
- The module `EquatableArray` (compiled `EquatableArrayModule`):
  `empty`, `ofArray`, `ofImmutableArray`, `ofList`, `ofSeq`,
  `singleton`, `unsafeOfArray`, `isEmpty`, `length`, `map`,
  `toImmutableArray`.
- The struct partial active patterns `Arr0` to `Arr3` and `ArrN`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One test class per behaviour: the module functions, the default value,
equality under every caller, comparison, the key split of array
elements (decision 5 of ADR 0002), formatting, the interop members,
JSON under System.Text.Json and FSharp.SystemTextJson, the active
patterns, and allocation-free equality, hashing, comparison, loops and
matches in Release builds, where tiered compilation is off so that the
counts are exact. `TestCategories.fs` holds the category attributes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A C# MSTest project covers what only the C# compiler binds: collection
expressions through the collection builder, indices and ranges through
`Slice`, the `==` and `!=` operators, the default collections, record
structs, the implicit conversions and the module through its compiled
names. The build and CI pick it up through the solution filter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds an Unreleased section to the changelog, the type to the collection
lists, and its folders and libraries to the agent instructions.

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