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..e7284ad8 100644 --- a/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj +++ b/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj @@ -18,9 +18,20 @@ true + + + + + - + + + + + + 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..b09b927a --- /dev/null +++ b/src/FSharp.Collections.Immutable/Helper/Arrays.fs @@ -0,0 +1,187 @@ +/// 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. +/// 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 + +/// 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) + + 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. 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. +[] +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> + + { + new IEqualityComparer> with + member _.Equals (left, right) = structural.Equals (left.Key, right.Key) + member _.GetHashCode key = structural.GetHashCode key.Key + } + +/// 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) + (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 + +/// 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) + (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 + +/// 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 + + for i = 0 to result.Length - 1 do + result[i] <- tuple i array[i] + + result + +/// 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 () + 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 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 + + 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 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 + 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) + +/// 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 + + for i = 0 to result.Length - 1 do + result[i] <- tuple array1[i] array2[i] + + result + +/// 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) + (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..1e42ecec --- /dev/null +++ b/src/FSharp.Collections.Immutable/Helper/ErrorStrings.fs @@ -0,0 +1,71 @@ +/// +/// 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. 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." + +/// 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." diff --git a/src/FSharp.Collections.Immutable/Helper/ImmutableCollectionUtil.fs b/src/FSharp.Collections.Immutable/Helper/ImmutableCollectionUtil.fs new file mode 100644 index 00000000..5a7e4eae --- /dev/null +++ b/src/FSharp.Collections.Immutable/Helper/ImmutableCollectionUtil.fs @@ -0,0 +1,116 @@ +/// 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 + +/// 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. +/// +/// +/// +/// 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. 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.IsValueType + && sourceType.IsGenericType + && Type.(=) (sourceType.GetGenericTypeDefinition (), typedefof>) + && readIsDefault sourceType 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..b36a1141 --- /dev/null +++ b/src/FSharp.Collections.Immutable/Helper/Raise.fs @@ -0,0 +1,69 @@ +/// +/// 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 + +/// The arrays of a function of two arrays have different lengths. +let invalidArgDifferentArrayLength (arg1 : string) (len1 : int) (arg2 : string) (len2 : int) : 'T = + 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 = + 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 = + 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 = + 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 = + raise (ArgumentException ($"{ErrorStrings.InputMustBeNonNegative}\n{arg} = {count}", arg)) + +/// A size is not positive. +let invalidArgInputMustBePositive (arg : string) (count : int) : 'T = + 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 = $"{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 = + $"{ErrorStrings.NotEnoughElements}\nThe list was {index} {elements} shorter than the index" + + 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)