From 3bd256d6852d846167b5fa10b26d9e07bf1ec434 Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Mon, 19 Oct 2026 01:01:41 +0200 Subject: [PATCH 01/10] refactor: split Helper.fs into root modules Each module of Helper.fs moves to its own file in the Helper folder, named after the module and declared as a root module with its full name, as the other files of the library are: ImmutableCollectionUtil, Comparers, ErrorStrings, Raise, ArrayChecks and Arrays. A file keeps the doc comment, the attributes and the accessibility of its module (AutoOpen on ImmutableCollectionUtil and Comparers, internal on all of them) and opens only the namespaces that it uses. The change is a move: the bodies lose four spaces of indentation and nothing else. The full names of the modules and of their members stay the same, so no other file changes. The compiled library differs from the previous one only in the empty startup classes that the compiler emits per file: one for Helper.fs before, one per module now. The conditional that declared the namespace global for F# Interactive is dropped: a root module has no namespace declaration, and nothing loads Helper.fs into F# Interactive (no script or build file mentions it). Co-Authored-By: Claude Sonnet 5.5 --- .github/copilot-instructions.md | 8 +- .../FSharp.Collections.Immutable.fsproj | 7 +- src/FSharp.Collections.Immutable/Helper.fs | 454 ------------------ .../Helper/ArrayChecks.fs | 22 + .../Helper/Arrays.fs | 156 ++++++ .../Helper/Comparers.fs | 29 ++ .../Helper/ErrorStrings.fs | 59 +++ .../Helper/ImmutableCollectionUtil.fs | 106 ++++ .../Helper/Raise.fs | 78 +++ 9 files changed, 463 insertions(+), 456 deletions(-) delete mode 100644 src/FSharp.Collections.Immutable/Helper.fs create mode 100644 src/FSharp.Collections.Immutable/Helper/ArrayChecks.fs create mode 100644 src/FSharp.Collections.Immutable/Helper/Arrays.fs create mode 100644 src/FSharp.Collections.Immutable/Helper/Comparers.fs create mode 100644 src/FSharp.Collections.Immutable/Helper/ErrorStrings.fs create mode 100644 src/FSharp.Collections.Immutable/Helper/ImmutableCollectionUtil.fs create mode 100644 src/FSharp.Collections.Immutable/Helper/Raise.fs diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 840846a3..316a54bf 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -23,7 +23,13 @@ module's `.fsi` and the `.fs` on its `.fs`, so that Visual Studio shows them as │ └── CheckOrder.fs – checks the category order of the signature files with the Fantomas parser ├── docsSrc/ – FSharp.Formatting documentation source ├── src/FSharp.Collections.Immutable/ – main library -│ ├── Helper.fs – argument checks and exceptions shared by the modules +│ ├── Helper/ – internal helpers shared by the modules, one root module per file +│ │ ├── ImmutableCollectionUtil.fs – argument checks and the wrapping of arrays +│ │ ├── Comparers.fs – the default comparer of F# generic comparison +│ │ ├── ErrorStrings.fs – the exception messages of FSharp.Core and the .NET libraries +│ │ ├── Raise.fs – the exceptions of the FSharp.Core DetailedExceptions module +│ │ ├── ArrayChecks.fs – the checks that the functions of several arrays share +│ │ └── Arrays.fs – the algorithms over arrays that the default and Tuple modules share │ ├── FlatList/ – ImmutableArray API surface and builders │ │ ├── FlatListTypes.fs – the FlatList abbreviation │ │ ├── FlatList.fs / FlatList.fsi – includes the nested Parallel module diff --git a/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj b/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj index 8e3efc2d..321383c7 100644 --- a/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj +++ b/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj @@ -20,7 +20,12 @@ - + + + + + + diff --git a/src/FSharp.Collections.Immutable/Helper.fs b/src/FSharp.Collections.Immutable/Helper.fs deleted file mode 100644 index efb31d80..00000000 --- a/src/FSharp.Collections.Immutable/Helper.fs +++ /dev/null @@ -1,454 +0,0 @@ -#if INTERACTIVE -namespace global -#else -namespace FSharp.Collections.Immutable -#endif - -open System -open System.Collections.Generic -open System.Collections.Immutable -open System.Linq -open System.Runtime.InteropServices - -/// Argument checks shared by the collection modules. -/// The module keeps its old name, so that the modules written before ADR 0001 compile unchanged until they are replaced. -[] -module internal ImmutableCollectionUtil = - - /// Throws `ArgumentNullException` for a `null` argument, as FSharp.Core does for a `null` array or list. - let inline checkNotNull name (arg : _ | null) = - match arg with - | null -> nullArg name - | _ -> () - - /// - /// Throws for a default . - /// - /// - /// A default immutable array has no underlying array. The collection modules treat it as FSharp.Core treats a - /// array (ADR 0001, D6). - /// - let inline checkNotDefault name (array : ImmutableArray<'T>) = - if array.IsDefault then - nullArg name - - /// - /// Tells whether an object is a default , of any element type. - /// - /// - /// - /// is covariant, so a sequence of a reference type is also a - /// sequence of its base types: a default ImmutableArray<string> can be a seq<obj>. The type - /// test :? ImmutableArray<obj> does not recognize it, because the type is another instantiation. - /// - /// - /// The function finds the instantiation from the type of the object and reads the property IsDefault through - /// reflection. It allocates nothing for an object of another type. It is inline, because the public inline members of - /// the computation expression call the check, and an inline member cannot call an internal function. - /// - /// - let inline isDefaultImmutableArray (source : obj) : bool = - let sourceType = source.GetType () - - sourceType.IsGenericType - && Type.(=) (sourceType.GetGenericTypeDefinition (), typedefof>) - && unbox((nonNull (sourceType.GetProperty "IsDefault")).GetValue source) - - /// - /// Throws for a sequence and for a default - /// that is given as a sequence. - /// - /// - /// - /// A default immutable array converts to a sequence without a check, and the sequence fails with - /// when a function enumerates it. A function that takes a sequence - /// calls this check, which treats a default immutable array as FSharp.Core treats a sequence - /// (ADR 0001, D6). Any other sequence passes. - /// - /// - /// An immutable array of the element type is recognized by its type, without reflection. A default immutable array of - /// a derived reference type, which arrives as a sequence of a base type by covariance, is recognized by its generic - /// type and by reading IsDefault through reflection. - /// - /// - let inline checkNotNullOrDefaultSeq name (source : seq<'T>) = - checkNotNull name source - - match source with - | :? ImmutableArray<'T> as array -> checkNotDefault name array - | _ -> - if isDefaultImmutableArray source then - nullArg name - - /// - /// The array under an immutable array, without a copy (ADR 0001, D5). - /// - /// - /// The caller reads the array and never writes to it: the immutable array and every value that shares it see the same - /// memory. A default immutable array throws for . - /// - let inline unwrapArray name (array : ImmutableArray<'T>) : 'T array = - checkNotDefault name array - // The check above guarantees an underlying array - ImmutableCollectionsMarshal.AsArray array - |> Unchecked.nonNull - - /// - /// An immutable array over an array, without a copy (ADR 0001, D5). - /// - /// - /// Only an array that nobody can write to any more may be wrapped: a fresh result of a computation. - /// - let inline wrapArray (array : 'T array) : ImmutableArray<'T> = ImmutableCollectionsMarshal.AsImmutableArray array - - /// The constructor of a struct pair, for the algorithms shared by the default and the `Tuple` modules. - let inline structPair (first : 'T1) (second : 'T2) = struct (first, second) - - /// The constructor of a reference pair, for the algorithms shared by the default and the `Tuple` modules. - let inline referencePair (first : 'T1) (second : 'T2) = (first, second) - - /// The constructor of a struct triple, for the algorithms shared by the default and the `Tuple` modules. - let inline structTriple (first : 'T1) (second : 'T2) (third : 'T3) = struct (first, second, third) - - /// The constructor of a reference triple, for the algorithms shared by the default and the `Tuple` modules. - let inline referenceTriple (first : 'T1) (second : 'T2) (third : 'T3) = (first, second, third) - -/// The default comparers of the collections (ADR 0001, D5). -[] -module internal Comparers = - - /// - /// The comparer of F# generic comparison for a type: the comparer of the sorts, of binarySearch and of the - /// sorted collections that are created without a comparer. - /// - /// - /// - /// The class resolves once per type, in - /// generic code, where FSharp.Core returns the comparer that it keeps for the type. - /// - /// - /// A call of FastGenericComparer at a concrete type that is not primitive creates a new comparer each time, and - /// the F# optimizer creates such calls when it inlines a small function of the library into its caller. Collections of - /// one type would then have different comparers, and the BCL could not take its shortcuts for two collections with the - /// same comparer, such as the one of ImmutableSortedSet.Union. A static field cannot be inlined. - /// - /// - [] - type DefaultComparer<'T when 'T : comparison> private () = - - static let comparer = LanguagePrimitives.FastGenericComparer<'T> - - /// The comparer of F# generic comparison for the type. - static member Instance : IComparer<'T> = comparer - -/// -/// The exception messages of FSharp.Core (FSCore.resx) and of the .NET libraries, in English. -/// -/// -/// The modules throw what the FSharp.Core function that they mirror throws (ADR 0001, D6). FSharp.Core keeps its resources -/// internal, so the texts are copied. The texts of the .NET libraries are for the functions that report a BCL exception -/// under the name of their own parameter. -/// -module internal ErrorStrings = - - [] - let ArraysHadDifferentLengths = "The arrays have different lengths." - - [] - let IndexOutOfBounds = "The index was outside the range of elements in the list." - - [] - let InputArrayEmpty = "The input array was empty." - - [] - let InputListWasEmpty = "The input list was empty." - - [] - let InputMustBeNonNegative = "The input must be non-negative." - - [] - let InputMustBePositive = "The input must be positive." - - [] - let InputSequenceEmpty = "The input sequence was empty." - - [] - let InputSequenceTooLong = "The input sequence contains more than one element." - - [] - let KeyNotFound = "The item, key, or index was not found in the collection." - - [] - let KeyNotFoundAlt = "An index satisfying the predicate was not found in the collection." - - [] - let ListsHadDifferentLengths = "The lists had different lengths." - - /// The message of the modules written before ADR 0001. - [] - let ListsHaveDifferentLengths = "The lists have different lengths." - - /// The message of the .NET libraries for a negative argument, as `Array.BinarySearch` reports it. - [] - let NeedNonNegativeNumber = "Non-negative number required." - - [] - let NotAPermutation = "The function did not compute a permutation." - - [] - let NotEnoughElements = "The input sequence has an insufficient number of elements." - - [] - let OutOfRange = "The index is outside the legal range." - -/// -/// The exceptions of the DetailedExceptions module of FSharp.Core, with the same types, messages and parameter names. -/// -/// -/// The callers pass the names of their own parameters (ADR 0001, D6). -/// -module internal Raise = - - let inline private invalidArgFormat (arg : string) (format : string) (paramArray : obj array) = - raise (ArgumentException (String.Format (format, paramArray), arg)) - - /// The arrays of a function of two arrays have different lengths. - let invalidArgDifferentArrayLength (arg1 : string) (len1 : int) (arg2 : string) (len2 : int) : 'T = - invalidArgFormat arg1 "{0}\n{1}.Length = {2}, {3}.Length = {4}" [| - ErrorStrings.ArraysHadDifferentLengths - arg1 - len1 - arg2 - len2 - |] - - /// The lists of a function of two lists have different lengths; `diff` is how much shorter `arg1` is. - let invalidArgDifferentListLength (arg1 : string) (arg2 : string) (diff : int) : 'T = - invalidArgFormat arg1 "{0}\n{1} is {2} {3} shorter than {4}" [| - ErrorStrings.ListsHadDifferentLengths - arg1 - diff - (if diff = 1 then "element" else "elements") - arg2 - |] - - /// The arrays of a function of three arrays have different lengths. - let invalidArg3ArraysDifferent (arg1 : string) (arg2 : string) (arg3 : string) (len1 : int) (len2 : int) (len3 : int) : 'T = - invalidArgFormat - (String.Concat [| arg1; ", "; arg2; ", "; arg3 |]) - "{0}\n {1}.Length = {2}, {3}.Length = {4}, {5}.Length = {6}" - [| ErrorStrings.ArraysHadDifferentLengths; arg1; len1; arg2; len2; arg3; len3 |] - - /// The lists of a function of three lists have different lengths. - let invalidArg3ListsDifferent (arg1 : string) (arg2 : string) (arg3 : string) (len1 : int) (len2 : int) (len3 : int) : 'T = - invalidArgFormat - (String.Concat [| arg1; ", "; arg2; ", "; arg3 |]) - "{0}\n {1}.Length = {2}, {3}.Length = {4}, {5}.Length = {6}" - [| ErrorStrings.ListsHadDifferentLengths; arg1; len1; arg2; len2; arg3; len3 |] - - /// A count or an index is negative. - let invalidArgInputMustBeNonNegative (arg : string) (count : int) : 'T = - invalidArgFormat arg "{0}\n{1} = {2}" [| ErrorStrings.InputMustBeNonNegative; arg; count |] - - /// A size is not positive. - let invalidArgInputMustBePositive (arg : string) (count : int) : 'T = - invalidArgFormat arg "{0}\n{1} = {2}" [| ErrorStrings.InputMustBePositive; arg; count |] - - /// An index or a count is beyond a bound. - let invalidArgOutOfRange (arg : string) (index : int) (text : string) (bound : int) : 'T = - let message = - String.Format ("{0}\n{1} = {2}, {3} = {4}", [| box ErrorStrings.OutOfRange; arg; index; text; bound |]) - - raise (ArgumentOutOfRangeException (arg, message)) - - /// A list is shorter than an index. - let invalidOpListNotEnoughElements (index : int) : 'T = - let message = - String.Format ( - "{0}\nThe list was {1} {2} shorter than the index", - [| box ErrorStrings.NotEnoughElements; index; (if index = 1 then "element" else "elements") |] - ) - - raise (InvalidOperationException message) - - /// No element satisfies the predicate of a `find` or `pick` function. - let indexNotFound () : 'T = raise (KeyNotFoundException ErrorStrings.KeyNotFoundAlt) - - /// A collection has fewer elements than a function needs. - let notEnoughElements () : 'T = raise (InvalidOperationException ErrorStrings.NotEnoughElements) - -/// The checks of the FSharp.Core Array module that the functions of several arrays share. -module internal ArrayChecks = - - /// The arrays of a function of two arrays must have the same length. - let inline sameLengths (name1 : string) (array1 : 'T1 array) (name2 : string) (array2 : 'T2 array) = - if array1.Length <> array2.Length then - Raise.invalidArgDifferentArrayLength name1 array1.Length name2 array2.Length - - /// The arrays of a function of three arrays must have the same length. - let inline sameLengths3 - (name1 : string) - (array1 : 'T1 array) - (name2 : string) - (array2 : 'T2 array) - (name3 : string) - (array3 : 'T3 array) - = - if - array1.Length <> array2.Length - || array1.Length <> array3.Length - then - Raise.invalidArg3ArraysDifferent name1 name2 name3 array1.Length array2.Length array3.Length - -/// -/// Algorithms over arrays that the default modules and the Tuple modules share (ADR 0001, D3). -/// -/// -/// Each algorithm takes the constructor of the tuples that it produces, so that neither variant pays for a second pass. -/// The callers check their arguments first, with the exceptions of the FSharp.Core module that they mirror. -/// -module internal Arrays = - - /// The pairs of every element of `array1` with every element of `array2`. - let inline allPairs ([] tuple : 'T1 -> 'T2 -> 'R) (array1 : 'T1 array) (array2 : 'T2 array) : 'R array = - let length2 = array2.Length - let result = Array.zeroCreate (array1.Length * length2) - - for i = 0 to array1.Length - 1 do - for j = 0 to length2 - 1 do - result[i * length2 + j] <- tuple array1[i] array2[j] - - result - - /// A key that may be null, for a dictionary, which rejects null keys. - [] - type NullableKey<'Key> = { Key : 'Key } - - /// Compares wrapped keys in F# structural equality, which handles null keys, as FSharp.Core does for its keys. - let nullableKeyComparer<'Key when 'Key : equality> () = - let structural = HashIdentity.Structural<'Key> - - { - new IEqualityComparer> with - member _.Equals (left, right) = structural.Equals (left.Key, right.Key) - member _.GetHashCode key = structural.GetHashCode key.Key - } - - /// - /// The keys of the projection in the order of their first occurrence, each with the number of its elements. - /// - /// - /// The counting keeps one counter per key, not the elements, so it needs memory for the keys only. - /// - let inline countBy - ([] tuple : 'Key -> int -> 'R) - (projection : 'T -> 'Key) - (array : 'T array) - : 'R array when 'Key : equality = - let indices = Dictionary, int>(nullableKeyComparer<'Key>()) - let keys = ResizeArray<'Key>() - let counts = ResizeArray() - - for item in array do - let key = projection item - let mutable index = 0 - - if indices.TryGetValue ({ Key = key }, &index) then - counts[index] <- counts[index] + 1 - else - indices.Add ({ Key = key }, keys.Count) - keys.Add key - counts.Add 1 - - let result = Array.zeroCreate keys.Count - - for index = 0 to result.Length - 1 do - result[index] <- tuple keys[index] counts[index] - - result - - /// The keys of the projection in the order of their first occurrence, each with its elements in their order. - let inline groupBy - ([] tuple : 'Key -> 'T array -> 'R) - (projection : 'T -> 'Key) - (array : 'T array) - : 'R array when 'Key : equality = - let lookup = array.ToLookup ((fun item -> projection item), HashIdentity.Structural<'Key>) - let result = Array.zeroCreate lookup.Count - let mutable index = 0 - - for grouping in lookup do - result[index] <- tuple grouping.Key (grouping.ToArray ()) - index <- index + 1 - - result - - /// Every element with its index. - let inline indexed ([] tuple : int -> 'T -> 'R) (array : 'T array) : 'R array = - let result = Array.zeroCreate array.Length - - for i = 0 to result.Length - 1 do - result[i] <- tuple i array[i] - - result - - /// Every element with its successor. - let inline pairwise ([] tuple : 'T -> 'T -> 'R) (array : 'T array) : 'R array = - if array.Length < 2 then - Array.Empty () - else - let result = Array.zeroCreate (array.Length - 1) - - for i = 0 to result.Length - 1 do - result[i] <- tuple array[i] array[i + 1] - - result - - /// Splits the pairs of an array with `split` into two arrays. - let inline unzip ([] split : 'P -> struct ('T1 * 'T2)) (array : 'P array) = - let result1 = Array.zeroCreate array.Length - let result2 = Array.zeroCreate array.Length - - for i = 0 to array.Length - 1 do - let struct (first, second) = split array[i] - result1[i] <- first - result2[i] <- second - - struct (result1, result2) - - /// Splits the triples of an array with `split` into three arrays. - let inline unzip3 ([] split : 'P -> struct ('T1 * 'T2 * 'T3)) (array : 'P array) = - let result1 = Array.zeroCreate array.Length - let result2 = Array.zeroCreate array.Length - let result3 = Array.zeroCreate array.Length - - for i = 0 to array.Length - 1 do - let struct (first, second, third) = split array[i] - result1[i] <- first - result2[i] <- second - result3[i] <- third - - struct (result1, result2, result3) - - /// Pairs the elements of two arrays of the same length. - let inline zip ([] tuple : 'T1 -> 'T2 -> 'R) (array1 : 'T1 array) (array2 : 'T2 array) : 'R array = - let result = Array.zeroCreate array1.Length - - for i = 0 to result.Length - 1 do - result[i] <- tuple array1[i] array2[i] - - result - - /// Combines the elements of three arrays of the same length. - let inline zip3 - ([] tuple : 'T1 -> 'T2 -> 'T3 -> 'R) - (array1 : 'T1 array) - (array2 : 'T2 array) - (array3 : 'T3 array) - : 'R array = - let result = Array.zeroCreate array1.Length - - for i = 0 to result.Length - 1 do - result[i] <- tuple array1[i] array2[i] array3[i] - - result diff --git a/src/FSharp.Collections.Immutable/Helper/ArrayChecks.fs b/src/FSharp.Collections.Immutable/Helper/ArrayChecks.fs new file mode 100644 index 00000000..4b97f77c --- /dev/null +++ b/src/FSharp.Collections.Immutable/Helper/ArrayChecks.fs @@ -0,0 +1,22 @@ +/// The checks of the FSharp.Core Array module that the functions of several arrays share. +module internal FSharp.Collections.Immutable.ArrayChecks + +/// The arrays of a function of two arrays must have the same length. +let inline sameLengths (name1 : string) (array1 : 'T1 array) (name2 : string) (array2 : 'T2 array) = + if array1.Length <> array2.Length then + Raise.invalidArgDifferentArrayLength name1 array1.Length name2 array2.Length + +/// The arrays of a function of three arrays must have the same length. +let inline sameLengths3 + (name1 : string) + (array1 : 'T1 array) + (name2 : string) + (array2 : 'T2 array) + (name3 : string) + (array3 : 'T3 array) + = + if + array1.Length <> array2.Length + || array1.Length <> array3.Length + then + Raise.invalidArg3ArraysDifferent name1 name2 name3 array1.Length array2.Length array3.Length diff --git a/src/FSharp.Collections.Immutable/Helper/Arrays.fs b/src/FSharp.Collections.Immutable/Helper/Arrays.fs new file mode 100644 index 00000000..3dedbd3a --- /dev/null +++ b/src/FSharp.Collections.Immutable/Helper/Arrays.fs @@ -0,0 +1,156 @@ +/// +/// Algorithms over arrays that the default modules and the Tuple modules share (ADR 0001, D3). +/// +/// +/// Each algorithm takes the constructor of the tuples that it produces, so that neither variant pays for a second pass. +/// The callers check their arguments first, with the exceptions of the FSharp.Core module that they mirror. +/// +module internal FSharp.Collections.Immutable.Arrays + +open System +open System.Collections.Generic +open System.Linq + +/// The pairs of every element of `array1` with every element of `array2`. +let inline allPairs ([] tuple : 'T1 -> 'T2 -> 'R) (array1 : 'T1 array) (array2 : 'T2 array) : 'R array = + let length2 = array2.Length + let result = Array.zeroCreate (array1.Length * length2) + + for i = 0 to array1.Length - 1 do + for j = 0 to length2 - 1 do + result[i * length2 + j] <- tuple array1[i] array2[j] + + result + +/// A key that may be null, for a dictionary, which rejects null keys. +[] +type NullableKey<'Key> = { Key : 'Key } + +/// Compares wrapped keys in F# structural equality, which handles null keys, as FSharp.Core does for its keys. +let nullableKeyComparer<'Key when 'Key : equality> () = + let structural = HashIdentity.Structural<'Key> + + { + new IEqualityComparer> with + member _.Equals (left, right) = structural.Equals (left.Key, right.Key) + member _.GetHashCode key = structural.GetHashCode key.Key + } + +/// +/// The keys of the projection in the order of their first occurrence, each with the number of its elements. +/// +/// +/// The counting keeps one counter per key, not the elements, so it needs memory for the keys only. +/// +let inline countBy + ([] tuple : 'Key -> int -> 'R) + (projection : 'T -> 'Key) + (array : 'T array) + : 'R array when 'Key : equality = + let indices = Dictionary, int>(nullableKeyComparer<'Key>()) + let keys = ResizeArray<'Key>() + let counts = ResizeArray() + + for item in array do + let key = projection item + let mutable index = 0 + + if indices.TryGetValue ({ Key = key }, &index) then + counts[index] <- counts[index] + 1 + else + indices.Add ({ Key = key }, keys.Count) + keys.Add key + counts.Add 1 + + let result = Array.zeroCreate keys.Count + + for index = 0 to result.Length - 1 do + result[index] <- tuple keys[index] counts[index] + + result + +/// The keys of the projection in the order of their first occurrence, each with its elements in their order. +let inline groupBy + ([] tuple : 'Key -> 'T array -> 'R) + (projection : 'T -> 'Key) + (array : 'T array) + : 'R array when 'Key : equality = + let lookup = array.ToLookup ((fun item -> projection item), HashIdentity.Structural<'Key>) + let result = Array.zeroCreate lookup.Count + let mutable index = 0 + + for grouping in lookup do + result[index] <- tuple grouping.Key (grouping.ToArray ()) + index <- index + 1 + + result + +/// Every element with its index. +let inline indexed ([] tuple : int -> 'T -> 'R) (array : 'T array) : 'R array = + let result = Array.zeroCreate array.Length + + for i = 0 to result.Length - 1 do + result[i] <- tuple i array[i] + + result + +/// Every element with its successor. +let inline pairwise ([] tuple : 'T -> 'T -> 'R) (array : 'T array) : 'R array = + if array.Length < 2 then + Array.Empty () + else + let result = Array.zeroCreate (array.Length - 1) + + for i = 0 to result.Length - 1 do + result[i] <- tuple array[i] array[i + 1] + + result + +/// Splits the pairs of an array with `split` into two arrays. +let inline unzip ([] split : 'P -> struct ('T1 * 'T2)) (array : 'P array) = + let result1 = Array.zeroCreate array.Length + let result2 = Array.zeroCreate array.Length + + for i = 0 to array.Length - 1 do + let struct (first, second) = split array[i] + result1[i] <- first + result2[i] <- second + + struct (result1, result2) + +/// Splits the triples of an array with `split` into three arrays. +let inline unzip3 ([] split : 'P -> struct ('T1 * 'T2 * 'T3)) (array : 'P array) = + let result1 = Array.zeroCreate array.Length + let result2 = Array.zeroCreate array.Length + let result3 = Array.zeroCreate array.Length + + for i = 0 to array.Length - 1 do + let struct (first, second, third) = split array[i] + result1[i] <- first + result2[i] <- second + result3[i] <- third + + struct (result1, result2, result3) + +/// Pairs the elements of two arrays of the same length. +let inline zip ([] tuple : 'T1 -> 'T2 -> 'R) (array1 : 'T1 array) (array2 : 'T2 array) : 'R array = + let result = Array.zeroCreate array1.Length + + for i = 0 to result.Length - 1 do + result[i] <- tuple array1[i] array2[i] + + result + +/// Combines the elements of three arrays of the same length. +let inline zip3 + ([] tuple : 'T1 -> 'T2 -> 'T3 -> 'R) + (array1 : 'T1 array) + (array2 : 'T2 array) + (array3 : 'T3 array) + : 'R array = + let result = Array.zeroCreate array1.Length + + for i = 0 to result.Length - 1 do + result[i] <- tuple array1[i] array2[i] array3[i] + + result diff --git a/src/FSharp.Collections.Immutable/Helper/Comparers.fs b/src/FSharp.Collections.Immutable/Helper/Comparers.fs new file mode 100644 index 00000000..68d08d17 --- /dev/null +++ b/src/FSharp.Collections.Immutable/Helper/Comparers.fs @@ -0,0 +1,29 @@ +/// The default comparers of the collections (ADR 0001, D5). +[] +module internal FSharp.Collections.Immutable.Comparers + +open System.Collections.Generic + +/// +/// The comparer of F# generic comparison for a type: the comparer of the sorts, of binarySearch and of the +/// sorted collections that are created without a comparer. +/// +/// +/// +/// The class resolves once per type, in +/// generic code, where FSharp.Core returns the comparer that it keeps for the type. +/// +/// +/// A call of FastGenericComparer at a concrete type that is not primitive creates a new comparer each time, and +/// the F# optimizer creates such calls when it inlines a small function of the library into its caller. Collections of +/// one type would then have different comparers, and the BCL could not take its shortcuts for two collections with the +/// same comparer, such as the one of ImmutableSortedSet.Union. A static field cannot be inlined. +/// +/// +[] +type DefaultComparer<'T when 'T : comparison> private () = + + static let comparer = LanguagePrimitives.FastGenericComparer<'T> + + /// The comparer of F# generic comparison for the type. + static member Instance : IComparer<'T> = comparer diff --git a/src/FSharp.Collections.Immutable/Helper/ErrorStrings.fs b/src/FSharp.Collections.Immutable/Helper/ErrorStrings.fs new file mode 100644 index 00000000..e4ae49ec --- /dev/null +++ b/src/FSharp.Collections.Immutable/Helper/ErrorStrings.fs @@ -0,0 +1,59 @@ +/// +/// The exception messages of FSharp.Core (FSCore.resx) and of the .NET libraries, in English. +/// +/// +/// The modules throw what the FSharp.Core function that they mirror throws (ADR 0001, D6). FSharp.Core keeps its resources +/// internal, so the texts are copied. The texts of the .NET libraries are for the functions that report a BCL exception +/// under the name of their own parameter. +/// +module internal FSharp.Collections.Immutable.ErrorStrings + +[] +let ArraysHadDifferentLengths = "The arrays have different lengths." + +[] +let IndexOutOfBounds = "The index was outside the range of elements in the list." + +[] +let InputArrayEmpty = "The input array was empty." + +[] +let InputListWasEmpty = "The input list was empty." + +[] +let InputMustBeNonNegative = "The input must be non-negative." + +[] +let InputMustBePositive = "The input must be positive." + +[] +let InputSequenceEmpty = "The input sequence was empty." + +[] +let InputSequenceTooLong = "The input sequence contains more than one element." + +[] +let KeyNotFound = "The item, key, or index was not found in the collection." + +[] +let KeyNotFoundAlt = "An index satisfying the predicate was not found in the collection." + +[] +let ListsHadDifferentLengths = "The lists had different lengths." + +/// The message of the modules written before ADR 0001. +[] +let ListsHaveDifferentLengths = "The lists have different lengths." + +/// The message of the .NET libraries for a negative argument, as `Array.BinarySearch` reports it. +[] +let NeedNonNegativeNumber = "Non-negative number required." + +[] +let NotAPermutation = "The function did not compute a permutation." + +[] +let NotEnoughElements = "The input sequence has an insufficient number of elements." + +[] +let OutOfRange = "The index is outside the legal range." diff --git a/src/FSharp.Collections.Immutable/Helper/ImmutableCollectionUtil.fs b/src/FSharp.Collections.Immutable/Helper/ImmutableCollectionUtil.fs new file mode 100644 index 00000000..b64dcc78 --- /dev/null +++ b/src/FSharp.Collections.Immutable/Helper/ImmutableCollectionUtil.fs @@ -0,0 +1,106 @@ +/// Argument checks shared by the collection modules. +/// The module keeps its old name, so that the modules written before ADR 0001 compile unchanged until they are replaced. +[] +module internal FSharp.Collections.Immutable.ImmutableCollectionUtil + +open System +open System.Collections.Immutable +open System.Runtime.InteropServices + +/// Throws `ArgumentNullException` for a `null` argument, as FSharp.Core does for a `null` array or list. +let inline checkNotNull name (arg : _ | null) = + match arg with + | null -> nullArg name + | _ -> () + +/// +/// Throws for a default . +/// +/// +/// A default immutable array has no underlying array. The collection modules treat it as FSharp.Core treats a +/// array (ADR 0001, D6). +/// +let inline checkNotDefault name (array : ImmutableArray<'T>) = + if array.IsDefault then + nullArg name + +/// +/// Tells whether an object is a default , of any element type. +/// +/// +/// +/// is covariant, so a sequence of a reference type is also a +/// sequence of its base types: a default ImmutableArray<string> can be a seq<obj>. The type +/// test :? ImmutableArray<obj> does not recognize it, because the type is another instantiation. +/// +/// +/// The function finds the instantiation from the type of the object and reads the property IsDefault through +/// reflection. It allocates nothing for an object of another type. It is inline, because the public inline members of +/// the computation expression call the check, and an inline member cannot call an internal function. +/// +/// +let inline isDefaultImmutableArray (source : obj) : bool = + let sourceType = source.GetType () + + sourceType.IsGenericType + && Type.(=) (sourceType.GetGenericTypeDefinition (), typedefof>) + && unbox((nonNull (sourceType.GetProperty "IsDefault")).GetValue source) + +/// +/// Throws for a sequence and for a default +/// that is given as a sequence. +/// +/// +/// +/// A default immutable array converts to a sequence without a check, and the sequence fails with +/// when a function enumerates it. A function that takes a sequence +/// calls this check, which treats a default immutable array as FSharp.Core treats a sequence +/// (ADR 0001, D6). Any other sequence passes. +/// +/// +/// An immutable array of the element type is recognized by its type, without reflection. A default immutable array of +/// a derived reference type, which arrives as a sequence of a base type by covariance, is recognized by its generic +/// type and by reading IsDefault through reflection. +/// +/// +let inline checkNotNullOrDefaultSeq name (source : seq<'T>) = + checkNotNull name source + + match source with + | :? ImmutableArray<'T> as array -> checkNotDefault name array + | _ -> + if isDefaultImmutableArray source then + nullArg name + +/// +/// The array under an immutable array, without a copy (ADR 0001, D5). +/// +/// +/// The caller reads the array and never writes to it: the immutable array and every value that shares it see the same +/// memory. A default immutable array throws for . +/// +let inline unwrapArray name (array : ImmutableArray<'T>) : 'T array = + checkNotDefault name array + // The check above guarantees an underlying array + ImmutableCollectionsMarshal.AsArray array + |> Unchecked.nonNull + +/// +/// An immutable array over an array, without a copy (ADR 0001, D5). +/// +/// +/// Only an array that nobody can write to any more may be wrapped: a fresh result of a computation. +/// +let inline wrapArray (array : 'T array) : ImmutableArray<'T> = ImmutableCollectionsMarshal.AsImmutableArray array + +/// The constructor of a struct pair, for the algorithms shared by the default and the `Tuple` modules. +let inline structPair (first : 'T1) (second : 'T2) = struct (first, second) + +/// The constructor of a reference pair, for the algorithms shared by the default and the `Tuple` modules. +let inline referencePair (first : 'T1) (second : 'T2) = (first, second) + +/// The constructor of a struct triple, for the algorithms shared by the default and the `Tuple` modules. +let inline structTriple (first : 'T1) (second : 'T2) (third : 'T3) = struct (first, second, third) + +/// The constructor of a reference triple, for the algorithms shared by the default and the `Tuple` modules. +let inline referenceTriple (first : 'T1) (second : 'T2) (third : 'T3) = (first, second, third) diff --git a/src/FSharp.Collections.Immutable/Helper/Raise.fs b/src/FSharp.Collections.Immutable/Helper/Raise.fs new file mode 100644 index 00000000..29f60999 --- /dev/null +++ b/src/FSharp.Collections.Immutable/Helper/Raise.fs @@ -0,0 +1,78 @@ +/// +/// The exceptions of the DetailedExceptions module of FSharp.Core, with the same types, messages and parameter names. +/// +/// +/// The callers pass the names of their own parameters (ADR 0001, D6). +/// +module internal FSharp.Collections.Immutable.Raise + +open System +open System.Collections.Generic + +let inline private invalidArgFormat (arg : string) (format : string) (paramArray : obj array) = + raise (ArgumentException (String.Format (format, paramArray), arg)) + +/// The arrays of a function of two arrays have different lengths. +let invalidArgDifferentArrayLength (arg1 : string) (len1 : int) (arg2 : string) (len2 : int) : 'T = + invalidArgFormat arg1 "{0}\n{1}.Length = {2}, {3}.Length = {4}" [| + ErrorStrings.ArraysHadDifferentLengths + arg1 + len1 + arg2 + len2 + |] + +/// The lists of a function of two lists have different lengths; `diff` is how much shorter `arg1` is. +let invalidArgDifferentListLength (arg1 : string) (arg2 : string) (diff : int) : 'T = + invalidArgFormat arg1 "{0}\n{1} is {2} {3} shorter than {4}" [| + ErrorStrings.ListsHadDifferentLengths + arg1 + diff + (if diff = 1 then "element" else "elements") + arg2 + |] + +/// The arrays of a function of three arrays have different lengths. +let invalidArg3ArraysDifferent (arg1 : string) (arg2 : string) (arg3 : string) (len1 : int) (len2 : int) (len3 : int) : 'T = + invalidArgFormat + (String.Concat [| arg1; ", "; arg2; ", "; arg3 |]) + "{0}\n {1}.Length = {2}, {3}.Length = {4}, {5}.Length = {6}" + [| ErrorStrings.ArraysHadDifferentLengths; arg1; len1; arg2; len2; arg3; len3 |] + +/// The lists of a function of three lists have different lengths. +let invalidArg3ListsDifferent (arg1 : string) (arg2 : string) (arg3 : string) (len1 : int) (len2 : int) (len3 : int) : 'T = + invalidArgFormat + (String.Concat [| arg1; ", "; arg2; ", "; arg3 |]) + "{0}\n {1}.Length = {2}, {3}.Length = {4}, {5}.Length = {6}" + [| ErrorStrings.ListsHadDifferentLengths; arg1; len1; arg2; len2; arg3; len3 |] + +/// A count or an index is negative. +let invalidArgInputMustBeNonNegative (arg : string) (count : int) : 'T = + invalidArgFormat arg "{0}\n{1} = {2}" [| ErrorStrings.InputMustBeNonNegative; arg; count |] + +/// A size is not positive. +let invalidArgInputMustBePositive (arg : string) (count : int) : 'T = + invalidArgFormat arg "{0}\n{1} = {2}" [| ErrorStrings.InputMustBePositive; arg; count |] + +/// An index or a count is beyond a bound. +let invalidArgOutOfRange (arg : string) (index : int) (text : string) (bound : int) : 'T = + let message = + String.Format ("{0}\n{1} = {2}, {3} = {4}", [| box ErrorStrings.OutOfRange; arg; index; text; bound |]) + + raise (ArgumentOutOfRangeException (arg, message)) + +/// A list is shorter than an index. +let invalidOpListNotEnoughElements (index : int) : 'T = + let message = + String.Format ( + "{0}\nThe list was {1} {2} shorter than the index", + [| box ErrorStrings.NotEnoughElements; index; (if index = 1 then "element" else "elements") |] + ) + + raise (InvalidOperationException message) + +/// No element satisfies the predicate of a `find` or `pick` function. +let indexNotFound () : 'T = raise (KeyNotFoundException ErrorStrings.KeyNotFoundAlt) + +/// A collection has fewer elements than a function needs. +let notEnoughElements () : 'T = raise (InvalidOperationException ErrorStrings.NotEnoughElements) From c633a795d65d56414f4cbd000e97de0e6435059c Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Mon, 19 Oct 2026 01:14:29 +0200 Subject: [PATCH 02/10] chore: give the tests access to internals through the project file The tests call internal algorithms directly, where reaching a case through a public function would need arrays too large for memory. The library project declares that access with an InternalsVisibleTo item, the project-file form of the InternalsVisibleTo attribute, so that no source file carries an assembly attribute for it. The item alone does nothing in this repository. AssemblyInfo.fs comes from the build script, so Directory.Build.props sets GenerateAssemblyInfo to false, and the SDK turns the items into attributes only in the target that GenerateAssemblyInfo guards. A test file that used an internal module failed to compile with FS0039. The library project therefore asks the SDK for the attributes of the items alone: it sets GenerateInternalsVisibleToAttributes, which the SDK leaves unset while GenerateAssemblyInfo is false, and runs the SDK target CoreGenerateAssemblyInfo before the compile. The generated file holds that one attribute, and the other assembly attributes stay as they are. The assemblies of both target frameworks now carry InternalsVisibleTo("FSharp.Collections.Immutable.Tests"), and the test file that failed to compile builds. Co-Authored-By: Claude Sonnet 5.5 --- .../FSharp.Collections.Immutable.fsproj | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj b/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj index 321383c7..ebdfb43b 100644 --- a/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj +++ b/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj @@ -47,6 +47,23 @@ + + + + + + + + true + + + + From 975eda469ead4942d5acbaede506190d54337713 Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Mon, 19 Oct 2026 14:06:33 +0200 Subject: [PATCH 03/10] refactor: name the resource of each text in `ErrorStrings` and drop the one that has none Every constant of the module copies one string resource of FSharp.Core (FSCore.resx) or of the .NET libraries, and the comment of each constant now names the resource: the key in FSCore.resx, or the key in System.Private.CoreLib. All texts but one were found in the resource tables of the assemblies (FSharp.Core 10.1, System.Private.CoreLib 10.0, System.Collections.Immutable 10.0) by comparing the strings. `ListsHaveDifferentLengths` ("The lists have different lengths.") is in none of them: it was the text of the modules written before ADR 0001, and no code uses it any more, so it is removed. Co-Authored-By: Claude Sonnet 5.5 --- .../Helper/ErrorStrings.fs | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/FSharp.Collections.Immutable/Helper/ErrorStrings.fs b/src/FSharp.Collections.Immutable/Helper/ErrorStrings.fs index e4ae49ec..1e42ecec 100644 --- a/src/FSharp.Collections.Immutable/Helper/ErrorStrings.fs +++ b/src/FSharp.Collections.Immutable/Helper/ErrorStrings.fs @@ -4,56 +4,68 @@ /// /// The modules throw what the FSharp.Core function that they mirror throws (ADR 0001, D6). FSharp.Core keeps its resources /// internal, so the texts are copied. The texts of the .NET libraries are for the functions that report a BCL exception -/// under the name of their own parameter. +/// under the name of their own parameter. Each constant is the text of one resource and its comment names the resource, +/// so a text that no resource of FSharp.Core or of the .NET libraries holds does not belong here. /// module internal FSharp.Collections.Immutable.ErrorStrings +/// FSCore.resx, arraysHadDifferentLengths. [] let ArraysHadDifferentLengths = "The arrays have different lengths." +/// FSCore.resx, indexOutOfBounds. [] let IndexOutOfBounds = "The index was outside the range of elements in the list." +/// FSCore.resx, arrayWasEmpty. [] let InputArrayEmpty = "The input array was empty." +/// FSCore.resx, inputListWasEmpty. [] let InputListWasEmpty = "The input list was empty." +/// FSCore.resx, inputMustBeNonNegative. [] let InputMustBeNonNegative = "The input must be non-negative." +/// FSCore.resx, inputMustBePositive. [] let InputMustBePositive = "The input must be positive." +/// FSCore.resx, inputSequenceEmpty. [] let InputSequenceEmpty = "The input sequence was empty." +/// FSCore.resx, inputSequenceTooLong. [] let InputSequenceTooLong = "The input sequence contains more than one element." +/// FSCore.resx, keyNotFound. [] let KeyNotFound = "The item, key, or index was not found in the collection." +/// FSCore.resx, keyNotFoundAlt. [] let KeyNotFoundAlt = "An index satisfying the predicate was not found in the collection." +/// FSCore.resx, listsHadDifferentLengths. [] let ListsHadDifferentLengths = "The lists had different lengths." -/// The message of the modules written before ADR 0001. -[] -let ListsHaveDifferentLengths = "The lists have different lengths." - -/// The message of the .NET libraries for a negative argument, as `Array.BinarySearch` reports it. +/// System.Private.CoreLib, ArgumentOutOfRange_NeedNonNegNum: the message of the .NET libraries for a negative argument, as +/// `Array.BinarySearch` reports it. [] let NeedNonNegativeNumber = "Non-negative number required." +/// FSCore.resx, notAPermutation. [] let NotAPermutation = "The function did not compute a permutation." +/// FSCore.resx, notEnoughElements. [] let NotEnoughElements = "The input sequence has an insufficient number of elements." +/// FSCore.resx, outOfRange. [] let OutOfRange = "The index is outside the legal range." From 0b4024bdb1dc1c4c99aeaa7dc15c11f5c580e9c9 Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Mon, 19 Oct 2026 14:19:27 +0200 Subject: [PATCH 04/10] build: generate the InternalsVisibleTo attribute with the properties of the SDK, not with a target The SDK turns InternalsVisibleTo items into an assembly attribute only when GenerateAssemblyInfo is true, and Directory.Build.props sets it to false because the build script writes AssemblyInfo.fs. The project now sets it to true and switches off, with the documented Generate...Attribute properties, every attribute that AssemblyInfo.fs or the package properties already provide, so that the SDK generates the InternalsVisibleTo attribute alone. That replaces the target that called the internal SDK target CoreGenerateAssemblyInfo. The assemblies of both target frameworks carry the same attributes as before plus InternalsVisibleTo("FSharp.Collections.Immutable.Tests"), as the listing of their custom attributes shows. The properties are in the first PropertyGroup, and the item is above the item group of the compile items. Co-Authored-By: Claude Sonnet 5.5 --- .../FSharp.Collections.Immutable.fsproj | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj b/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj index ebdfb43b..c0ff21cd 100644 --- a/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj +++ b/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj @@ -5,6 +5,20 @@ $(AssemblyBaseName) true true + + true + false + false + false + false + false + false + false + false + false @@ -18,6 +32,12 @@ true + + + + + @@ -47,23 +67,6 @@ - - - - - - - - true - - - - From 5dbc87db7067030041da360662af8e3b0c5d974c Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Mon, 19 Oct 2026 14:19:27 +0200 Subject: [PATCH 05/10] refactor: read IsDefault through a piped helper The expression that reads the property IsDefault of a boxed immutable array through reflection becomes the inline function readIsDefault, a pipeline of the lookup of the property, the check that it exists and the read. The function stays inline like isDefaultImmutableArray, which the public inline members of the computation expressions use. Co-Authored-By: Claude Sonnet 5.5 --- .../Helper/ImmutableCollectionUtil.fs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/FSharp.Collections.Immutable/Helper/ImmutableCollectionUtil.fs b/src/FSharp.Collections.Immutable/Helper/ImmutableCollectionUtil.fs index b64dcc78..9f5ac4c9 100644 --- a/src/FSharp.Collections.Immutable/Helper/ImmutableCollectionUtil.fs +++ b/src/FSharp.Collections.Immutable/Helper/ImmutableCollectionUtil.fs @@ -24,6 +24,13 @@ let inline checkNotDefault name (array : ImmutableArray<'T>) = if array.IsDefault then nullArg name +/// Reads the property IsDefault of a boxed immutable array through reflection, from the type of the array. +let inline readIsDefault (arrayType : Type) (array : obj) : bool = + arrayType.GetProperty "IsDefault" + |> nonNull + |> _.GetValue(array) + |> unbox + /// /// Tells whether an object is a default , of any element type. /// @@ -44,7 +51,7 @@ let inline isDefaultImmutableArray (source : obj) : bool = sourceType.IsGenericType && Type.(=) (sourceType.GetGenericTypeDefinition (), typedefof>) - && unbox((nonNull (sourceType.GetProperty "IsDefault")).GetValue source) + && readIsDefault sourceType source /// /// Throws for a sequence and for a default From 3aa7f18868872cd3e153e4e5ec2639c9f61a8e48 Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Mon, 19 Oct 2026 14:19:27 +0200 Subject: [PATCH 06/10] refactor: build the messages of the Raise module with interpolated strings The exceptions no longer go through String.Format with a format string and an array of boxed values. Each message is an interpolated string, and the helper that formatted the array is gone. A script called every function with 31 argument combinations in six cultures (invariant, en-US, de-DE, sv-SE, ar-SA and fa-IR) before and after the change: the 186 messages are identical, the minus sign of sv-SE included, because a hole without a format specifier formats with the current culture as String.Format does, as the messages of FSharp.Core do. Co-Authored-By: Claude Sonnet 5.5 --- .../Helper/Raise.fs | 57 ++++++++----------- 1 file changed, 24 insertions(+), 33 deletions(-) diff --git a/src/FSharp.Collections.Immutable/Helper/Raise.fs b/src/FSharp.Collections.Immutable/Helper/Raise.fs index 29f60999..b36a1141 100644 --- a/src/FSharp.Collections.Immutable/Helper/Raise.fs +++ b/src/FSharp.Collections.Immutable/Helper/Raise.fs @@ -9,65 +9,56 @@ module internal FSharp.Collections.Immutable.Raise open System open System.Collections.Generic -let inline private invalidArgFormat (arg : string) (format : string) (paramArray : obj array) = - raise (ArgumentException (String.Format (format, paramArray), arg)) - /// The arrays of a function of two arrays have different lengths. let invalidArgDifferentArrayLength (arg1 : string) (len1 : int) (arg2 : string) (len2 : int) : 'T = - invalidArgFormat arg1 "{0}\n{1}.Length = {2}, {3}.Length = {4}" [| - ErrorStrings.ArraysHadDifferentLengths - arg1 - len1 - arg2 - len2 - |] + let message = + $"{ErrorStrings.ArraysHadDifferentLengths}\n{arg1}.Length = {len1}, {arg2}.Length = {len2}" + + raise (ArgumentException (message, arg1)) /// The lists of a function of two lists have different lengths; `diff` is how much shorter `arg1` is. let invalidArgDifferentListLength (arg1 : string) (arg2 : string) (diff : int) : 'T = - invalidArgFormat arg1 "{0}\n{1} is {2} {3} shorter than {4}" [| - ErrorStrings.ListsHadDifferentLengths - arg1 - diff - (if diff = 1 then "element" else "elements") - arg2 - |] + let elements = if diff = 1 then "element" else "elements" + + let message = + $"{ErrorStrings.ListsHadDifferentLengths}\n{arg1} is {diff} {elements} shorter than {arg2}" + + raise (ArgumentException (message, arg1)) /// The arrays of a function of three arrays have different lengths. let invalidArg3ArraysDifferent (arg1 : string) (arg2 : string) (arg3 : string) (len1 : int) (len2 : int) (len3 : int) : 'T = - invalidArgFormat - (String.Concat [| arg1; ", "; arg2; ", "; arg3 |]) - "{0}\n {1}.Length = {2}, {3}.Length = {4}, {5}.Length = {6}" - [| ErrorStrings.ArraysHadDifferentLengths; arg1; len1; arg2; len2; arg3; len3 |] + let message = + $"{ErrorStrings.ArraysHadDifferentLengths}\n {arg1}.Length = {len1}, {arg2}.Length = {len2}, {arg3}.Length = {len3}" + + raise (ArgumentException (message, $"{arg1}, {arg2}, {arg3}")) /// The lists of a function of three lists have different lengths. let invalidArg3ListsDifferent (arg1 : string) (arg2 : string) (arg3 : string) (len1 : int) (len2 : int) (len3 : int) : 'T = - invalidArgFormat - (String.Concat [| arg1; ", "; arg2; ", "; arg3 |]) - "{0}\n {1}.Length = {2}, {3}.Length = {4}, {5}.Length = {6}" - [| ErrorStrings.ListsHadDifferentLengths; arg1; len1; arg2; len2; arg3; len3 |] + let message = + $"{ErrorStrings.ListsHadDifferentLengths}\n {arg1}.Length = {len1}, {arg2}.Length = {len2}, {arg3}.Length = {len3}" + + raise (ArgumentException (message, $"{arg1}, {arg2}, {arg3}")) /// A count or an index is negative. let invalidArgInputMustBeNonNegative (arg : string) (count : int) : 'T = - invalidArgFormat arg "{0}\n{1} = {2}" [| ErrorStrings.InputMustBeNonNegative; arg; count |] + raise (ArgumentException ($"{ErrorStrings.InputMustBeNonNegative}\n{arg} = {count}", arg)) /// A size is not positive. let invalidArgInputMustBePositive (arg : string) (count : int) : 'T = - invalidArgFormat arg "{0}\n{1} = {2}" [| ErrorStrings.InputMustBePositive; arg; count |] + raise (ArgumentException ($"{ErrorStrings.InputMustBePositive}\n{arg} = {count}", arg)) /// An index or a count is beyond a bound. let invalidArgOutOfRange (arg : string) (index : int) (text : string) (bound : int) : 'T = - let message = - String.Format ("{0}\n{1} = {2}, {3} = {4}", [| box ErrorStrings.OutOfRange; arg; index; text; bound |]) + let message = $"{ErrorStrings.OutOfRange}\n{arg} = {index}, {text} = {bound}" raise (ArgumentOutOfRangeException (arg, message)) /// A list is shorter than an index. let invalidOpListNotEnoughElements (index : int) : 'T = + let elements = if index = 1 then "element" else "elements" + let message = - String.Format ( - "{0}\nThe list was {1} {2} shorter than the index", - [| box ErrorStrings.NotEnoughElements; index; (if index = 1 then "element" else "elements") |] - ) + $"{ErrorStrings.NotEnoughElements}\nThe list was {index} {elements} shorter than the index" raise (InvalidOperationException message) From 76919117b54ec4c77c6f98a6495f5375936a7ed2 Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Mon, 19 Oct 2026 15:05:09 +0200 Subject: [PATCH 07/10] perf: reject a class before reading the generic definition in isDefaultImmutableArray A boxed immutable array is a value type, so isDefaultImmutableArray tests the IsValueType flag of the type first. A class (List, an F# list, a HashSet or a sequence expression) is rejected by that one flag, and the generic definition is read only for a boxed generic structure. In a micro-benchmark of 20 million calls the check for such a class takes 5.9 ns instead of 7.2 ns. The tests that give a default flat list of strings as a sequence of objects reach the reflection path through this check: a mutation that negates the flag makes them fail. Co-Authored-By: Claude Sonnet 5.5 --- .../Helper/ImmutableCollectionUtil.fs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/FSharp.Collections.Immutable/Helper/ImmutableCollectionUtil.fs b/src/FSharp.Collections.Immutable/Helper/ImmutableCollectionUtil.fs index 9f5ac4c9..5a7e4eae 100644 --- a/src/FSharp.Collections.Immutable/Helper/ImmutableCollectionUtil.fs +++ b/src/FSharp.Collections.Immutable/Helper/ImmutableCollectionUtil.fs @@ -42,14 +42,17 @@ let inline readIsDefault (arrayType : Type) (array : obj) : bool = /// /// /// The function finds the instantiation from the type of the object and reads the property IsDefault through -/// reflection. It allocates nothing for an object of another type. It is inline, because the public inline members of -/// the computation expression call the check, and an inline member cannot call an internal function. +/// reflection. A boxed immutable array is a value type, so every class is rejected first by one flag of its type, before +/// the generic definition is read. The function allocates nothing for an object of another type. It is inline, because +/// the public inline members of the computation expression call the check, and an inline member cannot call an internal +/// function. /// /// let inline isDefaultImmutableArray (source : obj) : bool = let sourceType = source.GetType () - sourceType.IsGenericType + sourceType.IsValueType + && sourceType.IsGenericType && Type.(=) (sourceType.GetGenericTypeDefinition (), typedefof>) && readIsDefault sourceType source From 5815d561dcfe2c63f69248d67ec00ecd81fbf48b Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Mon, 19 Oct 2026 15:05:10 +0200 Subject: [PATCH 08/10] docs: say why each algorithm of Arrays exists The comment of the module and of each function now give the reason: the function of the FSharp.Core Array module returns or takes reference tuples (or, for the keys of countBy and groupBy, needs a wrapper for null keys and a comparer for keys that are arrays), where FlatList returns struct tuples and the Tuple namespace returns reference tuples. Each algorithm has exactly two callers. The numbers come from a measurement of 10 000 elements: zip allocates 80 KB for struct tuples where Array.zip followed by a conversion allocates 400 KB, and allPairs of 300 by 300 elements allocates 720 KB where the conversion allocates 3.6 MB. Co-Authored-By: Claude Sonnet 5.5 --- .../Helper/Arrays.fs | 34 ++++++++++++++----- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/src/FSharp.Collections.Immutable/Helper/Arrays.fs b/src/FSharp.Collections.Immutable/Helper/Arrays.fs index 3dedbd3a..ee5d9f6a 100644 --- a/src/FSharp.Collections.Immutable/Helper/Arrays.fs +++ b/src/FSharp.Collections.Immutable/Helper/Arrays.fs @@ -1,10 +1,13 @@ -/// -/// Algorithms over arrays that the default modules and the Tuple modules share (ADR 0001, D3). -/// -/// +/// Algorithms over arrays that the default modules and the `Tuple` modules share (ADR 0001, D3). /// Each algorithm takes the constructor of the tuples that it produces, so that neither variant pays for a second pass. /// The callers check their arguments first, with the exceptions of the FSharp.Core module that they mirror. -/// +/// The algorithms exist because the functions of the FSharp.Core `Array` module that build or take pairs and triples work +/// with reference tuples. The default functions of `FlatList` return struct tuples, and the functions of the `Tuple` +/// namespace return reference tuples. Calling the `Array` function and converting its result would allocate every +/// reference tuple and a second array. Each algorithm has exactly two callers, the function of `FlatList` with +/// `structPair` or `structTriple` and the function of `Tuple.FlatList` with `referencePair` or `referenceTriple`, so +/// that it exists once. For 10 000 elements, `zip` of struct tuples allocates 80 KB where `Array.zip` followed by a +/// conversion allocates 400 KB. module internal FSharp.Collections.Immutable.Arrays open System @@ -12,6 +15,8 @@ open System.Collections.Generic open System.Linq /// The pairs of every element of `array1` with every element of `array2`. +/// It exists because `Array.allPairs` returns reference tuples. `FlatList.allPairs` builds struct tuples with it +/// directly (720 KB for 300 by 300 elements, where `Array.allPairs` followed by a conversion allocates 3.6 MB). let inline allPairs ([] tuple : 'T1 -> 'T2 -> 'R) (array1 : 'T1 array) (array2 : 'T2 array) : 'R array = let length2 = array2.Length let result = Array.zeroCreate (array1.Length * length2) @@ -23,10 +28,14 @@ let inline allPairs ([] tuple : 'T1 -> 'T2 -> 'R) (array1 : 'T1 result /// A key that may be null, for a dictionary, which rejects null keys. +/// It exists because `Array.countBy` and `Array.groupBy` accept a null key (`[| null; "a"; null |]` has two keys), and +/// `Dictionary` throws `ArgumentNullException` for one. [] type NullableKey<'Key> = { Key : 'Key } /// Compares wrapped keys in F# structural equality, which handles null keys, as FSharp.Core does for its keys. +/// It exists because the default comparer of a `Dictionary` compares array keys by reference, where `Array.countBy` +/// finds `[| 1 |]` and `[| 1 |]` equal. let nullableKeyComparer<'Key when 'Key : equality> () = let structural = HashIdentity.Structural<'Key> @@ -36,12 +45,9 @@ let nullableKeyComparer<'Key when 'Key : equality> () = member _.GetHashCode key = structural.GetHashCode key.Key } -/// /// The keys of the projection in the order of their first occurrence, each with the number of its elements. -/// -/// /// The counting keeps one counter per key, not the elements, so it needs memory for the keys only. -/// +/// It exists because `Array.countBy` returns reference tuples; null keys and keys that are arrays work as in FSharp.Core. let inline countBy ([] tuple : 'Key -> int -> 'R) (projection : 'T -> 'Key) @@ -70,6 +76,7 @@ let inline countBy result /// The keys of the projection in the order of their first occurrence, each with its elements in their order. +/// It exists because `Array.groupBy` returns reference tuples. It groups once with a lookup, as the project rules ask. let inline groupBy ([] tuple : 'Key -> 'T array -> 'R) (projection : 'T -> 'Key) @@ -86,6 +93,7 @@ let inline groupBy result /// Every element with its index. +/// It exists because `Array.indexed` returns reference tuples (320 KB for 10 000 elements, where struct tuples take 80 KB). let inline indexed ([] tuple : int -> 'T -> 'R) (array : 'T array) : 'R array = let result = Array.zeroCreate array.Length @@ -95,6 +103,7 @@ let inline indexed ([] tuple : int -> 'T -> 'R) (array : 'T arra result /// Every element with its successor. +/// It exists because `Array.pairwise` returns reference tuples. let inline pairwise ([] tuple : 'T -> 'T -> 'R) (array : 'T array) : 'R array = if array.Length < 2 then Array.Empty () @@ -107,6 +116,9 @@ let inline pairwise ([] tuple : 'T -> 'T -> 'R) (array : 'T arra result /// Splits the pairs of an array with `split` into two arrays. +/// It exists because `Array.unzip` takes reference tuples. `FlatList.unzip` has struct tuples and passes `id` as `split`, +/// `Tuple.FlatList.unzip` has reference tuples and converts one pair at a time, so that no array of converted pairs +/// (400 KB for 10 000 pairs) is built. let inline unzip ([] split : 'P -> struct ('T1 * 'T2)) (array : 'P array) = let result1 = Array.zeroCreate array.Length let result2 = Array.zeroCreate array.Length @@ -119,6 +131,7 @@ let inline unzip ([] split : 'P -> struct ('T1 * 'T2)) (array : struct (result1, result2) /// Splits the triples of an array with `split` into three arrays. +/// It exists for the reason of `unzip`: `Array.unzip3` takes reference tuples. let inline unzip3 ([] split : 'P -> struct ('T1 * 'T2 * 'T3)) (array : 'P array) = let result1 = Array.zeroCreate array.Length let result2 = Array.zeroCreate array.Length @@ -133,6 +146,8 @@ let inline unzip3 ([] split : 'P -> struct ('T1 * 'T2 * 'T3)) (a struct (result1, result2, result3) /// Pairs the elements of two arrays of the same length. +/// It exists because `Array.zip` returns reference tuples (400 KB for 10 000 elements with the conversion, 80 KB here). +/// The callers check the lengths first, with the exception of `Array.zip`. let inline zip ([] tuple : 'T1 -> 'T2 -> 'R) (array1 : 'T1 array) (array2 : 'T2 array) : 'R array = let result = Array.zeroCreate array1.Length @@ -142,6 +157,7 @@ let inline zip ([] tuple : 'T1 -> 'T2 -> 'R) (array1 : 'T1 array result /// Combines the elements of three arrays of the same length. +/// It exists for the reason of `zip`: `Array.zip3` returns reference tuples. The callers check the lengths first. let inline zip3 ([] tuple : 'T1 -> 'T2 -> 'T3 -> 'R) (array1 : 'T1 array) From 0e3d5225ff00bb7d9cf649d074c9617c885b024d Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Mon, 19 Oct 2026 15:08:03 +0200 Subject: [PATCH 09/10] docs: start the comments of Arrays with the summaries of the FSharp.Core functions The comment of each function of Arrays now begins with the summary of the function of the FSharp.Core Array module that it mirrors, word for word (FSharp.Core 10.1, read from its documentation file), and the sentences after it say what differs and why the function exists. The summary of Array.zip3 says "an array of pairs", which is a slip, so the comment of zip3 says triples and notes the slip. NullableKey has no original and says so. Co-Authored-By: Claude Sonnet 5.5 --- .../Helper/Arrays.fs | 63 ++++++++++++------- 1 file changed, 39 insertions(+), 24 deletions(-) diff --git a/src/FSharp.Collections.Immutable/Helper/Arrays.fs b/src/FSharp.Collections.Immutable/Helper/Arrays.fs index ee5d9f6a..b09b927a 100644 --- a/src/FSharp.Collections.Immutable/Helper/Arrays.fs +++ b/src/FSharp.Collections.Immutable/Helper/Arrays.fs @@ -8,15 +8,18 @@ /// `structPair` or `structTriple` and the function of `Tuple.FlatList` with `referencePair` or `referenceTriple`, so /// that it exists once. For 10 000 elements, `zip` of struct tuples allocates 80 KB where `Array.zip` followed by a /// conversion allocates 400 KB. +/// The first sentences of the comment of each function are the summary of the function of the FSharp.Core `Array` module +/// word for word (FSharp.Core 10.1), and the sentences after them say what differs and why the function exists. module internal FSharp.Collections.Immutable.Arrays open System open System.Collections.Generic open System.Linq -/// The pairs of every element of `array1` with every element of `array2`. -/// It exists because `Array.allPairs` returns reference tuples. `FlatList.allPairs` builds struct tuples with it -/// directly (720 KB for 300 by 300 elements, where `Array.allPairs` followed by a conversion allocates 3.6 MB). +/// Returns a new array that contains all pairings of elements from the first and second arrays. +/// The pairs are built with `tuple`. It exists because `Array.allPairs` returns reference tuples. `FlatList.allPairs` +/// builds struct tuples with it directly (720 KB for 300 by 300 elements, where `Array.allPairs` followed by a conversion +/// allocates 3.6 MB). let inline allPairs ([] tuple : 'T1 -> 'T2 -> 'R) (array1 : 'T1 array) (array2 : 'T2 array) : 'R array = let length2 = array2.Length let result = Array.zeroCreate (array1.Length * length2) @@ -27,7 +30,7 @@ let inline allPairs ([] tuple : 'T1 -> 'T2 -> 'R) (array1 : 'T1 result -/// A key that may be null, for a dictionary, which rejects null keys. +/// A key that may be null, for a dictionary, which rejects null keys. FSharp.Core has no such type in its public API. /// It exists because `Array.countBy` and `Array.groupBy` accept a null key (`[| null; "a"; null |]` has two keys), and /// `Dictionary` throws `ArgumentNullException` for one. [] @@ -45,9 +48,11 @@ let nullableKeyComparer<'Key when 'Key : equality> () = member _.GetHashCode key = structural.GetHashCode key.Key } -/// The keys of the projection in the order of their first occurrence, each with the number of its elements. -/// The counting keeps one counter per key, not the elements, so it needs memory for the keys only. -/// It exists because `Array.countBy` returns reference tuples; null keys and keys that are arrays work as in FSharp.Core. +/// Applies a key-generating function to each element of an array and returns an array yielding unique keys and their +/// number of occurrences in the original array. +/// The unique keys come in the order of their first occurrence, and the counting keeps one counter per key, not the +/// elements, so it needs memory for the keys only. The tuples are built with `tuple`. It exists because `Array.countBy` +/// returns reference tuples; null keys and keys that are arrays work as in FSharp.Core. let inline countBy ([] tuple : 'Key -> int -> 'R) (projection : 'T -> 'Key) @@ -75,8 +80,11 @@ let inline countBy result -/// The keys of the projection in the order of their first occurrence, each with its elements in their order. -/// It exists because `Array.groupBy` returns reference tuples. It groups once with a lookup, as the project rules ask. +/// Applies a key-generating function to each element of an array and yields an array of unique keys. Each unique key +/// contains an array of all elements that match to this key. +/// The keys come in the order of their first occurrence, and the elements of a key in their order. The tuples are built +/// with `tuple`. It exists because `Array.groupBy` returns reference tuples. It groups once with a lookup, as the project +/// rules ask. let inline groupBy ([] tuple : 'Key -> 'T array -> 'R) (projection : 'T -> 'Key) @@ -92,8 +100,10 @@ let inline groupBy result -/// Every element with its index. -/// It exists because `Array.indexed` returns reference tuples (320 KB for 10 000 elements, where struct tuples take 80 KB). +/// Builds a new array whose elements are the corresponding elements of the input array paired with the integer index +/// (from 0) of each element. +/// The pairs are built with `tuple`. It exists because `Array.indexed` returns reference tuples (320 KB for 10 000 +/// elements, where struct tuples take 80 KB). let inline indexed ([] tuple : int -> 'T -> 'R) (array : 'T array) : 'R array = let result = Array.zeroCreate array.Length @@ -102,8 +112,10 @@ let inline indexed ([] tuple : int -> 'T -> 'R) (array : 'T arra result -/// Every element with its successor. -/// It exists because `Array.pairwise` returns reference tuples. +/// Returns an array of each element in the input array paired with its predecessor, with the exception of the first +/// element which is only returned as the predecessor of the second element. The predecessor comes first in the returned +/// pairs. +/// The pairs are built with `tuple`. It exists because `Array.pairwise` returns reference tuples. let inline pairwise ([] tuple : 'T -> 'T -> 'R) (array : 'T array) : 'R array = if array.Length < 2 then Array.Empty () @@ -115,10 +127,10 @@ let inline pairwise ([] tuple : 'T -> 'T -> 'R) (array : 'T arra result -/// Splits the pairs of an array with `split` into two arrays. -/// It exists because `Array.unzip` takes reference tuples. `FlatList.unzip` has struct tuples and passes `id` as `split`, -/// `Tuple.FlatList.unzip` has reference tuples and converts one pair at a time, so that no array of converted pairs -/// (400 KB for 10 000 pairs) is built. +/// Splits an array of pairs into two arrays. +/// `split` reads a pair as a struct tuple. It exists because `Array.unzip` takes reference tuples. `FlatList.unzip` has +/// struct tuples and passes `id` as `split`, `Tuple.FlatList.unzip` has reference tuples and converts one pair at a time, +/// so that no array of converted pairs (400 KB for 10 000 pairs) is built. let inline unzip ([] split : 'P -> struct ('T1 * 'T2)) (array : 'P array) = let result1 = Array.zeroCreate array.Length let result2 = Array.zeroCreate array.Length @@ -130,8 +142,8 @@ let inline unzip ([] split : 'P -> struct ('T1 * 'T2)) (array : struct (result1, result2) -/// Splits the triples of an array with `split` into three arrays. -/// It exists for the reason of `unzip`: `Array.unzip3` takes reference tuples. +/// Splits an array of triples into three arrays. +/// `split` reads a triple as a struct tuple. It exists for the reason of `unzip`: `Array.unzip3` takes reference tuples. let inline unzip3 ([] split : 'P -> struct ('T1 * 'T2 * 'T3)) (array : 'P array) = let result1 = Array.zeroCreate array.Length let result2 = Array.zeroCreate array.Length @@ -145,9 +157,10 @@ let inline unzip3 ([] split : 'P -> struct ('T1 * 'T2 * 'T3)) (a struct (result1, result2, result3) -/// Pairs the elements of two arrays of the same length. -/// It exists because `Array.zip` returns reference tuples (400 KB for 10 000 elements with the conversion, 80 KB here). -/// The callers check the lengths first, with the exception of `Array.zip`. +/// Combines the two arrays into an array of pairs. The two arrays must have equal lengths, otherwise an ArgumentException +/// is raised. +/// The pairs are built with `tuple`, and the callers check the lengths first and raise the exception of `Array.zip`. It +/// exists because `Array.zip` returns reference tuples (400 KB for 10 000 elements with the conversion, 80 KB here). let inline zip ([] tuple : 'T1 -> 'T2 -> 'R) (array1 : 'T1 array) (array2 : 'T2 array) : 'R array = let result = Array.zeroCreate array1.Length @@ -156,8 +169,10 @@ let inline zip ([] tuple : 'T1 -> 'T2 -> 'R) (array1 : 'T1 array result -/// Combines the elements of three arrays of the same length. -/// It exists for the reason of `zip`: `Array.zip3` returns reference tuples. The callers check the lengths first. +/// Combines three arrays into an array of triples. The three arrays must have equal lengths, otherwise an +/// ArgumentException is raised. (The summary of `Array.zip3` says "an array of pairs", which is a slip.) +/// The triples are built with `tuple`, and the callers check the lengths first. It exists for the reason of `zip`: +/// `Array.zip3` returns reference tuples. let inline zip3 ([] tuple : 'T1 -> 'T2 -> 'T3 -> 'R) (array1 : 'T1 array) From 0685f2b4b7b86133daef231be9bf692f90f1a674 Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Mon, 19 Oct 2026 16:34:06 +0200 Subject: [PATCH 10/10] build: drop the properties that switch off the assembly attributes of the SDK The library project switched on the generation of assembly attributes, which Directory.Build.props had switched off, and then switched off the nine attributes that AssemblyInfo.fs provided, so that the SDK generated the InternalsVisibleTo attribute alone. FAKE writes the assembly information as MSBuild properties now, and the SDK generates all attributes. The InternalsVisibleTo item works as it is, and the project needs none of the properties. Co-Authored-By: Claude Sonnet 5.5 --- .../FSharp.Collections.Immutable.fsproj | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj b/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj index c0ff21cd..e7284ad8 100644 --- a/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj +++ b/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj @@ -5,20 +5,6 @@ $(AssemblyBaseName) true true - - true - false - false - false - false - false - false - false - false - false