From 45e75d5da0d98f71c3e419ca70b756f7c7dae378 Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Sun, 18 Oct 2026 12:45:41 +0200 Subject: [PATCH 01/14] feat: `FlatList.Parallel` module with the vocabulary of the FSharp.Core `Array.Parallel` module Adds the nested module `Parallel` at the end of `FlatList` in `FlatList/FlatList.fsi` and `FlatList/FlatList.fs`, as `Array.Parallel` lives inside `Array`: 32 functions in the canonical category order, each documented in the signature file and compiled under the name that `Array.Parallel` gives it. The `sortInPlace*` functions are left out. - Most functions unwrap the underlying array without a copy, call the `Array.Parallel` function and wrap its fresh result, so that they keep its semantics and exceptions, including the `OverflowException` of `init` for a negative count. `sum` and `sumBy` throw `ArgumentNullException` for a default flat list, which `Array.Parallel` documents but does not check. - `tryFind`, `tryFindIndex` and `tryPick` return `voption`, `choose` and `tryPick` take choosers that return `voption`, and `zip`, `groupBy`, `partition` and `partitionWith` return struct tuples. Where the `Array.Parallel` function takes or returns options or builds a tuple per element or per group, a new `ParallelArrays` algorithm in `Helper.fs` runs instead and allocates nothing per element: `choose` maps to value options in parallel and compacts them, `tryPick` keeps the value of the lowest index under a lock, `groupBy` computes the keys in parallel and groups them with `ToLookup`, and `zip` pairs the elements in the chunks of `Array.Parallel.zip`. - The `Parallel` modules of the `Option` and `Tuple` namespaces repeat `choose`, `tryFind`, `tryFindIndex` and `tryPick` with options, and `groupBy`, `partition`, `partitionWith` and `zip` with reference tuples. Co-Authored-By: Claude Opus 5.5 --- .../FSharp.Collections.Immutable.fsproj | 1 + .../FlatList/FlatList.fs | 200 ++++++++++ .../FlatList/FlatList.fsi | 343 ++++++++++++++++++ .../FlatList/FlatListOption.fs | 24 ++ .../FlatList/FlatListOption.fsi | 51 +++ .../FlatList/FlatListTuple.fs | 37 ++ .../FlatList/FlatListTuple.fsi | 53 +++ .../Helper/ParallelArrays.fs | 175 +++++++++ 8 files changed, 884 insertions(+) create mode 100644 src/FSharp.Collections.Immutable/Helper/ParallelArrays.fs diff --git a/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj b/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj index e7284ad8..293de773 100644 --- a/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj +++ b/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj @@ -32,6 +32,7 @@ + diff --git a/src/FSharp.Collections.Immutable/FlatList/FlatList.fs b/src/FSharp.Collections.Immutable/FlatList/FlatList.fs index 9ac7375d..01db002d 100644 --- a/src/FSharp.Collections.Immutable/FlatList/FlatList.fs +++ b/src/FSharp.Collections.Immutable/FlatList/FlatList.fs @@ -1006,3 +1006,203 @@ let isDefault (array : FlatList<'T>) = array.IsDefault [] let isDefaultOrEmpty (array : FlatList<'T>) = array.IsDefaultOrEmpty + +// Most functions unwrap the underlying array without a copy, call the Array.Parallel function, and wrap its fresh result +// without a copy (ADR 0001, D11), so that they have the semantics and the exceptions of Array.Parallel. Where the +// Array.Parallel function takes or returns options or builds a tuple per element or per group (choose, tryFind, +// tryFindIndex, tryPick, zip, groupBy), an algorithm of ParallelArrays with value options and struct tuples runs instead. +// Array.Parallel sorts a copy of its input, so the shared array is never changed. +[] +module Parallel = + + // ---- Creation ---- + + [] + let init count (initializer : int -> 'T) = Array.Parallel.init count initializer |> wrapArray + + // ---- Search ---- + + [] + let exists predicate (array : FlatList<'T>) = Array.Parallel.exists predicate (unwrapArray (nameof array) array) + + [] + let forall predicate (array : FlatList<'T>) = Array.Parallel.forall predicate (unwrapArray (nameof array) array) + + [] + let tryFind predicate (array : FlatList<'T>) = + let source = unwrapArray (nameof array) array + + match ParallelArrays.tryFindIndex predicate source with + | ValueSome index -> ValueSome source[index] + | ValueNone -> ValueNone + + [] + let tryFindIndex predicate (array : FlatList<'T>) = + ParallelArrays.tryFindIndex predicate (unwrapArray (nameof array) array) + + [] + let tryPick (chooser : 'T -> 'U voption) (array : FlatList<'T>) = + ParallelArrays.tryPick chooser (unwrapArray (nameof array) array) + + // ---- Transformation ---- + + [] + let choose (chooser : 'T -> 'U voption) (array : FlatList<'T>) = + ParallelArrays.choose chooser (unwrapArray (nameof array) array) + |> wrapArray + + [] + let collect (mapping : 'T -> FlatList<'U>) (array : FlatList<'T>) = + // As in Array.Parallel.collect, the mapping runs in parallel, and the results are copied in their order into one + // array of their total length. A default flat list from the mapping throws NullReferenceException there, as a + // null array does in Array.Parallel.collect. + let results = Array.Parallel.map mapping (unwrapArray (nameof array) array) + let mutable length = 0 + + for result in results do + length <- length + result.Length + + let builder = ImmutableArray.CreateBuilder<'U> length + + for result in results do + builder.AddRange result + + builder.MoveToImmutable () + + [] + let filter predicate (array : FlatList<'T>) = + Array.Parallel.filter predicate (unwrapArray (nameof array) array) + |> wrapArray + + [] + let map (mapping : 'T -> 'U) (array : FlatList<'T>) = + Array.Parallel.map mapping (unwrapArray (nameof array) array) + |> wrapArray + + [] + let mapi (mapping : int -> 'T -> 'U) (array : FlatList<'T>) = + Array.Parallel.mapi mapping (unwrapArray (nameof array) array) + |> wrapArray + + [] + let zip (array1 : FlatList<'T1>) (array2 : FlatList<'T2>) = + let source1 = unwrapArray (nameof array1) array1 + let source2 = unwrapArray (nameof array2) array2 + ArrayChecks.sameLengths (nameof array1) source1 (nameof array2) source2 + ParallelArrays.zip structPair source1 source2 |> wrapArray + + // ---- Folding ---- + + // The inline functions can use public members only, so they check and unwrap the array themselves + [] + let inline average (array : FlatList< ^T >) : ^T = + if array.IsDefault then + nullArg (nameof array) + + Array.Parallel.average ( + ImmutableCollectionsMarshal.AsArray array + |> Unchecked.nonNull + ) + + [] + let inline averageBy ([] projection : 'T -> ^U) (array : FlatList<'T>) : ^U = + if array.IsDefault then + nullArg (nameof array) + + Array.Parallel.averageBy + projection + (ImmutableCollectionsMarshal.AsArray array + |> Unchecked.nonNull) + + [] + let iter action (array : FlatList<'T>) = Array.Parallel.iter action (unwrapArray (nameof array) array) + + [] + let iteri action (array : FlatList<'T>) = Array.Parallel.iteri action (unwrapArray (nameof array) array) + + [] + let max (array : FlatList<'T>) = Array.Parallel.max (unwrapArray (nameof array) array) + + [] + let maxBy (projection : 'T -> 'U) (array : FlatList<'T>) = + Array.Parallel.maxBy projection (unwrapArray (nameof array) array) + + [] + let min (array : FlatList<'T>) = Array.Parallel.min (unwrapArray (nameof array) array) + + [] + let minBy (projection : 'T -> 'U) (array : FlatList<'T>) = + Array.Parallel.minBy projection (unwrapArray (nameof array) array) + + [] + let reduce reduction (array : FlatList<'T>) = Array.Parallel.reduce reduction (unwrapArray (nameof array) array) + + [] + let reduceBy (projection : 'T -> 'U) reduction (array : FlatList<'T>) = + Array.Parallel.reduceBy projection reduction (unwrapArray (nameof array) array) + + // Array.Parallel.sum dereferences a null array without a check; a default flat list throws ArgumentNullException, + // which its documentation promises + [] + let inline sum (array : FlatList< ^T >) : ^T = + if array.IsDefault then + nullArg (nameof array) + + Array.Parallel.sum ( + ImmutableCollectionsMarshal.AsArray array + |> Unchecked.nonNull + ) + + [] + let inline sumBy ([] projection : 'T -> ^U) (array : FlatList<'T>) : ^U = + if array.IsDefault then + nullArg (nameof array) + + Array.Parallel.sumBy + projection + (ImmutableCollectionsMarshal.AsArray array + |> Unchecked.nonNull) + + // ---- Partitioning and grouping ---- + + [] + let groupBy (projection : 'T -> 'Key) (array : FlatList<'T>) = + ParallelArrays.groupBy (fun key items -> struct (key, wrapArray items)) projection (unwrapArray (nameof array) array) + |> wrapArray + + [] + let partition predicate (array : FlatList<'T>) = + let accepted, rejected = Array.Parallel.partition predicate (unwrapArray (nameof array) array) + struct (wrapArray accepted, wrapArray rejected) + + [] + let partitionWith (partitioner : 'T -> Choice<'T1, 'T2>) (array : FlatList<'T>) = + let first, second = Array.Parallel.partitionWith partitioner (unwrapArray (nameof array) array) + struct (wrapArray first, wrapArray second) + + // ---- Sorting ---- + + [] + let sort (array : FlatList<'T>) = + Array.Parallel.sort (unwrapArray (nameof array) array) + |> wrapArray + + [] + let sortBy (projection : 'T -> 'Key) (array : FlatList<'T>) = + Array.Parallel.sortBy projection (unwrapArray (nameof array) array) + |> wrapArray + + [] + let sortByDescending (projection : 'T -> 'Key) (array : FlatList<'T>) = + Array.Parallel.sortByDescending projection (unwrapArray (nameof array) array) + |> wrapArray + + [] + let sortDescending (array : FlatList<'T>) = + Array.Parallel.sortDescending (unwrapArray (nameof array) array) + |> wrapArray + + [] + let sortWith comparer (array : FlatList<'T>) = + Array.Parallel.sortWith comparer (unwrapArray (nameof array) array) + |> wrapArray diff --git a/src/FSharp.Collections.Immutable/FlatList/FlatList.fsi b/src/FSharp.Collections.Immutable/FlatList/FlatList.fsi index 72a7904f..9987473b 100644 --- a/src/FSharp.Collections.Immutable/FlatList/FlatList.fsi +++ b/src/FSharp.Collections.Immutable/FlatList/FlatList.fsi @@ -2073,3 +2073,346 @@ val isDefault : array : FlatList<'T> -> bool /// [] val isDefaultOrEmpty : array : FlatList<'T> -> bool + +/// Functions that process the elements of a flat list in parallel. +/// +/// +/// The module mirrors : each function has the semantics, +/// the argument checks and the exceptions of the function with the same name in that module (ADR 0001, D11). The functions +/// read the underlying array without a copy and never change it; the in-place sorts of that module have no counterpart. +/// +/// +/// The functions call their function arguments on several threads at once and in an unspecified order, so the function +/// arguments must be thread-safe. An exception that a function argument throws can reach the caller wrapped in an +/// . +/// +/// +/// A default immutable array throws , as +/// does for a array. +/// +/// +/// Functions that return an optional value return a , and +/// functions that return tuples return struct tuples. The namespaces +/// and contain a module with the same name whose functions use options +/// and reference tuples instead. +/// +/// +[] +module Parallel = + + // ---- Creation ---- + + /// Creates a flat list from a function of the index, in parallel. + /// The length of the flat list. + /// The function that computes the element at each index, called in parallel. + /// The created flat list. + /// Thrown when is negative. + /// + /// O(n). A negative count throws , as in + /// , + /// where the sequential function throws . + /// + [] + val init : count : int -> initializer : (int -> 'T) -> FlatList<'T> + + // ---- Search ---- + + /// Returns whether any element satisfies the predicate. + /// The function to test the elements, called in parallel. + /// The input flat list. + /// when an element satisfies the predicate. + /// + /// O(n). An element that satisfies the predicate stops the tests of the other elements as soon as possible. + /// + [] + val exists : predicate : ('T -> bool) -> array : FlatList<'T> -> bool + + /// Returns whether all elements satisfy the predicate. + /// The function to test the elements, called in parallel. + /// The input flat list. + /// when every element satisfies the predicate. + /// + /// O(n). An element that does not satisfy the predicate stops the tests of the other elements as soon as possible. + /// + [] + val forall : predicate : ('T -> bool) -> array : FlatList<'T> -> bool + + /// Returns the first element that satisfies the predicate, or ValueNone. + /// The function to test the elements, called in parallel. + /// The input flat list. + /// The first element that satisfies the predicate, or ValueNone. + /// O(n). The predicate can also run for elements after the first one that satisfies it. + [] + val tryFind : predicate : ('T -> bool) -> array : FlatList<'T> -> 'T voption + + /// Returns the index of the first element that satisfies the predicate, or ValueNone. + /// The function to test the elements, called in parallel. + /// The input flat list. + /// The index of the first element that satisfies the predicate, or ValueNone. + /// O(n). The predicate can also run for elements after the first one that satisfies it. + [] + val tryFindIndex : predicate : ('T -> bool) -> array : FlatList<'T> -> int voption + + /// Returns the first value that the chooser returns for the elements, or ValueNone. + /// The function that returns a value, or ValueNone to go on, called in parallel. + /// The input flat list. + /// The value for the first element that the chooser returns a value for, or ValueNone. + /// O(n). The chooser can also run for elements after the first one that it returns a value for. + [] + val tryPick : chooser : ('T -> 'U voption) -> array : FlatList<'T> -> 'U voption + + // ---- Transformation ---- + + /// Returns the values that the chooser returns for the elements. + /// + /// The function that returns a value, or ValueNone to skip the element, called in parallel. + /// + /// The input flat list. + /// The chosen values, in the order of their elements. + /// O(n). + [] + val choose : chooser : ('T -> 'U voption) -> array : FlatList<'T> -> FlatList<'U> + + /// Concatenates the flat lists that a function returns for the elements. + /// The function that returns a flat list for each element, called in parallel. + /// The input flat list. + /// The concatenation of the returned flat lists, in the order of the elements. + /// O(n + the total length of the returned flat lists). + [] + val collect : mapping : ('T -> FlatList<'U>) -> array : FlatList<'T> -> FlatList<'U> + + /// Returns the elements that satisfy the predicate. + /// The function to test the elements, called in parallel. + /// The input flat list. + /// The elements that satisfy the predicate, in their order. + /// O(n). + [] + val filter : predicate : ('T -> bool) -> array : FlatList<'T> -> FlatList<'T> + + /// Returns the results of a function applied to each element. + /// The function to transform the elements, called in parallel. + /// The input flat list. + /// The results, in the order of the elements. + /// O(n). + [] + val map : mapping : ('T -> 'U) -> array : FlatList<'T> -> FlatList<'U> + + /// Returns the results of a function applied to each element and its index. + /// The function to transform the elements, called in parallel. + /// The input flat list. + /// The results, in the order of the elements. + /// O(n). + [] + val mapi : mapping : (int -> 'T -> 'U) -> array : FlatList<'T> -> FlatList<'U> + + /// Combines two flat lists into a flat list of pairs, in parallel. + /// The first flat list. + /// The second flat list. + /// The pairs of elements at the same index. + /// Thrown when the flat lists have different lengths. + /// O(n). + [] + val zip : array1 : FlatList<'T1> -> array2 : FlatList<'T2> -> FlatList + + // ---- Folding ---- + + /// Returns the average of the elements, which are added in parallel. + /// The input flat list. + /// The average. + /// Thrown when the flat list is empty. + /// + /// O(n). The elements are added in chunks, so a floating-point average can differ in its last digits from the + /// sequential one. + /// + [] + val inline average : + array : FlatList< ^T > -> ^T + when ^T : (static member (+) : ^T * ^T -> ^T) and ^T : (static member DivideByInt : ^T * int -> ^T) + + /// Returns the average of the results of a function applied to each element. + /// The function to transform the elements, called in parallel. + /// The input flat list. + /// The average of the results. + /// Thrown when the flat list is empty. + /// + /// O(n). The results are added in chunks, so a floating-point average can differ in its last digits from the + /// sequential one. + /// + [] + val inline averageBy : + projection : ('T -> ^U) -> array : FlatList<'T> -> ^U + when ^U : (static member (+) : ^U * ^U -> ^U) and ^U : (static member DivideByInt : ^U * int -> ^U) + + /// Applies an action to each element, in parallel. + /// The action, called in parallel. + /// The input flat list. + /// O(n). + [] + val iter : action : ('T -> unit) -> array : FlatList<'T> -> unit + + /// Applies an action to each element and its index, in parallel. + /// The action, called in parallel. + /// The input flat list. + /// O(n). + [] + val iteri : action : (int -> 'T -> unit) -> array : FlatList<'T> -> unit + + /// Returns the greatest element, in F# generic comparison. + /// The input flat list. + /// The greatest element. + /// Thrown when the flat list is empty. + /// O(n). The elements are compared in parallel chunks. + [] + val max : array : FlatList<'T> -> 'T when 'T : comparison + + /// Returns an element with the greatest result of a projection. + /// The function to transform the elements for the comparison, called in parallel. + /// The input flat list. + /// An element with the greatest result; which one of several such elements is not specified. + /// Thrown when the flat list is empty. + /// O(n). + [] + val maxBy : projection : ('T -> 'U) -> array : FlatList<'T> -> 'T when 'U : comparison + + /// Returns the smallest element, in F# generic comparison. + /// The input flat list. + /// The smallest element. + /// Thrown when the flat list is empty. + /// O(n). The elements are compared in parallel chunks. + [] + val min : array : FlatList<'T> -> 'T when 'T : comparison + + /// Returns an element with the smallest result of a projection. + /// The function to transform the elements for the comparison, called in parallel. + /// The input flat list. + /// An element with the smallest result; which one of several such elements is not specified. + /// Thrown when the flat list is empty. + /// O(n). + [] + val minBy : projection : ('T -> 'U) -> array : FlatList<'T> -> 'T when 'U : comparison + + /// Combines the elements with a function, in parallel. + /// + /// The function that combines two values. It combines the elements in parallel chunks and then the results of the + /// chunks, so it must be associative and commutative for a result that does not depend on the chunks. + /// + /// The input flat list. + /// The combined value. + /// Thrown when the flat list is empty. + /// O(n). + [] + val reduce : reduction : ('T -> 'T -> 'T) -> array : FlatList<'T> -> 'T + + /// Combines the results of a function applied to each element with another function, in parallel. + /// The function to transform the elements, called in parallel. + /// + /// The function that combines two results. It combines the results in parallel chunks and then the results of the + /// chunks, so it must be associative and commutative for a result that does not depend on the chunks. + /// + /// The input flat list. + /// The combined value. + /// Thrown when the flat list is empty. + /// O(n). The sequential module has no counterpart: the value is the reduction of the mapped elements. + [] + val reduceBy : projection : ('T -> 'U) -> reduction : ('U -> 'U -> 'U) -> array : FlatList<'T> -> 'U + + /// Returns the sum of the elements, which are added in parallel. + /// The input flat list. + /// The sum; zero for an empty flat list. + /// + /// O(n). The elements are added in chunks, so a floating-point sum can differ in its last digits from the sequential + /// one. + /// + [] + val inline sum : + array : FlatList< ^T > -> ^T when ^T : (static member (+) : ^T * ^T -> ^T) and ^T : (static member Zero : ^T) + + /// Returns the sum of the results of a function applied to each element. + /// The function to transform the elements, called in parallel. + /// The input flat list. + /// The sum of the results; zero for an empty flat list. + /// + /// O(n). The results are added in chunks, so a floating-point sum can differ in its last digits from the sequential + /// one. + /// + [] + val inline sumBy : + projection : ('T -> ^U) -> array : FlatList<'T> -> ^U + when ^U : (static member (+) : ^U * ^U -> ^U) and ^U : (static member Zero : ^U) + + // ---- Partitioning and grouping ---- + + /// Groups the elements by the key of a projection. + /// The function that returns the key of an element, called in parallel. + /// The input flat list. + /// + /// The keys with their elements. The order of the keys and the order of the elements of a key are not specified. + /// + /// O(n). The keys compare in F# structural equality. + [] + val groupBy : + projection : ('T -> 'Key) -> array : FlatList<'T> -> FlatList)> when 'Key : equality + + /// Splits the elements by a predicate. + /// The function to test the elements, called in parallel. + /// The input flat list. + /// The elements that satisfy the predicate and the other ones, both in their order. + /// O(n). + [] + val partition : predicate : ('T -> bool) -> array : FlatList<'T> -> struct (FlatList<'T> * FlatList<'T>) + + /// Splits the elements by the choice that a function returns for them. + /// + /// The function that returns Choice1Of2 or Choice2Of2 for an element, called in parallel. + /// + /// The input flat list. + /// The values of the first choices and of the second choices, both in the order of the elements. + /// O(n). + [] + val partitionWith : + partitioner : ('T -> Choice<'T1, 'T2>) -> array : FlatList<'T> -> struct (FlatList<'T1> * FlatList<'T2>) + + // ---- Sorting ---- + + /// Sorts the elements in parallel, in the order of F# generic comparison. + /// The input flat list. + /// The sorted flat list. + /// + /// O(n log n). This is not a stable sort, as + /// . + /// + [] + val sort : array : FlatList<'T> -> FlatList<'T> when 'T : comparison + + /// + /// Sorts the elements in parallel by the result of a projection, in the order of F# generic comparison. + /// + /// The function that returns the key of an element, called in parallel. + /// The input flat list. + /// The sorted flat list. + /// O(n log n). This is not a stable sort. + [] + val sortBy : projection : ('T -> 'Key) -> array : FlatList<'T> -> FlatList<'T> when 'Key : comparison + + /// Sorts the elements in parallel by the result of a projection, in descending order. + /// The function that returns the key of an element, called in parallel. + /// The input flat list. + /// The sorted flat list. + /// O(n log n). This is not a stable sort. + [] + val sortByDescending : projection : ('T -> 'Key) -> array : FlatList<'T> -> FlatList<'T> when 'Key : comparison + + /// Sorts the elements in parallel, in descending order of F# generic comparison. + /// The input flat list. + /// The sorted flat list. + /// O(n log n). This is not a stable sort. + [] + val sortDescending : array : FlatList<'T> -> FlatList<'T> when 'T : comparison + + /// Sorts the elements in parallel with a comparison function. + /// The function that compares two elements, called in parallel. + /// The input flat list. + /// The sorted flat list. + /// O(n log n). This is not a stable sort. + [] + val sortWith : comparer : ('T -> 'T -> int) -> array : FlatList<'T> -> FlatList<'T> diff --git a/src/FSharp.Collections.Immutable/FlatList/FlatListOption.fs b/src/FSharp.Collections.Immutable/FlatList/FlatListOption.fs index b510cf8b..2ed305e5 100644 --- a/src/FSharp.Collections.Immutable/FlatList/FlatListOption.fs +++ b/src/FSharp.Collections.Immutable/FlatList/FlatListOption.fs @@ -56,3 +56,27 @@ let tryPick (chooser : 'T -> 'U option) (array : FlatList<'T>) = Array.tryPick c let choose (chooser : 'T -> 'U option) (array : FlatList<'T>) = Array.choose chooser (unwrapArray (nameof array) array) |> wrapArray + +// The functions call the Array.Parallel functions, which take and return options, over the underlying array +[] +module Parallel = + + // ---- Search ---- + + [] + let tryFind predicate (array : FlatList<'T>) = Array.Parallel.tryFind predicate (unwrapArray (nameof array) array) + + [] + let tryFindIndex predicate (array : FlatList<'T>) = + Array.Parallel.tryFindIndex predicate (unwrapArray (nameof array) array) + + [] + let tryPick (chooser : 'T -> 'U option) (array : FlatList<'T>) = + Array.Parallel.tryPick chooser (unwrapArray (nameof array) array) + + // ---- Transformation ---- + + [] + let choose (chooser : 'T -> 'U option) (array : FlatList<'T>) = + Array.Parallel.choose chooser (unwrapArray (nameof array) array) + |> wrapArray diff --git a/src/FSharp.Collections.Immutable/FlatList/FlatListOption.fsi b/src/FSharp.Collections.Immutable/FlatList/FlatListOption.fsi index 33a1a000..d81c4522 100644 --- a/src/FSharp.Collections.Immutable/FlatList/FlatListOption.fsi +++ b/src/FSharp.Collections.Immutable/FlatList/FlatListOption.fsi @@ -173,3 +173,54 @@ val tryPick : chooser : ('T -> 'U option) -> array : FlatList<'T> -> 'U option /// [] val choose : chooser : ('T -> 'U option) -> array : FlatList<'T> -> FlatList<'U> + +/// +/// The functions of that take or return an optional +/// value, with instead of +/// . +/// +/// +/// The other functions of the default module stay available after open FSharp.Collections.Immutable.Option. Each +/// function has the signature of the function with the same name in +/// . +/// +[] +module Parallel = + + // ---- Search ---- + + /// Returns the first element that satisfies the predicate, or None. + /// The function to test the elements, called in parallel. + /// The input flat list. + /// The first element that satisfies the predicate, or None. + /// O(n). The predicate can also run for elements after the first one that satisfies it. + [] + val tryFind : predicate : ('T -> bool) -> array : FlatList<'T> -> 'T option + + /// Returns the index of the first element that satisfies the predicate, or None. + /// The function to test the elements, called in parallel. + /// The input flat list. + /// The index of the first element that satisfies the predicate, or None. + /// O(n). The predicate can also run for elements after the first one that satisfies it. + [] + val tryFindIndex : predicate : ('T -> bool) -> array : FlatList<'T> -> int option + + /// Returns the first value that the chooser returns for the elements, or None. + /// The function that returns a value, or None to go on, called in parallel. + /// The input flat list. + /// The value for the first element that the chooser returns a value for, or None. + /// O(n). The chooser can also run for elements after the first one that it returns a value for. + [] + val tryPick : chooser : ('T -> 'U option) -> array : FlatList<'T> -> 'U option + + // ---- Transformation ---- + + /// Returns the values that the chooser returns for the elements. + /// + /// The function that returns a value, or None to skip the element, called in parallel. + /// + /// The input flat list. + /// The chosen values, in the order of their elements. + /// O(n). + [] + val choose : chooser : ('T -> 'U option) -> array : FlatList<'T> -> FlatList<'U> diff --git a/src/FSharp.Collections.Immutable/FlatList/FlatListTuple.fs b/src/FSharp.Collections.Immutable/FlatList/FlatListTuple.fs index 7425d044..3cc458b3 100644 --- a/src/FSharp.Collections.Immutable/FlatList/FlatListTuple.fs +++ b/src/FSharp.Collections.Immutable/FlatList/FlatListTuple.fs @@ -110,3 +110,40 @@ let partitionWith (partitioner : 'T -> Choice<'T1, 'T2>) (array : FlatList<'T>) let splitAt index (array : FlatList<'T>) = let struct (before, after) = FSharp.Collections.Immutable.FlatList.splitAt index array before, after + +// As in the module above: the functions that build a tuple per element share their algorithm with the default module, +// and the others delegate to it and repack the tuples +[] +module Parallel = + + // ---- Transformation ---- + + [] + let zip (array1 : FlatList<'T1>) (array2 : FlatList<'T2>) = + let source1 = unwrapArray (nameof array1) array1 + let source2 = unwrapArray (nameof array2) array2 + ArrayChecks.sameLengths (nameof array1) source1 (nameof array2) source2 + + ParallelArrays.zip referencePair source1 source2 + |> wrapArray + + // ---- Partitioning and grouping ---- + + [] + let groupBy (projection : 'T -> 'Key) (array : FlatList<'T>) = + ParallelArrays.groupBy (fun key items -> key, wrapArray items) projection (unwrapArray (nameof array) array) + |> wrapArray + + [] + let partition predicate (array : FlatList<'T>) = + let struct (accepted, rejected) = + FSharp.Collections.Immutable.FlatList.Parallel.partition predicate array + + accepted, rejected + + [] + let partitionWith (partitioner : 'T -> Choice<'T1, 'T2>) (array : FlatList<'T>) = + let struct (first, second) = + FSharp.Collections.Immutable.FlatList.Parallel.partitionWith partitioner array + + first, second diff --git a/src/FSharp.Collections.Immutable/FlatList/FlatListTuple.fsi b/src/FSharp.Collections.Immutable/FlatList/FlatListTuple.fsi index 6000cc94..d1b516a5 100644 --- a/src/FSharp.Collections.Immutable/FlatList/FlatListTuple.fsi +++ b/src/FSharp.Collections.Immutable/FlatList/FlatListTuple.fsi @@ -169,3 +169,56 @@ val partitionWith : partitioner : ('T -> Choice<'T1, 'T2>) -> array : FlatList<' /// [] val splitAt : index : int -> array : FlatList<'T> -> FlatList<'T> * FlatList<'T> + +/// +/// The functions of that take or return tuples, with +/// reference tuples instead of struct tuples. +/// +/// +/// The other functions of the default module stay available after open FSharp.Collections.Immutable.Tuple. Each +/// function has the signature of the function with the same name in +/// . +/// +[] +module Parallel = + + // ---- Transformation ---- + + /// Combines two flat lists into a flat list of pairs, in parallel. + /// The first flat list. + /// The second flat list. + /// The pairs of elements at the same index. + /// Thrown when the flat lists have different lengths. + /// O(n). + [] + val zip : array1 : FlatList<'T1> -> array2 : FlatList<'T2> -> FlatList<'T1 * 'T2> + + // ---- Partitioning and grouping ---- + + /// Groups the elements by the key of a projection. + /// The function that returns the key of an element, called in parallel. + /// The input flat list. + /// + /// The keys with their elements. The order of the keys and the order of the elements of a key are not specified. + /// + /// O(n). The keys compare in F# structural equality. + [] + val groupBy : projection : ('T -> 'Key) -> array : FlatList<'T> -> FlatList<'Key * FlatList<'T>> when 'Key : equality + + /// Splits the elements by a predicate. + /// The function to test the elements, called in parallel. + /// The input flat list. + /// The elements that satisfy the predicate and the other ones, both in their order. + /// O(n). + [] + val partition : predicate : ('T -> bool) -> array : FlatList<'T> -> FlatList<'T> * FlatList<'T> + + /// Splits the elements by the choice that a function returns for them. + /// + /// The function that returns Choice1Of2 or Choice2Of2 for an element, called in parallel. + /// + /// The input flat list. + /// The values of the first choices and of the second choices, both in the order of the elements. + /// O(n). + [] + val partitionWith : partitioner : ('T -> Choice<'T1, 'T2>) -> array : FlatList<'T> -> FlatList<'T1> * FlatList<'T2> diff --git a/src/FSharp.Collections.Immutable/Helper/ParallelArrays.fs b/src/FSharp.Collections.Immutable/Helper/ParallelArrays.fs new file mode 100644 index 00000000..b056e8ea --- /dev/null +++ b/src/FSharp.Collections.Immutable/Helper/ParallelArrays.fs @@ -0,0 +1,175 @@ +/// +/// Parallel algorithms over arrays for the functions of +/// that take or return options or reference tuples (ADR 0001, D3 and D11). +/// +/// +/// The algorithms take value options and tuple constructors instead, so that the default modules and the Tuple modules +/// share them and nothing is allocated per element. The callers check their arguments first, with the exceptions of +/// . +/// +module internal FSharp.Collections.Immutable.ParallelArrays + +open System +open System.Linq +open System.Threading.Tasks + +/// The length of the chunks of a chunked parallel loop over elements. +/// +/// The chunks are those of the chunked loops of : one chunk +/// per processor, and a single chunk for fewer than 256 elements, which are not worth a second thread. +/// +let chunkLength (length : int) = + let processors = Environment.ProcessorCount + + if length < 256 then length + elif length % processors = 0 then length / processors + else length / processors + 1 + +/// Runs an action on the chunks of the indices from 0 to in parallel. +/// +/// The action takes the first index of its chunk and the index after its last one. A loop per chunk calls no delegate per +/// element. +/// +let inline forEachChunk (length : int) ([] action : int -> int -> unit) = + let chunk = chunkLength length + let chunks = if chunk = 0 then 0 else (length + chunk - 1) / chunk + + Parallel.For ( + 0, + chunks, + fun index -> + let first = index * chunk + action first (min length (first + chunk)) + ) + |> ignore + +/// The values that the chooser returns for the elements, in the order of the elements. +/// +/// The chooser runs in parallel into an array of value options, which a sequential pass compacts. Calling +/// +/// instead would convert every value option to an option. +/// +let choose (chooser : 'T -> 'U voption) (array : 'T array) : 'U array = + let chosen = Array.Parallel.map chooser array + let mutable count = 0 + + for value in chosen do + if value.IsSome then + count <- count + 1 + + let result = Array.zeroCreate count + let mutable index = 0 + + for value in chosen do + match value with + | ValueSome item -> + result[index] <- item + index <- index + 1 + | ValueNone -> () + + result + +/// The keys of the projection, each with its elements; the order of both is not specified. +/// +/// The projection, the work of the caller, runs in parallel. The grouping then runs over the computed keys with +/// , +/// as the sequential grouping of the Arrays module does, so that the keys compare in F# structural equality and a +/// key is a key like any other. The result has the order of +/// , +/// one of the orders that +/// +/// allows. +/// +let inline groupBy + ([] tuple : 'Key -> 'T array -> 'R) + (projection : 'T -> 'Key) + (array : 'T array) + : 'R array when 'Key : equality = + let keys = Array.Parallel.map projection array + + let lookup = + Enumerable + .Range(0, array.Length) + .ToLookup((fun index -> keys[index]), (fun index -> array[index]), 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 + +/// The index of the first element that satisfies the predicate, or ValueNone. +/// +/// The predicate runs in parallel. A match breaks the loop, which then starts only the elements before it, and the lowest +/// index that broke the loop wins, as in +/// . +/// +let tryFindIndex (predicate : 'T -> bool) (array : 'T array) : int voption = + let result = + Parallel.For ( + 0, + array.Length, + fun index (state : ParallelLoopState) -> + if predicate array[index] then + state.Break () + ) + + let lowest = result.LowestBreakIteration + + if lowest.HasValue then + ValueSome (int lowest.Value) + else + ValueNone + +/// The value that the chooser returns for the first element with a value, or ValueNone. +/// +/// The chooser runs in parallel. A value breaks the loop, which then starts only the elements before it, and the value of +/// the lowest index wins. The function of FSharp.Core, +/// , +/// keeps every value in a concurrent dictionary by index; here a lock guards the value of the lowest index so far, and +/// only the elements that have a value take it. +/// +let tryPick (chooser : 'T -> 'U voption) (array : 'T array) : 'U voption = + let gate = obj () + let lowest = ref Int32.MaxValue + let chosen = ref ValueNone + + Parallel.For ( + 0, + array.Length, + fun index (state : ParallelLoopState) -> + match chooser array[index] with + | ValueSome _ as value -> + lock + gate + (fun () -> + if index < lowest.Value then + lowest.Value <- index + chosen.Value <- value + ) + + state.Break () + | ValueNone -> () + ) + |> ignore + + chosen.Value + +/// Pairs the elements of two arrays of the same length in parallel. +/// +/// The chunks are those of . +/// +let inline zip ([] tuple : 'T1 -> 'T2 -> 'R) (array1 : 'T1 array) (array2 : 'T2 array) : 'R array = + let result = Array.zeroCreate array1.Length + + forEachChunk + result.Length + (fun first last -> + for index = first to last - 1 do + result[index] <- tuple array1[index] array2[index] + ) + + result From 3182c12d89de7497f32a7b68a7725e5bfdf82767 Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Sun, 18 Oct 2026 12:45:41 +0200 Subject: [PATCH 02/14] test: `FlatList.Parallel` against the sequential functions and `Array.Parallel` - Every function of `FlatList.Parallel` runs on the inputs of the contracts and on 10 000 scrambled elements, enough to split the work among threads, and is compared with its sequential `FlatList` counterpart. `groupBy`, `iter` and `iteri`, whose order is open, are compared after sorting, and `reduceBy`, which has no sequential counterpart, with `Array.Parallel.reduceBy`. - Exceptions for empty, mismatched and default inputs are compared with those of `Array.Parallel`, and so is the choice of `maxBy` and `minBy` among equal keys. - The tests of the `Option` and `Tuple` namespaces prove that their nested `Parallel` modules shadow the default functions and fall through to them otherwise. Co-Authored-By: Claude Opus 5.5 --- .../FSharp.Collections.Immutable.Tests.fsproj | 3 + .../FlatList/FlatListParallelOptionTests.fs | 115 +++ .../FlatList/FlatListParallelTests.fs | 827 ++++++++++++++++++ .../FlatList/FlatListParallelTupleTests.fs | 110 +++ 4 files changed, 1055 insertions(+) create mode 100644 tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelOptionTests.fs create mode 100644 tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelTests.fs create mode 100644 tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelTupleTests.fs diff --git a/tests/FSharp.Collections.Immutable.Tests/FSharp.Collections.Immutable.Tests.fsproj b/tests/FSharp.Collections.Immutable.Tests/FSharp.Collections.Immutable.Tests.fsproj index be217625..42889292 100644 --- a/tests/FSharp.Collections.Immutable.Tests/FSharp.Collections.Immutable.Tests.fsproj +++ b/tests/FSharp.Collections.Immutable.Tests/FSharp.Collections.Immutable.Tests.fsproj @@ -58,6 +58,9 @@ + + + diff --git a/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelOptionTests.fs b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelOptionTests.fs new file mode 100644 index 00000000..35f6a887 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelOptionTests.fs @@ -0,0 +1,115 @@ +namespace FSharp.Collections.Immutable.Tests + +open FSharp.Collections.Immutable +open FSharp.Collections.Immutable.Tuple +open FSharp.Collections.Immutable.Option +open Microsoft.VisualStudio.TestTools.UnitTesting + +// The namespaces are opened in the order that gives the shapes of FSharp.Core: Tuple first, Option last. +// The type annotations of the results are the proof that the functions of the Option namespace shadow the default ones, +// also in the nested Parallel module. + +/// The `FlatList.Parallel` module of the `Option` namespace. +[] +type FlatListParallelOptionTests () = + + static let array = FlatList.ofList Inputs.several + + /// A flat list long enough for the functions to split their work among several threads. + static let large = FlatList.init 10_000 (fun index -> index % 1_000 - 500) + + static let defaultArray = Unchecked.defaultof> + + static let nullArray = Unchecked.defaultof + + static let isAbove (limit : int) (item : int) = item > limit + + static let negatedIfNegative (item : int) = if item < 0 then Some (-item) else None + + [] + member _.``tryFind returns an option`` () = + for source in [ array; large ] do + let found : int option = FlatList.Parallel.tryFind (isAbove 6) source + + OverrideAssert.optionMatches + (FSharp.Collections.Immutable.FlatList.Parallel.tryFind (isAbove 6) source) + found + $"Option.FlatList.Parallel.tryFind over %d{source.Length} elements must match the default" + + [] + member _.``tryFindIndex returns an option`` () = + for source in [ array; large ] do + let found : int option = FlatList.Parallel.tryFindIndex (isAbove 6) source + + OverrideAssert.optionMatches + (FSharp.Collections.Immutable.FlatList.Parallel.tryFindIndex (isAbove 6) source) + found + $"Option.FlatList.Parallel.tryFindIndex over %d{source.Length} elements must match the default" + + [] + member _.``tryPick takes a chooser of options and returns an option`` () = + for source in [ array; large ] do + let found : int option = FlatList.Parallel.tryPick negatedIfNegative source + + Assert.AreEqual( + Array.tryPick negatedIfNegative (FlatList.toArray source), + found, + $"Option.FlatList.Parallel.tryPick over %d{source.Length} elements must pick as Array.tryPick does" + ) + + [] + member _.``choose takes a chooser of options`` () = + for source in [ array; large ] do + let chosen : FlatList = FlatList.Parallel.choose negatedIfNegative source + + Assert.AreEqual( + List.choose negatedIfNegative (FlatList.toList source), + FlatList.toList chosen, + $"Option.FlatList.Parallel.choose over %d{source.Length} elements must choose as List.choose does" + ) + + [] + member _.``the functions reject a default flat list as Array.Parallel rejects a null array`` () = + let cases : struct (string * (unit -> unit) * (unit -> unit)) list = [ + struct (nameof FlatList.Parallel.choose, + (fun () -> Array.Parallel.choose negatedIfNegative nullArray |> ignore), + (fun () -> + FlatList.Parallel.choose negatedIfNegative defaultArray + |> ignore + )) + struct (nameof FlatList.Parallel.tryFind, + (fun () -> Array.Parallel.tryFind (isAbove 6) nullArray |> ignore), + (fun () -> FlatList.Parallel.tryFind (isAbove 6) defaultArray |> ignore)) + struct (nameof FlatList.Parallel.tryFindIndex, + (fun () -> Array.Parallel.tryFindIndex (isAbove 6) nullArray |> ignore), + (fun () -> + FlatList.Parallel.tryFindIndex (isAbove 6) defaultArray + |> ignore + )) + struct (nameof FlatList.Parallel.tryPick, + (fun () -> Array.Parallel.tryPick negatedIfNegative nullArray |> ignore), + (fun () -> + FlatList.Parallel.tryPick negatedIfNegative defaultArray + |> ignore + )) + ] + + for struct (name, oracle, actual) in cases do + ExceptionAssert.same id oracle actual $"Option.FlatList.Parallel.%s{name} must reject a default flat list" + + [] + member _.``the functions without an option keep resolving to the default module`` () = + let mapped : FlatList = FlatList.Parallel.map (fun item -> item * 2) array + let zipped : FlatList = FlatList.Parallel.zip array array + + Assert.AreEqual( + List.map (fun item -> item * 2) Inputs.several, + FlatList.toList mapped, + "FlatList.Parallel.map must resolve to the default module" + ) + + Assert.AreEqual<(int * int) list>( + List.zip Inputs.several Inputs.several, + FlatList.toList zipped, + "FlatList.Parallel.zip must resolve to the Tuple module" + ) diff --git a/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelTests.fs b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelTests.fs new file mode 100644 index 00000000..210ab986 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelTests.fs @@ -0,0 +1,827 @@ +namespace FSharp.Collections.Immutable.Tests + +open System +open System.Collections.Concurrent +open FSharp.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// The `FlatList.Parallel` module, compared with the sequential functions of `FlatList` (ADR 0001, D11). +/// Every function runs on the inputs of the contracts and on a larger input, which is long enough for the functions to split +/// their work among several threads. Results whose order the module leaves open are compared after sorting, and exceptions +/// are compared with those of `Array.Parallel`, which the module mirrors. +[] +type FlatListParallelTests () = + + static let defaultArray = Unchecked.defaultof> + + static let nullArray = Unchecked.defaultof + + /// Numbers from -1000 to 999 in a scrambled order, each about five times. 7919 and 10 007 are primes, so the products + /// modulo 10 007 do not repeat before the last remainder folds them onto 2000 values. + static let large = List.init 10_000 (fun index -> index * 7_919 % 10_007 % 2_000 - 1_000) + + /// The inputs of the contracts and the larger input. + static let allInputs = [ yield! Inputs.all; large ] + + /// The inputs with at least one element. + static let nonEmptyInputs = [ yield! Inputs.nonEmpty; large ] + + /// Runs a check on the flat list of every given input, with a description of the input for the messages. + static let forEach (inputs : int list list) (check : FlatList -> string -> unit) = + for input in inputs do + let description = + if input.Length > Inputs.several.Length then + $"%d{input.Length} elements" + else + $"%A{input}" + + check (FlatList.ofList input) description + + /// The predicates of the tests: matches at the start, matches late and rare in the larger input, and no match. + static let predicates : (int -> bool) list = [ + (fun item -> item % 2 = 0) + (fun item -> item < 0) + (fun item -> item > 900) + (fun _ -> false) + ] + + /// The choosers of the tests, which return a value for the same elements as the predicates. + static let choosers : (int -> int voption) list = [ + (fun item -> if item % 2 = 0 then ValueSome (item / 2) else ValueNone) + (fun item -> if item < 0 then ValueSome (-item) else ValueNone) + (fun item -> if item > 900 then ValueSome (item * 2) else ValueNone) + (fun _ -> ValueNone) + ] + + /// Negation is injective, so only equal elements share a key, and the result of a sort does not depend on its stability. + static let negate (item : int) = -item + + /// The groups with their keys and the elements of every key sorted, for the comparison of groupings whose order is open. + static let normalized (groups : FlatList)>) = + groups + |> Seq.map (fun struct (key, items) -> struct (key, items |> Seq.sort |> Seq.toList)) + |> Seq.sortBy (fun struct (key, _) -> key) + |> Seq.toList + + // ---- Creation ---- + + [] + member _.``init creates the elements as the sequential init does`` () = + for count in [ 0; 1; Inputs.several.Length; large.Length ] do + Assert.AreEqual( + FlatList.toList (FlatList.init count (fun index -> index * 3)), + FlatList.toList (FlatList.Parallel.init count (fun index -> index * 3)), + $"FlatList.Parallel.init %d{count}" + ) + + [] + member _.``init throws for a negative count as Array.Parallel does`` () = + ExceptionAssert.same + id + (fun () -> Array.Parallel.init -1 id |> ignore) + (fun () -> FlatList.Parallel.init -1 id |> ignore) + "FlatList.Parallel.init -1 must throw as Array.Parallel.init does" + + // ---- Search ---- + + [] + member _.``exists answers as the sequential exists does`` () = + forEach + allInputs + (fun array description -> + for predicate in predicates do + Assert.AreEqual( + FlatList.exists predicate array, + FlatList.Parallel.exists predicate array, + $"FlatList.Parallel.exists over %s{description}" + ) + ) + + [] + member _.``forall answers as the sequential forall does`` () = + forEach + allInputs + (fun array description -> + for predicate in predicates do + let negated item = not (predicate item) + + for test in [ predicate; negated ] do + Assert.AreEqual( + FlatList.forall test array, + FlatList.Parallel.forall test array, + $"FlatList.Parallel.forall over %s{description}" + ) + ) + + [] + member _.``tryFind returns the first element that satisfies the predicate`` () = + forEach + allInputs + (fun array description -> + for predicate in predicates do + Assert.AreEqual( + FlatList.tryFind predicate array, + FlatList.Parallel.tryFind predicate array, + $"FlatList.Parallel.tryFind in %s{description}" + ) + ) + + [] + member _.``tryFindIndex returns the index of the first element that satisfies the predicate`` () = + forEach + allInputs + (fun array description -> + for predicate in predicates do + Assert.AreEqual( + FlatList.tryFindIndex predicate array, + FlatList.Parallel.tryFindIndex predicate array, + $"FlatList.Parallel.tryFindIndex in %s{description}" + ) + ) + + [] + member _.``tryPick returns the value for the first element that the chooser returns a value for`` () = + forEach + allInputs + (fun array description -> + for chooser in choosers do + Assert.AreEqual( + FlatList.tryPick chooser array, + FlatList.Parallel.tryPick chooser array, + $"FlatList.Parallel.tryPick in %s{description}" + ) + ) + + // ---- Transformation ---- + + [] + member _.``choose keeps the chosen values in the order of their elements`` () = + forEach + allInputs + (fun array description -> + for chooser in choosers do + Assert.AreEqual( + FlatList.toList (FlatList.choose chooser array), + FlatList.toList (FlatList.Parallel.choose chooser array), + $"FlatList.Parallel.choose of %s{description}" + ) + ) + + [] + member _.``collect concatenates the flat lists in the order of the elements`` () = + // The flat lists of the mapping have zero, one or two elements + let mapping (item : int) = FlatList.replicate (abs item % 3) item + + forEach + allInputs + (fun array description -> + Assert.AreEqual( + FlatList.toList (FlatList.collect mapping array), + FlatList.toList (FlatList.Parallel.collect mapping array), + $"FlatList.Parallel.collect of %s{description}" + ) + ) + + [] + member _.``collect throws for a default flat list of the mapping as Array.Parallel does for a null array`` () = + ExceptionAssert.same + id + (fun () -> + Array.Parallel.collect (fun _ -> nullArray) [| 1 |] + |> ignore + ) + (fun () -> + FlatList.Parallel.collect (fun _ -> defaultArray) (FlatList.singleton 1) + |> ignore + ) + "FlatList.Parallel.collect must throw for a default flat list of the mapping as Array.Parallel.collect does" + + [] + member _.``filter keeps the elements that satisfy the predicate in their order`` () = + forEach + allInputs + (fun array description -> + for predicate in predicates do + Assert.AreEqual( + FlatList.toList (FlatList.filter predicate array), + FlatList.toList (FlatList.Parallel.filter predicate array), + $"FlatList.Parallel.filter of %s{description}" + ) + ) + + [] + member _.``map transforms every element in its order`` () = + forEach + allInputs + (fun array description -> + Assert.AreEqual( + FlatList.toList (FlatList.map string array), + FlatList.toList (FlatList.Parallel.map string array), + $"FlatList.Parallel.map of %s{description}" + ) + ) + + [] + member _.``mapi transforms every element with its index`` () = + let mapping (index : int) (item : int) = struct (index, item * 3) + + forEach + allInputs + (fun array description -> + Assert.AreEqual( + FlatList.toList (FlatList.mapi mapping array), + FlatList.toList (FlatList.Parallel.mapi mapping array), + $"FlatList.Parallel.mapi of %s{description}" + ) + ) + + [] + member _.``zip pairs the elements at the same index`` () = + forEach + allInputs + (fun array description -> + let other = FlatList.rev array + + Assert.AreEqual( + FlatList.toList (FlatList.zip array other), + FlatList.toList (FlatList.Parallel.zip array other), + $"FlatList.Parallel.zip of %s{description}" + ) + ) + + [] + member _.``zip throws for different lengths as Array.Parallel does`` () = + let short = List.toArray Inputs.singleton + let long = List.toArray Inputs.several + + for struct (first, second) in [ struct (short, long); struct (long, short) ] do + ExceptionAssert.same + id + (fun () -> Array.Parallel.zip first second |> ignore) + (fun () -> + FlatList.Parallel.zip (FlatList.ofArray first) (FlatList.ofArray second) + |> ignore + ) + $"FlatList.Parallel.zip of %d{first.Length} and %d{second.Length} elements must throw as Array.Parallel.zip does" + + // ---- Folding ---- + + [] + member _.``average returns the average of the elements`` () = + // Whole numbers add up exactly in any order, so the parallel sum is the sequential one + forEach + nonEmptyInputs + (fun array description -> + let floats = FlatList.map float array + + Assert.AreEqual( + FlatList.average floats, + FlatList.Parallel.average floats, + $"FlatList.Parallel.average of %s{description}" + ) + ) + + [] + member _.``average throws for an empty flat list as Array.Parallel does`` () = + ExceptionAssert.same + id + (fun () -> Array.Parallel.average Array.empty |> ignore) + (fun () -> FlatList.Parallel.average FlatList.empty |> ignore) + "FlatList.Parallel.average of an empty flat list must throw as Array.Parallel.average does" + + [] + member _.``averageBy returns the average of the projections`` () = + forEach + nonEmptyInputs + (fun array description -> + Assert.AreEqual( + FlatList.averageBy float array, + FlatList.Parallel.averageBy float array, + $"FlatList.Parallel.averageBy of %s{description}" + ) + ) + + [] + member _.``averageBy throws for an empty flat list as Array.Parallel does`` () = + ExceptionAssert.same + id + (fun () -> Array.Parallel.averageBy float Array.empty |> ignore) + (fun () -> + FlatList.Parallel.averageBy float FlatList.empty + |> ignore + ) + "FlatList.Parallel.averageBy of an empty flat list must throw as Array.Parallel.averageBy does" + + [] + member _.``iter visits every element once`` () = + forEach + allInputs + (fun array description -> + let visited = ConcurrentQueue() + FlatList.Parallel.iter visited.Enqueue array + + Assert.AreEqual( + array |> Seq.sort |> Seq.toList, + visited |> Seq.sort |> Seq.toList, + $"FlatList.Parallel.iter over %s{description}" + ) + ) + + [] + member _.``iteri visits every element once with its index`` () = + forEach + allInputs + (fun array description -> + let visited = ConcurrentQueue() + FlatList.Parallel.iteri (fun index item -> visited.Enqueue (struct (index, item))) array + + Assert.AreEqual( + FlatList.toList (FlatList.indexed array), + visited + |> Seq.sortBy (fun struct (index, _) -> index) + |> Seq.toList, + $"FlatList.Parallel.iteri over %s{description}" + ) + ) + + [] + member _.``max returns the greatest element`` () = + forEach + nonEmptyInputs + (fun array description -> + Assert.AreEqual( + FlatList.max array, + FlatList.Parallel.max array, + $"FlatList.Parallel.max of %s{description}" + ) + ) + + [] + member _.``max throws for an empty flat list as Array.Parallel does`` () = + ExceptionAssert.same + id + (fun () -> Array.Parallel.max Array.empty |> ignore) + (fun () -> FlatList.Parallel.max FlatList.empty |> ignore) + "FlatList.Parallel.max of an empty flat list must throw as Array.Parallel.max does" + + [] + member _.``maxBy returns the element with the greatest projection`` () = + // In every input, only equal elements share the greatest absolute value, so any of them is the expected one + forEach + nonEmptyInputs + (fun array description -> + Assert.AreEqual( + FlatList.maxBy abs array, + FlatList.Parallel.maxBy abs array, + $"FlatList.Parallel.maxBy of %s{description}" + ) + ) + + [] + member _.``maxBy chooses among equal projections as Array.Parallel does`` () = + let key (struct (key : int, _ : char)) = key + + Assert.AreEqual( + Array.Parallel.maxBy key (List.toArray Inputs.pairs), + FlatList.Parallel.maxBy key (FlatList.ofList Inputs.pairs), + "FlatList.Parallel.maxBy must choose among equal keys as Array.Parallel.maxBy does" + ) + + [] + member _.``maxBy throws for an empty flat list as Array.Parallel does`` () = + ExceptionAssert.same + id + (fun () -> Array.Parallel.maxBy abs Array.empty |> ignore) + (fun () -> FlatList.Parallel.maxBy abs FlatList.empty |> ignore) + "FlatList.Parallel.maxBy of an empty flat list must throw as Array.Parallel.maxBy does" + + [] + member _.``min returns the smallest element`` () = + forEach + nonEmptyInputs + (fun array description -> + Assert.AreEqual( + FlatList.min array, + FlatList.Parallel.min array, + $"FlatList.Parallel.min of %s{description}" + ) + ) + + [] + member _.``min throws for an empty flat list as Array.Parallel does`` () = + ExceptionAssert.same + id + (fun () -> Array.Parallel.min Array.empty |> ignore) + (fun () -> FlatList.Parallel.min FlatList.empty |> ignore) + "FlatList.Parallel.min of an empty flat list must throw as Array.Parallel.min does" + + [] + member _.``minBy returns the element with the smallest projection`` () = + // In every input, only equal elements share the smallest absolute value, so any of them is the expected one + forEach + nonEmptyInputs + (fun array description -> + Assert.AreEqual( + FlatList.minBy abs array, + FlatList.Parallel.minBy abs array, + $"FlatList.Parallel.minBy of %s{description}" + ) + ) + + [] + member _.``minBy chooses among equal projections as Array.Parallel does`` () = + let key (struct (key : int, _ : char)) = key + + Assert.AreEqual( + Array.Parallel.minBy key (List.toArray Inputs.pairs), + FlatList.Parallel.minBy key (FlatList.ofList Inputs.pairs), + "FlatList.Parallel.minBy must choose among equal keys as Array.Parallel.minBy does" + ) + + [] + member _.``minBy throws for an empty flat list as Array.Parallel does`` () = + ExceptionAssert.same + id + (fun () -> Array.Parallel.minBy abs Array.empty |> ignore) + (fun () -> FlatList.Parallel.minBy abs FlatList.empty |> ignore) + "FlatList.Parallel.minBy of an empty flat list must throw as Array.Parallel.minBy does" + + [] + member _.``reduce combines the elements as the sequential reduce does for an associative and commutative function`` () = + forEach + nonEmptyInputs + (fun array description -> + Assert.AreEqual( + FlatList.reduce (+) array, + FlatList.Parallel.reduce (+) array, + $"FlatList.Parallel.reduce of %s{description}" + ) + ) + + [] + member _.``reduce throws for an empty flat list as Array.Parallel does`` () = + ExceptionAssert.same + id + (fun () -> Array.Parallel.reduce (+) Array.empty |> ignore) + (fun () -> FlatList.Parallel.reduce (+) FlatList.empty |> ignore) + "FlatList.Parallel.reduce of an empty flat list must throw as Array.Parallel.reduce does" + + [] + member _.``reduceBy combines the projections as Array.Parallel does`` () = + // The sequential module has no reduceBy; it is reduce after map there + forEach + nonEmptyInputs + (fun array description -> + let combined = FlatList.Parallel.reduceBy abs (+) array + + Assert.AreEqual( + Array.Parallel.reduceBy abs (+) (FlatList.toArray array), + combined, + $"FlatList.Parallel.reduceBy of %s{description} must combine as Array.Parallel.reduceBy does" + ) + + Assert.AreEqual( + FlatList.map abs array |> FlatList.reduce (+), + combined, + $"FlatList.Parallel.reduceBy of %s{description} must combine as the sequential reduce after map does" + ) + ) + + [] + member _.``reduceBy throws for an empty flat list as Array.Parallel does`` () = + ExceptionAssert.same + id + (fun () -> Array.Parallel.reduceBy abs (+) Array.empty |> ignore) + (fun () -> + FlatList.Parallel.reduceBy abs (+) FlatList.empty + |> ignore + ) + "FlatList.Parallel.reduceBy of an empty flat list must throw as Array.Parallel.reduceBy does" + + [] + member _.``sum adds the elements`` () = + forEach + allInputs + (fun array description -> + Assert.AreEqual( + FlatList.sum array, + FlatList.Parallel.sum array, + $"FlatList.Parallel.sum of %s{description}" + ) + ) + + [] + member _.``sumBy adds the projections`` () = + forEach + allInputs + (fun array description -> + Assert.AreEqual( + FlatList.sumBy abs array, + FlatList.Parallel.sumBy abs array, + $"FlatList.Parallel.sumBy of %s{description}" + ) + ) + + // ---- Partitioning and grouping ---- + + [] + member _.``groupBy groups the elements as the sequential groupBy does`` () = + forEach + allInputs + (fun array description -> + Assert.AreEqual( + normalized (FlatList.groupBy (fun item -> item % 3) array), + normalized (FlatList.Parallel.groupBy (fun item -> item % 3) array), + $"FlatList.Parallel.groupBy of %s{description}" + ) + ) + + [] + member _.``groupBy groups the elements of a null key`` () = + let projection (item : int) : string | null = if item < 0 then null else string (item % 3) + + forEach + allInputs + (fun array description -> + Assert.AreEqual( + normalized (FlatList.groupBy projection array), + normalized (FlatList.Parallel.groupBy projection array), + $"FlatList.Parallel.groupBy with null keys of %s{description}" + ) + ) + + [] + member _.``partition splits the elements in their order`` () = + forEach + allInputs + (fun array description -> + for predicate in predicates do + let struct (expectedAccepted, expectedRejected) = FlatList.partition predicate array + let struct (accepted, rejected) = FlatList.Parallel.partition predicate array + + Assert.AreEqual( + FlatList.toList expectedAccepted, + FlatList.toList accepted, + $"FlatList.Parallel.partition accepted of %s{description}" + ) + + Assert.AreEqual( + FlatList.toList expectedRejected, + FlatList.toList rejected, + $"FlatList.Parallel.partition rejected of %s{description}" + ) + ) + + [] + member _.``partitionWith splits the elements by the choice in their order`` () = + let partitioner (item : int) = + if item > 0 then + Choice1Of2 item + else + Choice2Of2 (string item) + + forEach + allInputs + (fun array description -> + let struct (expectedFirst, expectedSecond) = FlatList.partitionWith partitioner array + let struct (first, second) = FlatList.Parallel.partitionWith partitioner array + + Assert.AreEqual( + FlatList.toList expectedFirst, + FlatList.toList first, + $"FlatList.Parallel.partitionWith first choices of %s{description}" + ) + + Assert.AreEqual( + FlatList.toList expectedSecond, + FlatList.toList second, + $"FlatList.Parallel.partitionWith second choices of %s{description}" + ) + ) + + // ---- Sorting ---- + + [] + member _.``sort orders the elements ascending`` () = + forEach + allInputs + (fun array description -> + Assert.AreEqual( + FlatList.toList (FlatList.sort array), + FlatList.toList (FlatList.Parallel.sort array), + $"FlatList.Parallel.sort of %s{description}" + ) + ) + + [] + member _.``sortBy orders the elements by the projection`` () = + forEach + allInputs + (fun array description -> + Assert.AreEqual( + FlatList.toList (FlatList.sortBy negate array), + FlatList.toList (FlatList.Parallel.sortBy negate array), + $"FlatList.Parallel.sortBy of %s{description}" + ) + ) + + [] + member _.``sortByDescending orders the elements by the projection descending`` () = + forEach + allInputs + (fun array description -> + Assert.AreEqual( + FlatList.toList (FlatList.sortByDescending negate array), + FlatList.toList (FlatList.Parallel.sortByDescending negate array), + $"FlatList.Parallel.sortByDescending of %s{description}" + ) + ) + + [] + member _.``sortDescending orders the elements descending`` () = + forEach + allInputs + (fun array description -> + Assert.AreEqual( + FlatList.toList (FlatList.sortDescending array), + FlatList.toList (FlatList.Parallel.sortDescending array), + $"FlatList.Parallel.sortDescending of %s{description}" + ) + ) + + [] + member _.``sortWith orders the elements with the comparison function`` () = + let descending (left : int) (right : int) = compare right left + + forEach + allInputs + (fun array description -> + Assert.AreEqual( + FlatList.toList (FlatList.sortWith descending array), + FlatList.toList (FlatList.Parallel.sortWith descending array), + $"FlatList.Parallel.sortWith of %s{description}" + ) + ) + + [] + member _.``the sorting functions leave the shared array of the input unchanged`` () = + let array = FlatList.ofList large + + FlatList.Parallel.sort array |> ignore + FlatList.Parallel.sortBy negate array |> ignore + FlatList.Parallel.sortByDescending negate array |> ignore + FlatList.Parallel.sortDescending array |> ignore + FlatList.Parallel.sortWith compare array |> ignore + + Assert.AreEqual(large, FlatList.toList array, "The sorting functions must sort a copy of the input") + + // ---- Default flat lists ---- + + [] + member _.``functions reject a default flat list as Array.Parallel rejects a null array`` () = + let firstChoice (item : int) : Choice = Choice1Of2 item + + let cases : struct (string * (unit -> unit) * (unit -> unit)) list = [ + struct (nameof FlatList.Parallel.average, + (fun () -> + Array.Parallel.average (Unchecked.defaultof) + |> ignore + ), + (fun () -> + FlatList.Parallel.average (Unchecked.defaultof>) + |> ignore + )) + struct (nameof FlatList.Parallel.averageBy, + (fun () -> Array.Parallel.averageBy float nullArray |> ignore), + (fun () -> FlatList.Parallel.averageBy float defaultArray |> ignore)) + struct (nameof FlatList.Parallel.choose, + (fun () -> Array.Parallel.choose Some nullArray |> ignore), + (fun () -> FlatList.Parallel.choose ValueSome defaultArray |> ignore)) + struct (nameof FlatList.Parallel.collect, + (fun () -> Array.Parallel.collect Array.singleton nullArray |> ignore), + (fun () -> + FlatList.Parallel.collect FlatList.singleton defaultArray + |> ignore + )) + struct (nameof FlatList.Parallel.exists, + (fun () -> Array.Parallel.exists ((=) 1) nullArray |> ignore), + (fun () -> FlatList.Parallel.exists ((=) 1) defaultArray |> ignore)) + struct (nameof FlatList.Parallel.filter, + (fun () -> Array.Parallel.filter ((=) 1) nullArray |> ignore), + (fun () -> FlatList.Parallel.filter ((=) 1) defaultArray |> ignore)) + struct (nameof FlatList.Parallel.forall, + (fun () -> Array.Parallel.forall ((=) 1) nullArray |> ignore), + (fun () -> FlatList.Parallel.forall ((=) 1) defaultArray |> ignore)) + struct (nameof FlatList.Parallel.groupBy, + (fun () -> Array.Parallel.groupBy id nullArray |> ignore), + (fun () -> FlatList.Parallel.groupBy id defaultArray |> ignore)) + struct (nameof FlatList.Parallel.iter, + (fun () -> Array.Parallel.iter ignore nullArray), + (fun () -> FlatList.Parallel.iter ignore defaultArray)) + struct (nameof FlatList.Parallel.iteri, + (fun () -> Array.Parallel.iteri (fun _ _ -> ()) nullArray), + (fun () -> FlatList.Parallel.iteri (fun _ _ -> ()) defaultArray)) + struct (nameof FlatList.Parallel.map, + (fun () -> Array.Parallel.map id nullArray |> ignore), + (fun () -> FlatList.Parallel.map id defaultArray |> ignore)) + struct (nameof FlatList.Parallel.mapi, + (fun () -> Array.Parallel.mapi (fun _ item -> item) nullArray |> ignore), + (fun () -> + FlatList.Parallel.mapi (fun _ item -> item) defaultArray + |> ignore + )) + struct (nameof FlatList.Parallel.max, + (fun () -> Array.Parallel.max nullArray |> ignore), + (fun () -> FlatList.Parallel.max defaultArray |> ignore)) + struct (nameof FlatList.Parallel.maxBy, + (fun () -> Array.Parallel.maxBy abs nullArray |> ignore), + (fun () -> FlatList.Parallel.maxBy abs defaultArray |> ignore)) + struct (nameof FlatList.Parallel.min, + (fun () -> Array.Parallel.min nullArray |> ignore), + (fun () -> FlatList.Parallel.min defaultArray |> ignore)) + struct (nameof FlatList.Parallel.minBy, + (fun () -> Array.Parallel.minBy abs nullArray |> ignore), + (fun () -> FlatList.Parallel.minBy abs defaultArray |> ignore)) + struct (nameof FlatList.Parallel.partition, + (fun () -> Array.Parallel.partition ((=) 1) nullArray |> ignore), + (fun () -> FlatList.Parallel.partition ((=) 1) defaultArray |> ignore)) + struct (nameof FlatList.Parallel.partitionWith, + (fun () -> Array.Parallel.partitionWith firstChoice nullArray |> ignore), + (fun () -> + FlatList.Parallel.partitionWith firstChoice defaultArray + |> ignore + )) + struct (nameof FlatList.Parallel.reduce, + (fun () -> Array.Parallel.reduce (+) nullArray |> ignore), + (fun () -> FlatList.Parallel.reduce (+) defaultArray |> ignore)) + struct (nameof FlatList.Parallel.reduceBy, + (fun () -> Array.Parallel.reduceBy abs (+) nullArray |> ignore), + (fun () -> FlatList.Parallel.reduceBy abs (+) defaultArray |> ignore)) + struct (nameof FlatList.Parallel.sort, + (fun () -> Array.Parallel.sort nullArray |> ignore), + (fun () -> FlatList.Parallel.sort defaultArray |> ignore)) + struct (nameof FlatList.Parallel.sortBy, + (fun () -> Array.Parallel.sortBy negate nullArray |> ignore), + (fun () -> FlatList.Parallel.sortBy negate defaultArray |> ignore)) + struct (nameof FlatList.Parallel.sortByDescending, + (fun () -> Array.Parallel.sortByDescending negate nullArray |> ignore), + (fun () -> + FlatList.Parallel.sortByDescending negate defaultArray + |> ignore + )) + struct (nameof FlatList.Parallel.sortDescending, + (fun () -> Array.Parallel.sortDescending nullArray |> ignore), + (fun () -> FlatList.Parallel.sortDescending defaultArray |> ignore)) + struct (nameof FlatList.Parallel.sortWith, + (fun () -> Array.Parallel.sortWith compare nullArray |> ignore), + (fun () -> FlatList.Parallel.sortWith compare defaultArray |> ignore)) + struct (nameof FlatList.Parallel.tryFind, + (fun () -> Array.Parallel.tryFind ((=) 1) nullArray |> ignore), + (fun () -> FlatList.Parallel.tryFind ((=) 1) defaultArray |> ignore)) + struct (nameof FlatList.Parallel.tryFindIndex, + (fun () -> Array.Parallel.tryFindIndex ((=) 1) nullArray |> ignore), + (fun () -> + FlatList.Parallel.tryFindIndex ((=) 1) defaultArray + |> ignore + )) + struct (nameof FlatList.Parallel.tryPick, + (fun () -> Array.Parallel.tryPick Some nullArray |> ignore), + (fun () -> FlatList.Parallel.tryPick ValueSome defaultArray |> ignore)) + struct (nameof FlatList.Parallel.zip, + (fun () -> Array.Parallel.zip nullArray [| 1 |] |> ignore), + (fun () -> + FlatList.Parallel.zip defaultArray (FlatList.singleton 1) + |> ignore + )) + struct (nameof FlatList.Parallel.zip, + (fun () -> Array.Parallel.zip [| 1 |] nullArray |> ignore), + (fun () -> + FlatList.Parallel.zip (FlatList.singleton 1) defaultArray + |> ignore + )) + ] + + for struct (name, oracle, actual) in cases do + ExceptionAssert.same id oracle actual $"FlatList.Parallel.%s{name} must reject a default flat list" + + [] + member _.``sum and sumBy reject a default flat list where Array.Parallel dereferences a null array`` () = + // Array.Parallel.sum and sumBy throw NullReferenceException for a null array; their documentation promises + // ArgumentNullException, which a default flat list throws + let cases : struct (string * (unit -> unit)) list = [ + struct (nameof FlatList.Parallel.sum, (fun () -> FlatList.Parallel.sum defaultArray |> ignore)) + struct (nameof FlatList.Parallel.sumBy, (fun () -> FlatList.Parallel.sumBy abs defaultArray |> ignore)) + ] + + for struct (name, actual) in cases do + let ex = + Assert.ThrowsExactly( + Action actual, + $"FlatList.Parallel.%s{name} must reject a default flat list" + ) + + Assert.AreEqual( + "array", + ex.ParamName, + $"FlatList.Parallel.%s{name} must name the parameter of the default flat list" + ) diff --git a/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelTupleTests.fs b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelTupleTests.fs new file mode 100644 index 00000000..8b9891d1 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelTupleTests.fs @@ -0,0 +1,110 @@ +namespace FSharp.Collections.Immutable.Tests + +open FSharp.Collections.Immutable +open FSharp.Collections.Immutable.Tuple +open Microsoft.VisualStudio.TestTools.UnitTesting + +// The type annotations of the results are the proof that the functions of the Tuple namespace shadow the default ones, +// also in the nested Parallel module. + +/// The `FlatList.Parallel` module of the `Tuple` namespace. +[] +type FlatListParallelTupleTests () = + + static let array = FlatList.ofList Inputs.several + + static let other = FlatList.ofList Inputs.other + + /// A flat list long enough for the functions to split their work among several threads. + static let large = FlatList.init 10_000 (fun index -> index % 1_000 - 500) + + static let isPositive (item : int) = item > 0 + + [] + member _.``zip returns reference tuples`` () = + for struct (first, second) in [ struct (array, other); struct (large, FlatList.rev large) ] do + let pairs : FlatList = FlatList.Parallel.zip first second + + Assert.AreEqual<(int * int) list>( + List.zip (FlatList.toList first) (FlatList.toList second), + FlatList.toList pairs, + $"Tuple.FlatList.Parallel.zip of %d{first.Length} elements must zip as List.zip does" + ) + + [] + member _.``zip throws for different lengths as the default does`` () = + ExceptionAssert.same + id + (fun () -> + FSharp.Collections.Immutable.FlatList.Parallel.zip array (FlatList.singleton 1) + |> ignore + ) + (fun () -> FlatList.Parallel.zip array (FlatList.singleton 1) |> ignore) + "Tuple.FlatList.Parallel.zip must throw as the default zip does" + + [] + member _.``groupBy returns reference tuples`` () = + for source in [ array; large ] do + let groups : FlatList> = FlatList.Parallel.groupBy (fun item -> item % 3) source + + // The order of the keys and of the elements of a key is open, so both are sorted + Assert.AreEqual<(int * int list) list>( + List.groupBy (fun item -> item % 3) (FlatList.toList source) + |> Seq.map (fun (key, items) -> key, List.sort items) + |> Seq.sortBy fst + |> Seq.toList, + groups + |> Seq.map (fun (key, items) -> key, items |> Seq.sort |> Seq.toList) + |> Seq.sortBy fst + |> Seq.toList, + $"Tuple.FlatList.Parallel.groupBy of %d{source.Length} elements must group as List.groupBy does" + ) + + [] + member _.``partition returns a reference tuple`` () = + for source in [ array; large ] do + let (accepted : FlatList), (rejected : FlatList) = + FlatList.Parallel.partition isPositive source + let expectedAccepted, expectedRejected = List.partition isPositive (FlatList.toList source) + + Assert.AreEqual( + expectedAccepted, + FlatList.toList accepted, + $"Tuple.FlatList.Parallel.partition of %d{source.Length} elements must return the accepted elements" + ) + + Assert.AreEqual( + expectedRejected, + FlatList.toList rejected, + $"Tuple.FlatList.Parallel.partition of %d{source.Length} elements must return the rejected elements" + ) + + [] + member _.``partitionWith returns a reference tuple`` () = + let partitioner (item : int) = + if item > 0 then + Choice1Of2 item + else + Choice2Of2 (string item) + + for source in [ array; large ] do + let (first : FlatList), (second : FlatList) = + FlatList.Parallel.partitionWith partitioner source + let expectedFirst, expectedSecond = List.partitionWith partitioner (FlatList.toList source) + + Assert.AreEqual( + expectedFirst, + FlatList.toList first, + $"Tuple.FlatList.Parallel.partitionWith of %d{source.Length} elements must return the first choices" + ) + + Assert.AreEqual( + expectedSecond, + FlatList.toList second, + $"Tuple.FlatList.Parallel.partitionWith of %d{source.Length} elements must return the second choices" + ) + + [] + member _.``the functions without a tuple keep resolving to the default module`` () = + let found : int voption = FlatList.Parallel.tryFind (fun item -> item > 6) array + Assert.AreEqual(ValueSome 8, found, "FlatList.Parallel.tryFind must resolve to the default module") From 560985d6a56f8585c7fc7ea506f97b77b6320806 Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Sun, 18 Oct 2026 12:45:41 +0200 Subject: [PATCH 03/14] docs: `FlatList.Parallel` in the changelog Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7d6d007..8373ad55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 change the builder return it. `replace` and `replaceWith` leave the builder as it is when no element equals the old value, as `ImmutableArray<'T>.Builder.Replace` does, where `FlatList.replace` throws `ArgumentException`. * `flatList { }` builds a flat list. +* `FlatList.Parallel` has every function of the FSharp.Core `Array.Parallel` module except the in-place sorts, with its + semantics and exceptions. Its `tryFind`, `tryFindIndex` and `tryPick` return `voption`, `choose` and `tryPick` take + functions that return `voption`, and `groupBy`, `partition`, `partitionWith` and `zip` return struct tuples; the + `Parallel` modules of the `Option` and `Tuple` namespaces repeat them with options and reference tuples. ### Changed From 8d68438a141e5684816d06412aacdfd2ce0cf69e Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Sun, 18 Oct 2026 13:00:36 +0200 Subject: [PATCH 04/14] fixup! feat: `FlatList.Parallel` module with the vocabulary of the FSharp.Core `Array.Parallel` module Parallel.collect rejects a default flat list from the mapping with ArgumentNullException, as the sequential collect does. Co-Authored-By: Claude Opus 5.5 --- src/FSharp.Collections.Immutable/FlatList/FlatList.fs | 5 +++-- src/FSharp.Collections.Immutable/FlatList/FlatList.fsi | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/FSharp.Collections.Immutable/FlatList/FlatList.fs b/src/FSharp.Collections.Immutable/FlatList/FlatList.fs index 01db002d..93a7c25a 100644 --- a/src/FSharp.Collections.Immutable/FlatList/FlatList.fs +++ b/src/FSharp.Collections.Immutable/FlatList/FlatList.fs @@ -1054,12 +1054,13 @@ module Parallel = [] let collect (mapping : 'T -> FlatList<'U>) (array : FlatList<'T>) = // As in Array.Parallel.collect, the mapping runs in parallel, and the results are copied in their order into one - // array of their total length. A default flat list from the mapping throws NullReferenceException there, as a - // null array does in Array.Parallel.collect. + // array of their total length. Array.Parallel.collect dereferences a null result; a default flat list from the + // mapping throws ArgumentNullException instead, as in the sequential collect (ADR 0001, D6). let results = Array.Parallel.map mapping (unwrapArray (nameof array) array) let mutable length = 0 for result in results do + checkNotDefault (nameof mapping) result length <- length + result.Length let builder = ImmutableArray.CreateBuilder<'U> length diff --git a/src/FSharp.Collections.Immutable/FlatList/FlatList.fsi b/src/FSharp.Collections.Immutable/FlatList/FlatList.fsi index 9987473b..9813a9c7 100644 --- a/src/FSharp.Collections.Immutable/FlatList/FlatList.fsi +++ b/src/FSharp.Collections.Immutable/FlatList/FlatList.fsi @@ -2177,6 +2177,7 @@ module Parallel = /// The function that returns a flat list for each element, called in parallel. /// The input flat list. /// The concatenation of the returned flat lists, in the order of the elements. + /// Thrown when the mapping returns a default flat list. /// O(n + the total length of the returned flat lists). [] val collect : mapping : ('T -> FlatList<'U>) -> array : FlatList<'T> -> FlatList<'U> From 87488d46225f8be281be70f55bb954261f4f5e11 Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Sun, 18 Oct 2026 13:00:36 +0200 Subject: [PATCH 05/14] fixup! test: `FlatList.Parallel` against the sequential functions and `Array.Parallel` Parallel.collect is expected to throw ArgumentNullException naming the mapping for a default flat list. Co-Authored-By: Claude Opus 5.5 --- .../FlatList/FlatListParallelTests.fs | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelTests.fs b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelTests.fs index 210ab986..ec302c5d 100644 --- a/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelTests.fs +++ b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelTests.fs @@ -183,18 +183,22 @@ type FlatListParallelTests () = ) [] - member _.``collect throws for a default flat list of the mapping as Array.Parallel does for a null array`` () = - ExceptionAssert.same - id - (fun () -> - Array.Parallel.collect (fun _ -> nullArray) [| 1 |] - |> ignore - ) - (fun () -> - FlatList.Parallel.collect (fun _ -> defaultArray) (FlatList.singleton 1) - |> ignore + member _.``collect throws ArgumentNullException for a default flat list of the mapping`` () = + // Array.Parallel.collect dereferences a null result; a default flat list is rejected as in the sequential collect + let ex = + Assert.ThrowsExactly( + Action (fun () -> + FlatList.Parallel.collect (fun _ -> defaultArray) (FlatList.singleton 1) + |> ignore + ), + "FlatList.Parallel.collect must reject a default flat list of the mapping" ) - "FlatList.Parallel.collect must throw for a default flat list of the mapping as Array.Parallel.collect does" + + Assert.AreEqual( + "mapping", + ex.ParamName, + "FlatList.Parallel.collect must name the mapping as the source of the default flat list" + ) [] member _.``filter keeps the elements that satisfy the predicate in their order`` () = From 5db37afaea83e663d1a6ef5e12dcefefa9ef5973 Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Sun, 18 Oct 2026 13:09:15 +0200 Subject: [PATCH 06/14] fixup! feat: `FlatList.Parallel` module with the vocabulary of the FSharp.Core `Array.Parallel` module The chunk count of the parallel loops divides the length minus one, and the end of a chunk adds only the remaining elements, so neither overflows Int32 for arrays near the maximum length; the chunks are a pure function of the processor count and the length, which the tests reach through InternalsVisibleTo. Parallel.groupBy compares float and float32 keys with GenericEqualityER as Array.Parallel.groupBy does, so all NaN keys are in one group. Co-Authored-By: Claude Opus 5.5 --- .../FlatList/FlatList.fsi | 7 +- .../FlatList/FlatListTuple.fsi | 7 +- .../Helper/ParallelArrays.fs | 71 +++++++++++++++---- 3 files changed, 69 insertions(+), 16 deletions(-) diff --git a/src/FSharp.Collections.Immutable/FlatList/FlatList.fsi b/src/FSharp.Collections.Immutable/FlatList/FlatList.fsi index 9813a9c7..dd4e4ea7 100644 --- a/src/FSharp.Collections.Immutable/FlatList/FlatList.fsi +++ b/src/FSharp.Collections.Immutable/FlatList/FlatList.fsi @@ -2349,7 +2349,12 @@ module Parallel = /// /// The keys with their elements. The order of the keys and the order of the elements of a key are not specified. /// - /// O(n). The keys compare in F# structural equality. + /// + /// O(n). The keys compare in F# structural equality, except that keys of type and + /// compare as in + /// , + /// which puts all NaN keys in one group. + /// [] val groupBy : projection : ('T -> 'Key) -> array : FlatList<'T> -> FlatList)> when 'Key : equality diff --git a/src/FSharp.Collections.Immutable/FlatList/FlatListTuple.fsi b/src/FSharp.Collections.Immutable/FlatList/FlatListTuple.fsi index d1b516a5..64970702 100644 --- a/src/FSharp.Collections.Immutable/FlatList/FlatListTuple.fsi +++ b/src/FSharp.Collections.Immutable/FlatList/FlatListTuple.fsi @@ -201,7 +201,12 @@ module Parallel = /// /// The keys with their elements. The order of the keys and the order of the elements of a key are not specified. /// - /// O(n). The keys compare in F# structural equality. + /// + /// O(n). The keys compare in F# structural equality, except that keys of type and + /// compare as in + /// , + /// which puts all NaN keys in one group. + /// [] val groupBy : projection : ('T -> 'Key) -> array : FlatList<'T> -> FlatList<'Key * FlatList<'T>> when 'Key : equality diff --git a/src/FSharp.Collections.Immutable/Helper/ParallelArrays.fs b/src/FSharp.Collections.Immutable/Helper/ParallelArrays.fs index b056e8ea..f004267b 100644 --- a/src/FSharp.Collections.Immutable/Helper/ParallelArrays.fs +++ b/src/FSharp.Collections.Immutable/Helper/ParallelArrays.fs @@ -10,20 +10,40 @@ module internal FSharp.Collections.Immutable.ParallelArrays open System +open System.Collections.Generic open System.Linq open System.Threading.Tasks -/// The length of the chunks of a chunked parallel loop over elements. +/// +/// The chunks of a chunked parallel loop over elements on +/// processors, each as its first index and the index after its last one. +/// /// +/// /// The chunks are those of the chunked loops of : one chunk /// per processor, and a single chunk for fewer than 256 elements, which are not worth a second thread. +/// +/// +/// The arithmetic stays in the range of for every array length. The count divides the length +/// minus one, because the length plus the chunk length overflows for arrays of more than half the maximum length. The end +/// of a chunk adds only the elements that remain to its first index, because the first index plus the full chunk length +/// can overflow for arrays near the maximum length on machines with more than 56 processors. +/// /// -let chunkLength (length : int) = - let processors = Environment.ProcessorCount +let chunks (processors : int) (length : int) : struct (int * int) array = + let chunk = + if length < 256 then length + elif length % processors = 0 then length / processors + else length / processors + 1 + + let count = if chunk = 0 then 0 else (length - 1) / chunk + 1 - if length < 256 then length - elif length % processors = 0 then length / processors - else length / processors + 1 + Array.init + count + (fun index -> + let first = index * chunk + struct (first, first + min chunk (length - first)) + ) /// Runs an action on the chunks of the indices from 0 to in parallel. /// @@ -31,15 +51,14 @@ let chunkLength (length : int) = /// element. /// let inline forEachChunk (length : int) ([] action : int -> int -> unit) = - let chunk = chunkLength length - let chunks = if chunk = 0 then 0 else (length + chunk - 1) / chunk + let bounds = chunks Environment.ProcessorCount length Parallel.For ( 0, - chunks, + bounds.Length, fun index -> - let first = index * chunk - action first (min length (first + chunk)) + let struct (first, last) = bounds[index] + action first last ) |> ignore @@ -69,11 +88,35 @@ let choose (chooser : 'T -> 'U voption) (array : 'T array) : 'U array = result +/// The comparer of the keys of a parallel grouping. +/// +/// +/// Keys of type and compare as in +/// : +/// with , so that all NaN keys are in +/// one group. Other keys compare in F# structural equality, in which NaN is not equal to itself. +/// +/// +/// A key that contains NaN, such as a tuple, therefore makes a group of its own, as in +/// . +/// The function of FSharp.Core throws for such a key: its second pass cannot find +/// the key that its first pass added. That failure is not mirrored. +/// +/// +let groupingComparer<'Key when 'Key : equality> () : IEqualityComparer<'Key> = + if + Type.(=) (typeof<'Key>, typeof) + || Type.(=) (typeof<'Key>, typeof) + then + HashIdentity.FromFunctions<'Key> LanguagePrimitives.GenericHash LanguagePrimitives.GenericEqualityER + else + HashIdentity.Structural<'Key> + /// The keys of the projection, each with its elements; the order of both is not specified. /// /// The projection, the work of the caller, runs in parallel. The grouping then runs over the computed keys with -/// , -/// as the sequential grouping of the Arrays module does, so that the keys compare in F# structural equality and a +/// +/// and the comparer of , so that a /// key is a key like any other. The result has the order of /// , /// one of the orders that @@ -90,7 +133,7 @@ let inline groupBy let lookup = Enumerable .Range(0, array.Length) - .ToLookup((fun index -> keys[index]), (fun index -> array[index]), HashIdentity.Structural<'Key>) + .ToLookup((fun index -> keys[index]), (fun index -> array[index]), groupingComparer<'Key>()) let result = Array.zeroCreate lookup.Count let mutable index = 0 From 4e1c711b86fc0bab248539abea5e42608af204ec Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Sun, 18 Oct 2026 13:09:15 +0200 Subject: [PATCH 07/14] fixup! test: `FlatList.Parallel` against the sequential functions and `Array.Parallel` The chunks of zip are checked for 1.5 billion elements and for the maximum array length on 64 processors, and groupBy of both modules for NaN keys of type float and float32 against Array.Parallel.groupBy. Co-Authored-By: Claude Opus 5.5 --- .../FlatList/FlatListParallelTests.fs | 58 +++++++++++++++++++ .../FlatList/FlatListParallelTupleTests.fs | 21 +++++++ 2 files changed, 79 insertions(+) diff --git a/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelTests.fs b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelTests.fs index ec302c5d..7fade75e 100644 --- a/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelTests.fs +++ b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelTests.fs @@ -268,6 +268,44 @@ type FlatListParallelTests () = ) $"FlatList.Parallel.zip of %d{first.Length} and %d{second.Length} elements must throw as Array.Parallel.zip does" + [] + member _.``zip divides every array length into chunks without overflow`` () = + // The chunks of the parallel loop of zip are checked directly: arrays of these lengths do not fit into the memory of + // a test run. The length plus the chunk length overflowed for 1.5 billion elements, so the loop did nothing. + let cases = [ + struct (4, 0, []) + struct (4, 255, [ struct (0, 255) ]) + struct (3, 1_000, [ struct (0, 334); struct (334, 668); struct (668, 1_000) ]) + struct (2, 1_500_000_000, [ struct (0, 750_000_000); struct (750_000_000, 1_500_000_000) ]) + ] + + for struct (processors, length, expected) in cases do + Assert.AreEqual( + expected, + ParallelArrays.chunks processors length |> Array.toList, + $"The chunks of %d{length} elements on %d{processors} processors" + ) + + // On 64 processors, the first index plus the chunk length of the last chunk of the longest array is more than + // Int32.MaxValue, so the chunk must be cut to the length before the addition + let bounds = ParallelArrays.chunks 64 Array.MaxLength + Assert.HasCount (64, bounds, "The longest array on 64 processors must have one chunk per processor") + + Assert.AreEqual( + struct (2_113_929_216, Array.MaxLength), + Array.last bounds, + "The last chunk of the longest array must end at its length" + ) + + for index = 1 to bounds.Length - 1 do + let struct (_, previousEnd) = bounds[index - 1] + let struct (first, _) = bounds[index] + Assert.AreEqual( + previousEnd, + first, + $"Chunk %d{index} of the longest array must start where the chunk before it ends" + ) + // ---- Folding ---- [] @@ -554,6 +592,26 @@ type FlatListParallelTests () = ) ) + [] + member _.``groupBy puts the NaN keys of type float and float32 in one group as Array.Parallel does`` () = + // Array.Parallel.groupBy compares these keys with ER semantics, while the sequential Array.groupBy makes a group of + // each NaN. The keys are compared as text, because NaN is not equal to itself. + let check (source : 'Key array) = + Assert.AreEqual<(string * int) list>( + Array.Parallel.groupBy id source + |> Seq.map (fun (key, items) -> string key, items.Length) + |> Seq.sort + |> Seq.toList, + FlatList.Parallel.groupBy id (FlatList.ofArray source) + |> Seq.map (fun struct (key, items) -> string key, items.Length) + |> Seq.sort + |> Seq.toList, + $"FlatList.Parallel.groupBy of %A{source}" + ) + + check [| nan; 1.0; nan; 2.0; nan |] + check [| nanf; 1.0f; nanf; 2.0f; nanf |] + [] member _.``partition splits the elements in their order`` () = forEach diff --git a/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelTupleTests.fs b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelTupleTests.fs index 8b9891d1..0ed9c904 100644 --- a/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelTupleTests.fs +++ b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelTupleTests.fs @@ -60,6 +60,27 @@ type FlatListParallelTupleTests () = $"Tuple.FlatList.Parallel.groupBy of %d{source.Length} elements must group as List.groupBy does" ) + [] + member _.``groupBy puts the NaN keys of type float and float32 in one group as Array.Parallel does`` () = + // The keys are compared as text, because NaN is not equal to itself + let check (source : 'Key array) = + let groups : FlatList<'Key * FlatList<'Key>> = FlatList.Parallel.groupBy id (FlatList.ofArray source) + + Assert.AreEqual<(string * int) list>( + Array.Parallel.groupBy id source + |> Seq.map (fun (key, items) -> string key, items.Length) + |> Seq.sort + |> Seq.toList, + groups + |> Seq.map (fun (key, items) -> string key, items.Length) + |> Seq.sort + |> Seq.toList, + $"Tuple.FlatList.Parallel.groupBy of %A{source}" + ) + + check [| nan; 1.0; nan; 2.0; nan |] + check [| nanf; 1.0f; nanf; 2.0f; nanf |] + [] member _.``partition returns a reference tuple`` () = for source in [ array; large ] do From fbda26b5ad712c7b020c60e32af70a517f322678 Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Sun, 18 Oct 2026 15:30:10 +0200 Subject: [PATCH 08/14] fixup! feat: `FlatList.Parallel` module with the vocabulary of the FSharp.Core `Array.Parallel` module The nested `Parallel` modules follow their root modules (ADR 0001, D2) one level less indented; Fantomas joins the lines that now fit. Co-Authored-By: Claude Opus 5.5 --- src/FSharp.Collections.Immutable/FlatList/FlatList.fs | 9 +++------ src/FSharp.Collections.Immutable/FlatList/FlatList.fsi | 9 +++------ .../FlatList/FlatListOption.fs | 3 +-- 3 files changed, 7 insertions(+), 14 deletions(-) diff --git a/src/FSharp.Collections.Immutable/FlatList/FlatList.fs b/src/FSharp.Collections.Immutable/FlatList/FlatList.fs index 93a7c25a..54a4f7fa 100644 --- a/src/FSharp.Collections.Immutable/FlatList/FlatList.fs +++ b/src/FSharp.Collections.Immutable/FlatList/FlatList.fs @@ -1037,8 +1037,7 @@ module Parallel = | ValueNone -> ValueNone [] - let tryFindIndex predicate (array : FlatList<'T>) = - ParallelArrays.tryFindIndex predicate (unwrapArray (nameof array) array) + let tryFindIndex predicate (array : FlatList<'T>) = ParallelArrays.tryFindIndex predicate (unwrapArray (nameof array) array) [] let tryPick (chooser : 'T -> 'U voption) (array : FlatList<'T>) = @@ -1125,15 +1124,13 @@ module Parallel = let max (array : FlatList<'T>) = Array.Parallel.max (unwrapArray (nameof array) array) [] - let maxBy (projection : 'T -> 'U) (array : FlatList<'T>) = - Array.Parallel.maxBy projection (unwrapArray (nameof array) array) + let maxBy (projection : 'T -> 'U) (array : FlatList<'T>) = Array.Parallel.maxBy projection (unwrapArray (nameof array) array) [] let min (array : FlatList<'T>) = Array.Parallel.min (unwrapArray (nameof array) array) [] - let minBy (projection : 'T -> 'U) (array : FlatList<'T>) = - Array.Parallel.minBy projection (unwrapArray (nameof array) array) + let minBy (projection : 'T -> 'U) (array : FlatList<'T>) = Array.Parallel.minBy projection (unwrapArray (nameof array) array) [] let reduce reduction (array : FlatList<'T>) = Array.Parallel.reduce reduction (unwrapArray (nameof array) array) diff --git a/src/FSharp.Collections.Immutable/FlatList/FlatList.fsi b/src/FSharp.Collections.Immutable/FlatList/FlatList.fsi index dd4e4ea7..c97080ab 100644 --- a/src/FSharp.Collections.Immutable/FlatList/FlatList.fsi +++ b/src/FSharp.Collections.Immutable/FlatList/FlatList.fsi @@ -2325,8 +2325,7 @@ module Parallel = /// one. /// [] - val inline sum : - array : FlatList< ^T > -> ^T when ^T : (static member (+) : ^T * ^T -> ^T) and ^T : (static member Zero : ^T) + val inline sum : array : FlatList< ^T > -> ^T when ^T : (static member (+) : ^T * ^T -> ^T) and ^T : (static member Zero : ^T) /// Returns the sum of the results of a function applied to each element. /// The function to transform the elements, called in parallel. @@ -2356,8 +2355,7 @@ module Parallel = /// which puts all NaN keys in one group. /// [] - val groupBy : - projection : ('T -> 'Key) -> array : FlatList<'T> -> FlatList)> when 'Key : equality + val groupBy : projection : ('T -> 'Key) -> array : FlatList<'T> -> FlatList)> when 'Key : equality /// Splits the elements by a predicate. /// The function to test the elements, called in parallel. @@ -2375,8 +2373,7 @@ module Parallel = /// The values of the first choices and of the second choices, both in the order of the elements. /// O(n). [] - val partitionWith : - partitioner : ('T -> Choice<'T1, 'T2>) -> array : FlatList<'T> -> struct (FlatList<'T1> * FlatList<'T2>) + val partitionWith : partitioner : ('T -> Choice<'T1, 'T2>) -> array : FlatList<'T> -> struct (FlatList<'T1> * FlatList<'T2>) // ---- Sorting ---- diff --git a/src/FSharp.Collections.Immutable/FlatList/FlatListOption.fs b/src/FSharp.Collections.Immutable/FlatList/FlatListOption.fs index 2ed305e5..7307fb3a 100644 --- a/src/FSharp.Collections.Immutable/FlatList/FlatListOption.fs +++ b/src/FSharp.Collections.Immutable/FlatList/FlatListOption.fs @@ -67,8 +67,7 @@ module Parallel = let tryFind predicate (array : FlatList<'T>) = Array.Parallel.tryFind predicate (unwrapArray (nameof array) array) [] - let tryFindIndex predicate (array : FlatList<'T>) = - Array.Parallel.tryFindIndex predicate (unwrapArray (nameof array) array) + let tryFindIndex predicate (array : FlatList<'T>) = Array.Parallel.tryFindIndex predicate (unwrapArray (nameof array) array) [] let tryPick (chooser : 'T -> 'U option) (array : FlatList<'T>) = From f85716551b85ae216720e4b319e21e93c434238b Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Sun, 18 Oct 2026 15:30:10 +0200 Subject: [PATCH 09/14] fixup! test: `FlatList.Parallel` against the sequential functions and `Array.Parallel` Test methods carry no categories: the classes carry them (ADR 0001, D8). Co-Authored-By: Claude Opus 5.5 --- .../FlatList/FlatListParallelOptionTests.fs | 12 +-- .../FlatList/FlatListParallelTests.fs | 102 +++++++++--------- .../FlatList/FlatListParallelTupleTests.fs | 14 +-- 3 files changed, 64 insertions(+), 64 deletions(-) diff --git a/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelOptionTests.fs b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelOptionTests.fs index 35f6a887..9957a5e7 100644 --- a/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelOptionTests.fs +++ b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelOptionTests.fs @@ -26,7 +26,7 @@ type FlatListParallelOptionTests () = static let negatedIfNegative (item : int) = if item < 0 then Some (-item) else None - [] + [] member _.``tryFind returns an option`` () = for source in [ array; large ] do let found : int option = FlatList.Parallel.tryFind (isAbove 6) source @@ -36,7 +36,7 @@ type FlatListParallelOptionTests () = found $"Option.FlatList.Parallel.tryFind over %d{source.Length} elements must match the default" - [] + [] member _.``tryFindIndex returns an option`` () = for source in [ array; large ] do let found : int option = FlatList.Parallel.tryFindIndex (isAbove 6) source @@ -46,7 +46,7 @@ type FlatListParallelOptionTests () = found $"Option.FlatList.Parallel.tryFindIndex over %d{source.Length} elements must match the default" - [] + [] member _.``tryPick takes a chooser of options and returns an option`` () = for source in [ array; large ] do let found : int option = FlatList.Parallel.tryPick negatedIfNegative source @@ -57,7 +57,7 @@ type FlatListParallelOptionTests () = $"Option.FlatList.Parallel.tryPick over %d{source.Length} elements must pick as Array.tryPick does" ) - [] + [] member _.``choose takes a chooser of options`` () = for source in [ array; large ] do let chosen : FlatList = FlatList.Parallel.choose negatedIfNegative source @@ -68,7 +68,7 @@ type FlatListParallelOptionTests () = $"Option.FlatList.Parallel.choose over %d{source.Length} elements must choose as List.choose does" ) - [] + [] member _.``the functions reject a default flat list as Array.Parallel rejects a null array`` () = let cases : struct (string * (unit -> unit) * (unit -> unit)) list = [ struct (nameof FlatList.Parallel.choose, @@ -97,7 +97,7 @@ type FlatListParallelOptionTests () = for struct (name, oracle, actual) in cases do ExceptionAssert.same id oracle actual $"Option.FlatList.Parallel.%s{name} must reject a default flat list" - [] + [] member _.``the functions without an option keep resolving to the default module`` () = let mapped : FlatList = FlatList.Parallel.map (fun item -> item * 2) array let zipped : FlatList = FlatList.Parallel.zip array array diff --git a/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelTests.fs b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelTests.fs index 7fade75e..d001672d 100644 --- a/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelTests.fs +++ b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelTests.fs @@ -65,7 +65,7 @@ type FlatListParallelTests () = // ---- Creation ---- - [] + [] member _.``init creates the elements as the sequential init does`` () = for count in [ 0; 1; Inputs.several.Length; large.Length ] do Assert.AreEqual( @@ -74,7 +74,7 @@ type FlatListParallelTests () = $"FlatList.Parallel.init %d{count}" ) - [] + [] member _.``init throws for a negative count as Array.Parallel does`` () = ExceptionAssert.same id @@ -84,7 +84,7 @@ type FlatListParallelTests () = // ---- Search ---- - [] + [] member _.``exists answers as the sequential exists does`` () = forEach allInputs @@ -97,7 +97,7 @@ type FlatListParallelTests () = ) ) - [] + [] member _.``forall answers as the sequential forall does`` () = forEach allInputs @@ -113,7 +113,7 @@ type FlatListParallelTests () = ) ) - [] + [] member _.``tryFind returns the first element that satisfies the predicate`` () = forEach allInputs @@ -126,7 +126,7 @@ type FlatListParallelTests () = ) ) - [] + [] member _.``tryFindIndex returns the index of the first element that satisfies the predicate`` () = forEach allInputs @@ -139,7 +139,7 @@ type FlatListParallelTests () = ) ) - [] + [] member _.``tryPick returns the value for the first element that the chooser returns a value for`` () = forEach allInputs @@ -154,7 +154,7 @@ type FlatListParallelTests () = // ---- Transformation ---- - [] + [] member _.``choose keeps the chosen values in the order of their elements`` () = forEach allInputs @@ -167,7 +167,7 @@ type FlatListParallelTests () = ) ) - [] + [] member _.``collect concatenates the flat lists in the order of the elements`` () = // The flat lists of the mapping have zero, one or two elements let mapping (item : int) = FlatList.replicate (abs item % 3) item @@ -182,7 +182,7 @@ type FlatListParallelTests () = ) ) - [] + [] member _.``collect throws ArgumentNullException for a default flat list of the mapping`` () = // Array.Parallel.collect dereferences a null result; a default flat list is rejected as in the sequential collect let ex = @@ -200,7 +200,7 @@ type FlatListParallelTests () = "FlatList.Parallel.collect must name the mapping as the source of the default flat list" ) - [] + [] member _.``filter keeps the elements that satisfy the predicate in their order`` () = forEach allInputs @@ -213,7 +213,7 @@ type FlatListParallelTests () = ) ) - [] + [] member _.``map transforms every element in its order`` () = forEach allInputs @@ -225,7 +225,7 @@ type FlatListParallelTests () = ) ) - [] + [] member _.``mapi transforms every element with its index`` () = let mapping (index : int) (item : int) = struct (index, item * 3) @@ -239,7 +239,7 @@ type FlatListParallelTests () = ) ) - [] + [] member _.``zip pairs the elements at the same index`` () = forEach allInputs @@ -253,7 +253,7 @@ type FlatListParallelTests () = ) ) - [] + [] member _.``zip throws for different lengths as Array.Parallel does`` () = let short = List.toArray Inputs.singleton let long = List.toArray Inputs.several @@ -268,7 +268,7 @@ type FlatListParallelTests () = ) $"FlatList.Parallel.zip of %d{first.Length} and %d{second.Length} elements must throw as Array.Parallel.zip does" - [] + [] member _.``zip divides every array length into chunks without overflow`` () = // The chunks of the parallel loop of zip are checked directly: arrays of these lengths do not fit into the memory of // a test run. The length plus the chunk length overflowed for 1.5 billion elements, so the loop did nothing. @@ -308,7 +308,7 @@ type FlatListParallelTests () = // ---- Folding ---- - [] + [] member _.``average returns the average of the elements`` () = // Whole numbers add up exactly in any order, so the parallel sum is the sequential one forEach @@ -323,7 +323,7 @@ type FlatListParallelTests () = ) ) - [] + [] member _.``average throws for an empty flat list as Array.Parallel does`` () = ExceptionAssert.same id @@ -331,7 +331,7 @@ type FlatListParallelTests () = (fun () -> FlatList.Parallel.average FlatList.empty |> ignore) "FlatList.Parallel.average of an empty flat list must throw as Array.Parallel.average does" - [] + [] member _.``averageBy returns the average of the projections`` () = forEach nonEmptyInputs @@ -343,7 +343,7 @@ type FlatListParallelTests () = ) ) - [] + [] member _.``averageBy throws for an empty flat list as Array.Parallel does`` () = ExceptionAssert.same id @@ -354,7 +354,7 @@ type FlatListParallelTests () = ) "FlatList.Parallel.averageBy of an empty flat list must throw as Array.Parallel.averageBy does" - [] + [] member _.``iter visits every element once`` () = forEach allInputs @@ -369,7 +369,7 @@ type FlatListParallelTests () = ) ) - [] + [] member _.``iteri visits every element once with its index`` () = forEach allInputs @@ -386,7 +386,7 @@ type FlatListParallelTests () = ) ) - [] + [] member _.``max returns the greatest element`` () = forEach nonEmptyInputs @@ -398,7 +398,7 @@ type FlatListParallelTests () = ) ) - [] + [] member _.``max throws for an empty flat list as Array.Parallel does`` () = ExceptionAssert.same id @@ -406,7 +406,7 @@ type FlatListParallelTests () = (fun () -> FlatList.Parallel.max FlatList.empty |> ignore) "FlatList.Parallel.max of an empty flat list must throw as Array.Parallel.max does" - [] + [] member _.``maxBy returns the element with the greatest projection`` () = // In every input, only equal elements share the greatest absolute value, so any of them is the expected one forEach @@ -419,7 +419,7 @@ type FlatListParallelTests () = ) ) - [] + [] member _.``maxBy chooses among equal projections as Array.Parallel does`` () = let key (struct (key : int, _ : char)) = key @@ -429,7 +429,7 @@ type FlatListParallelTests () = "FlatList.Parallel.maxBy must choose among equal keys as Array.Parallel.maxBy does" ) - [] + [] member _.``maxBy throws for an empty flat list as Array.Parallel does`` () = ExceptionAssert.same id @@ -437,7 +437,7 @@ type FlatListParallelTests () = (fun () -> FlatList.Parallel.maxBy abs FlatList.empty |> ignore) "FlatList.Parallel.maxBy of an empty flat list must throw as Array.Parallel.maxBy does" - [] + [] member _.``min returns the smallest element`` () = forEach nonEmptyInputs @@ -449,7 +449,7 @@ type FlatListParallelTests () = ) ) - [] + [] member _.``min throws for an empty flat list as Array.Parallel does`` () = ExceptionAssert.same id @@ -457,7 +457,7 @@ type FlatListParallelTests () = (fun () -> FlatList.Parallel.min FlatList.empty |> ignore) "FlatList.Parallel.min of an empty flat list must throw as Array.Parallel.min does" - [] + [] member _.``minBy returns the element with the smallest projection`` () = // In every input, only equal elements share the smallest absolute value, so any of them is the expected one forEach @@ -470,7 +470,7 @@ type FlatListParallelTests () = ) ) - [] + [] member _.``minBy chooses among equal projections as Array.Parallel does`` () = let key (struct (key : int, _ : char)) = key @@ -480,7 +480,7 @@ type FlatListParallelTests () = "FlatList.Parallel.minBy must choose among equal keys as Array.Parallel.minBy does" ) - [] + [] member _.``minBy throws for an empty flat list as Array.Parallel does`` () = ExceptionAssert.same id @@ -488,7 +488,7 @@ type FlatListParallelTests () = (fun () -> FlatList.Parallel.minBy abs FlatList.empty |> ignore) "FlatList.Parallel.minBy of an empty flat list must throw as Array.Parallel.minBy does" - [] + [] member _.``reduce combines the elements as the sequential reduce does for an associative and commutative function`` () = forEach nonEmptyInputs @@ -500,7 +500,7 @@ type FlatListParallelTests () = ) ) - [] + [] member _.``reduce throws for an empty flat list as Array.Parallel does`` () = ExceptionAssert.same id @@ -508,7 +508,7 @@ type FlatListParallelTests () = (fun () -> FlatList.Parallel.reduce (+) FlatList.empty |> ignore) "FlatList.Parallel.reduce of an empty flat list must throw as Array.Parallel.reduce does" - [] + [] member _.``reduceBy combines the projections as Array.Parallel does`` () = // The sequential module has no reduceBy; it is reduce after map there forEach @@ -529,7 +529,7 @@ type FlatListParallelTests () = ) ) - [] + [] member _.``reduceBy throws for an empty flat list as Array.Parallel does`` () = ExceptionAssert.same id @@ -540,7 +540,7 @@ type FlatListParallelTests () = ) "FlatList.Parallel.reduceBy of an empty flat list must throw as Array.Parallel.reduceBy does" - [] + [] member _.``sum adds the elements`` () = forEach allInputs @@ -552,7 +552,7 @@ type FlatListParallelTests () = ) ) - [] + [] member _.``sumBy adds the projections`` () = forEach allInputs @@ -566,7 +566,7 @@ type FlatListParallelTests () = // ---- Partitioning and grouping ---- - [] + [] member _.``groupBy groups the elements as the sequential groupBy does`` () = forEach allInputs @@ -578,7 +578,7 @@ type FlatListParallelTests () = ) ) - [] + [] member _.``groupBy groups the elements of a null key`` () = let projection (item : int) : string | null = if item < 0 then null else string (item % 3) @@ -592,7 +592,7 @@ type FlatListParallelTests () = ) ) - [] + [] member _.``groupBy puts the NaN keys of type float and float32 in one group as Array.Parallel does`` () = // Array.Parallel.groupBy compares these keys with ER semantics, while the sequential Array.groupBy makes a group of // each NaN. The keys are compared as text, because NaN is not equal to itself. @@ -612,7 +612,7 @@ type FlatListParallelTests () = check [| nan; 1.0; nan; 2.0; nan |] check [| nanf; 1.0f; nanf; 2.0f; nanf |] - [] + [] member _.``partition splits the elements in their order`` () = forEach allInputs @@ -634,7 +634,7 @@ type FlatListParallelTests () = ) ) - [] + [] member _.``partitionWith splits the elements by the choice in their order`` () = let partitioner (item : int) = if item > 0 then @@ -663,7 +663,7 @@ type FlatListParallelTests () = // ---- Sorting ---- - [] + [] member _.``sort orders the elements ascending`` () = forEach allInputs @@ -675,7 +675,7 @@ type FlatListParallelTests () = ) ) - [] + [] member _.``sortBy orders the elements by the projection`` () = forEach allInputs @@ -687,7 +687,7 @@ type FlatListParallelTests () = ) ) - [] + [] member _.``sortByDescending orders the elements by the projection descending`` () = forEach allInputs @@ -699,7 +699,7 @@ type FlatListParallelTests () = ) ) - [] + [] member _.``sortDescending orders the elements descending`` () = forEach allInputs @@ -711,7 +711,7 @@ type FlatListParallelTests () = ) ) - [] + [] member _.``sortWith orders the elements with the comparison function`` () = let descending (left : int) (right : int) = compare right left @@ -725,7 +725,7 @@ type FlatListParallelTests () = ) ) - [] + [] member _.``the sorting functions leave the shared array of the input unchanged`` () = let array = FlatList.ofList large @@ -739,7 +739,7 @@ type FlatListParallelTests () = // ---- Default flat lists ---- - [] + [] member _.``functions reject a default flat list as Array.Parallel rejects a null array`` () = let firstChoice (item : int) : Choice = Choice1Of2 item @@ -866,7 +866,7 @@ type FlatListParallelTests () = for struct (name, oracle, actual) in cases do ExceptionAssert.same id oracle actual $"FlatList.Parallel.%s{name} must reject a default flat list" - [] + [] member _.``sum and sumBy reject a default flat list where Array.Parallel dereferences a null array`` () = // Array.Parallel.sum and sumBy throw NullReferenceException for a null array; their documentation promises // ArgumentNullException, which a default flat list throws diff --git a/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelTupleTests.fs b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelTupleTests.fs index 0ed9c904..3da9500f 100644 --- a/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelTupleTests.fs +++ b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelTupleTests.fs @@ -20,7 +20,7 @@ type FlatListParallelTupleTests () = static let isPositive (item : int) = item > 0 - [] + [] member _.``zip returns reference tuples`` () = for struct (first, second) in [ struct (array, other); struct (large, FlatList.rev large) ] do let pairs : FlatList = FlatList.Parallel.zip first second @@ -31,7 +31,7 @@ type FlatListParallelTupleTests () = $"Tuple.FlatList.Parallel.zip of %d{first.Length} elements must zip as List.zip does" ) - [] + [] member _.``zip throws for different lengths as the default does`` () = ExceptionAssert.same id @@ -42,7 +42,7 @@ type FlatListParallelTupleTests () = (fun () -> FlatList.Parallel.zip array (FlatList.singleton 1) |> ignore) "Tuple.FlatList.Parallel.zip must throw as the default zip does" - [] + [] member _.``groupBy returns reference tuples`` () = for source in [ array; large ] do let groups : FlatList> = FlatList.Parallel.groupBy (fun item -> item % 3) source @@ -60,7 +60,7 @@ type FlatListParallelTupleTests () = $"Tuple.FlatList.Parallel.groupBy of %d{source.Length} elements must group as List.groupBy does" ) - [] + [] member _.``groupBy puts the NaN keys of type float and float32 in one group as Array.Parallel does`` () = // The keys are compared as text, because NaN is not equal to itself let check (source : 'Key array) = @@ -81,7 +81,7 @@ type FlatListParallelTupleTests () = check [| nan; 1.0; nan; 2.0; nan |] check [| nanf; 1.0f; nanf; 2.0f; nanf |] - [] + [] member _.``partition returns a reference tuple`` () = for source in [ array; large ] do let (accepted : FlatList), (rejected : FlatList) = @@ -100,7 +100,7 @@ type FlatListParallelTupleTests () = $"Tuple.FlatList.Parallel.partition of %d{source.Length} elements must return the rejected elements" ) - [] + [] member _.``partitionWith returns a reference tuple`` () = let partitioner (item : int) = if item > 0 then @@ -125,7 +125,7 @@ type FlatListParallelTupleTests () = $"Tuple.FlatList.Parallel.partitionWith of %d{source.Length} elements must return the second choices" ) - [] + [] member _.``the functions without a tuple keep resolving to the default module`` () = let found : int voption = FlatList.Parallel.tryFind (fun item -> item > 6) array Assert.AreEqual(ValueSome 8, found, "FlatList.Parallel.tryFind must resolve to the default module") From 23a517a09ca3e70fb34b4f7613bf0d0b10fd509b Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Sun, 18 Oct 2026 17:04:16 +0200 Subject: [PATCH 10/14] fixup! test: `FlatList.Parallel` against the sequential functions and `Array.Parallel` The tests compare arrays and take their cases as data rows, so every case runs and reports as a test of its own (313 tests in the three classes instead of 64): - The inputs are the cases of `ParallelInput`: MSTest names a row after its values, and the larger input would write ten thousand numbers into the name of each of its rows. `Elements` gives the elements of a case, and each class builds its own larger input. - The predicates and their choosers are the cases of `ParallelPredicate`; `forall` runs each of them as it is and negated. The tables of the default flat lists give their names as rows; the two cases of `zip` are named after the parameter that is default. - The chunks of the parallel loop of `zip` are rows; the check of the longest array on 64 processors is a test of its own instead of a check repeated by every row. - Groupings are compared by their keys and their elements, which a pair would compare by reference. Co-Authored-By: Claude Opus 5.5 --- .../FlatList/FlatListParallelOptionTests.fs | 129 +- .../FlatList/FlatListParallelTests.fs | 1288 +++++++++-------- .../FlatList/FlatListParallelTupleTests.fs | 172 ++- 3 files changed, 832 insertions(+), 757 deletions(-) diff --git a/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelOptionTests.fs b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelOptionTests.fs index 9957a5e7..07f0657d 100644 --- a/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelOptionTests.fs +++ b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelOptionTests.fs @@ -13,10 +13,10 @@ open Microsoft.VisualStudio.TestTools.UnitTesting [] type FlatListParallelOptionTests () = - static let array = FlatList.ofList Inputs.several + static let array = FlatList.ofArray Inputs.Several - /// A flat list long enough for the functions to split their work among several threads. - static let large = FlatList.init 10_000 (fun index -> index % 1_000 - 500) + /// The elements of a flat list long enough for the functions to split their work among several threads. + static let large () = Array.init 10_000 (fun index -> index % 1_000 - 500) static let defaultArray = Unchecked.defaultof> @@ -26,51 +26,13 @@ type FlatListParallelOptionTests () = static let negatedIfNegative (item : int) = if item < 0 then Some (-item) else None - [] - member _.``tryFind returns an option`` () = - for source in [ array; large ] do - let found : int option = FlatList.Parallel.tryFind (isAbove 6) source - - OverrideAssert.optionMatches - (FSharp.Collections.Immutable.FlatList.Parallel.tryFind (isAbove 6) source) - found - $"Option.FlatList.Parallel.tryFind over %d{source.Length} elements must match the default" - - [] - member _.``tryFindIndex returns an option`` () = - for source in [ array; large ] do - let found : int option = FlatList.Parallel.tryFindIndex (isAbove 6) source - - OverrideAssert.optionMatches - (FSharp.Collections.Immutable.FlatList.Parallel.tryFindIndex (isAbove 6) source) - found - $"Option.FlatList.Parallel.tryFindIndex over %d{source.Length} elements must match the default" - - [] - member _.``tryPick takes a chooser of options and returns an option`` () = - for source in [ array; large ] do - let found : int option = FlatList.Parallel.tryPick negatedIfNegative source - - Assert.AreEqual( - Array.tryPick negatedIfNegative (FlatList.toArray source), - found, - $"Option.FlatList.Parallel.tryPick over %d{source.Length} elements must pick as Array.tryPick does" - ) + /// The flat list of the elements of an input. + static let flatListOf (input : ParallelInput) = FlatList.ofArray (input.Elements large) - [] - member _.``choose takes a chooser of options`` () = - for source in [ array; large ] do - let chosen : FlatList = FlatList.Parallel.choose negatedIfNegative source - - Assert.AreEqual( - List.choose negatedIfNegative (FlatList.toList source), - FlatList.toList chosen, - $"Option.FlatList.Parallel.choose over %d{source.Length} elements must choose as List.choose does" - ) - - [] - member _.``the functions reject a default flat list as Array.Parallel rejects a null array`` () = - let cases : struct (string * (unit -> unit) * (unit -> unit)) list = [ + /// The functions that mirror an Array.Parallel function, by name, with that function on a null array and the function + /// on a default flat list. The rows of the test are the names: a function has no readable name in a row. + static let arrayRejections = + [| struct (nameof FlatList.Parallel.choose, (fun () -> Array.Parallel.choose negatedIfNegative nullArray |> ignore), (fun () -> @@ -92,24 +54,79 @@ type FlatListParallelOptionTests () = FlatList.Parallel.tryPick negatedIfNegative defaultArray |> ignore )) - ] + |] + |> Array.map (fun struct (name, oracle, actual) -> name, struct (oracle, actual)) + |> Map.ofArray + + /// The input with several elements of the contracts and the larger input. + static member Sources = seq { + ParallelInput.Several + ParallelInput.Large + } + + /// The names of the functions that mirror an Array.Parallel function, as the rows of their test. + static member ArrayRejections = arrayRejections.Keys + + [] + member _.``tryFind returns an option`` (input : ParallelInput) = + let source = flatListOf input + let found : int option = FlatList.Parallel.tryFind (isAbove 6) source + + OverrideAssert.optionMatches + (FSharp.Collections.Immutable.FlatList.Parallel.tryFind (isAbove 6) source) + found + "Option.FlatList.Parallel.tryFind must match the default" + + [] + member _.``tryFindIndex returns an option`` (input : ParallelInput) = + let source = flatListOf input + let found : int option = FlatList.Parallel.tryFindIndex (isAbove 6) source + + OverrideAssert.optionMatches + (FSharp.Collections.Immutable.FlatList.Parallel.tryFindIndex (isAbove 6) source) + found + "Option.FlatList.Parallel.tryFindIndex must match the default" + + [] + member _.``tryPick takes a chooser of options and returns an option`` (input : ParallelInput) = + let source = flatListOf input + let found : int option = FlatList.Parallel.tryPick negatedIfNegative source + + Assert.AreEqual( + Array.tryPick negatedIfNegative (FlatList.toArray source), + found, + "Option.FlatList.Parallel.tryPick must pick as Array.tryPick does" + ) + + [] + member _.``choose takes a chooser of options`` (input : ParallelInput) = + let source = flatListOf input + let chosen : FlatList = FlatList.Parallel.choose negatedIfNegative source + + CollectionAssert.AreEqual ( + Array.choose negatedIfNegative (FlatList.toArray source), + FlatList.toArray chosen, + "Option.FlatList.Parallel.choose must choose as Array.choose does" + ) - for struct (name, oracle, actual) in cases do - ExceptionAssert.same id oracle actual $"Option.FlatList.Parallel.%s{name} must reject a default flat list" + [] + member _.``the functions reject a default flat list as Array.Parallel rejects a null array`` (name : string) = + let struct (oracle, actual) = arrayRejections[name] + ExceptionAssert.same id oracle actual $"Option.FlatList.Parallel.%s{name} must reject a default flat list" [] member _.``the functions without an option keep resolving to the default module`` () = let mapped : FlatList = FlatList.Parallel.map (fun item -> item * 2) array let zipped : FlatList = FlatList.Parallel.zip array array - Assert.AreEqual( - List.map (fun item -> item * 2) Inputs.several, - FlatList.toList mapped, + CollectionAssert.AreEqual ( + Array.map (fun item -> item * 2) Inputs.Several, + FlatList.toArray mapped, "FlatList.Parallel.map must resolve to the default module" ) - Assert.AreEqual<(int * int) list>( - List.zip Inputs.several Inputs.several, - FlatList.toList zipped, + CollectionAssert.AreEqual ( + Array.zip Inputs.Several Inputs.Several, + FlatList.toArray zipped, "FlatList.Parallel.zip must resolve to the Tuple module" ) diff --git a/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelTests.fs b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelTests.fs index d001672d..c0723696 100644 --- a/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelTests.fs +++ b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelTests.fs @@ -5,6 +5,65 @@ open System.Collections.Concurrent open FSharp.Collections.Immutable open Microsoft.VisualStudio.TestTools.UnitTesting +/// The inputs of the tests of FlatList.Parallel, as the cases of their rows. +/// +/// MSTest names a row after its values, and the larger input would write ten thousand numbers into the name of each of +/// its rows, so a row holds a case and the test takes the elements of the case. +/// +[] +type ParallelInput = + /// The input without elements of the contracts. + | Empty + /// The input with one element of the contracts. + | Singleton + /// The input with several elements of the contracts. + | Several + /// An input long enough for the functions to split their work among several threads. + | Large + + /// The elements of the input; the test class builds the elements of the larger input. + member this.Elements (large : unit -> int array) = + match this with + | ParallelInput.Empty -> Inputs.Empty + | ParallelInput.Singleton -> Inputs.Singleton + | ParallelInput.Several -> Inputs.Several + | ParallelInput.Large -> large () + +/// +/// The predicates of the tests of FlatList.Parallel, with the choosers that return a value for the same elements, +/// as the cases of their rows: matches at the start, matches late and rare in the larger input, and no match. +/// +/// +/// MSTest names a row after its values, and a function has no readable name, so a row holds a case and the test takes +/// the function of the case. +/// +[] +type ParallelPredicate = + /// The even elements. + | IsEven + /// The negative elements. + | IsNegative + /// The elements above 900, which come late and rarely in the larger input. + | IsAboveNineHundred + /// No element. + | Never + + /// Tells whether an element satisfies the predicate. + member this.Matches (item : int) = + match this with + | ParallelPredicate.IsEven -> item % 2 = 0 + | ParallelPredicate.IsNegative -> item < 0 + | ParallelPredicate.IsAboveNineHundred -> item > 900 + | ParallelPredicate.Never -> false + + /// A chooser that returns a value for the elements that satisfy the predicate. + member this.Choose (item : int) = + match this with + | ParallelPredicate.IsEven -> if item % 2 = 0 then ValueSome (item / 2) else ValueNone + | ParallelPredicate.IsNegative -> if item < 0 then ValueSome (-item) else ValueNone + | ParallelPredicate.IsAboveNineHundred -> if item > 900 then ValueSome (item * 2) else ValueNone + | ParallelPredicate.Never -> ValueNone + /// The `FlatList.Parallel` module, compared with the sequential functions of `FlatList` (ADR 0001, D11). /// Every function runs on the inputs of the contracts and on a larger input, which is long enough for the functions to split /// their work among several threads. Results whose order the module leaves open are compared after sorting, and exceptions @@ -18,61 +77,230 @@ type FlatListParallelTests () = /// Numbers from -1000 to 999 in a scrambled order, each about five times. 7919 and 10 007 are primes, so the products /// modulo 10 007 do not repeat before the last remainder folds them onto 2000 values. - static let large = List.init 10_000 (fun index -> index * 7_919 % 10_007 % 2_000 - 1_000) + static let large () = Array.init 10_000 (fun index -> index * 7_919 % 10_007 % 2_000 - 1_000) /// The inputs of the contracts and the larger input. - static let allInputs = [ yield! Inputs.all; large ] + static let allInputs = [| ParallelInput.Empty; ParallelInput.Singleton; ParallelInput.Several; ParallelInput.Large |] - /// The inputs with at least one element. - static let nonEmptyInputs = [ yield! Inputs.nonEmpty; large ] - - /// Runs a check on the flat list of every given input, with a description of the input for the messages. - static let forEach (inputs : int list list) (check : FlatList -> string -> unit) = - for input in inputs do - let description = - if input.Length > Inputs.several.Length then - $"%d{input.Length} elements" - else - $"%A{input}" - - check (FlatList.ofList input) description - - /// The predicates of the tests: matches at the start, matches late and rare in the larger input, and no match. - static let predicates : (int -> bool) list = [ - (fun item -> item % 2 = 0) - (fun item -> item < 0) - (fun item -> item > 900) - (fun _ -> false) - ] - - /// The choosers of the tests, which return a value for the same elements as the predicates. - static let choosers : (int -> int voption) list = [ - (fun item -> if item % 2 = 0 then ValueSome (item / 2) else ValueNone) - (fun item -> if item < 0 then ValueSome (-item) else ValueNone) - (fun item -> if item > 900 then ValueSome (item * 2) else ValueNone) - (fun _ -> ValueNone) - ] + /// The predicates of the tests. + static let predicates = [| + ParallelPredicate.IsEven + ParallelPredicate.IsNegative + ParallelPredicate.IsAboveNineHundred + ParallelPredicate.Never + |] + + /// The flat list of the elements of an input. + static let flatListOf (input : ParallelInput) = FlatList.ofArray (input.Elements large) /// Negation is injective, so only equal elements share a key, and the result of a sort does not depend on its stability. static let negate (item : int) = -item - /// The groups with their keys and the elements of every key sorted, for the comparison of groupings whose order is open. + /// The keys of the groups and the elements of every key, sorted, for the comparison of groupings whose order is open. static let normalized (groups : FlatList)>) = - groups - |> Seq.map (fun struct (key, items) -> struct (key, items |> Seq.sort |> Seq.toList)) - |> Seq.sortBy (fun struct (key, _) -> key) - |> Seq.toList + let sorted = + groups + |> Seq.map (fun struct (key, items) -> struct (key, items |> Seq.sort |> Seq.toArray)) + |> Seq.sortBy (fun struct (key, _) -> key) + |> Seq.toArray + + struct (sorted |> Array.map (fun struct (key, _) -> key), sorted |> Array.map (fun struct (_, items) -> items)) + + static let firstChoice (item : int) : Choice = Choice1Of2 item + + /// The functions that mirror an Array.Parallel function, by name, with that function on a null array and the function + /// on a default flat list. The rows of the test are the names: a function has no readable name in a row. + static let arrayRejections = + [| + struct (nameof FlatList.Parallel.average, + (fun () -> + Array.Parallel.average (Unchecked.defaultof) + |> ignore + ), + (fun () -> + FlatList.Parallel.average (Unchecked.defaultof>) + |> ignore + )) + struct (nameof FlatList.Parallel.averageBy, + (fun () -> Array.Parallel.averageBy float nullArray |> ignore), + (fun () -> FlatList.Parallel.averageBy float defaultArray |> ignore)) + struct (nameof FlatList.Parallel.choose, + (fun () -> Array.Parallel.choose Some nullArray |> ignore), + (fun () -> FlatList.Parallel.choose ValueSome defaultArray |> ignore)) + struct (nameof FlatList.Parallel.collect, + (fun () -> Array.Parallel.collect Array.singleton nullArray |> ignore), + (fun () -> + FlatList.Parallel.collect FlatList.singleton defaultArray + |> ignore + )) + struct (nameof FlatList.Parallel.exists, + (fun () -> Array.Parallel.exists ((=) 1) nullArray |> ignore), + (fun () -> FlatList.Parallel.exists ((=) 1) defaultArray |> ignore)) + struct (nameof FlatList.Parallel.filter, + (fun () -> Array.Parallel.filter ((=) 1) nullArray |> ignore), + (fun () -> FlatList.Parallel.filter ((=) 1) defaultArray |> ignore)) + struct (nameof FlatList.Parallel.forall, + (fun () -> Array.Parallel.forall ((=) 1) nullArray |> ignore), + (fun () -> FlatList.Parallel.forall ((=) 1) defaultArray |> ignore)) + struct (nameof FlatList.Parallel.groupBy, + (fun () -> Array.Parallel.groupBy id nullArray |> ignore), + (fun () -> FlatList.Parallel.groupBy id defaultArray |> ignore)) + struct (nameof FlatList.Parallel.iter, + (fun () -> Array.Parallel.iter ignore nullArray), + (fun () -> FlatList.Parallel.iter ignore defaultArray)) + struct (nameof FlatList.Parallel.iteri, + (fun () -> Array.Parallel.iteri (fun _ _ -> ()) nullArray), + (fun () -> FlatList.Parallel.iteri (fun _ _ -> ()) defaultArray)) + struct (nameof FlatList.Parallel.map, + (fun () -> Array.Parallel.map id nullArray |> ignore), + (fun () -> FlatList.Parallel.map id defaultArray |> ignore)) + struct (nameof FlatList.Parallel.mapi, + (fun () -> Array.Parallel.mapi (fun _ item -> item) nullArray |> ignore), + (fun () -> + FlatList.Parallel.mapi (fun _ item -> item) defaultArray + |> ignore + )) + struct (nameof FlatList.Parallel.max, + (fun () -> Array.Parallel.max nullArray |> ignore), + (fun () -> FlatList.Parallel.max defaultArray |> ignore)) + struct (nameof FlatList.Parallel.maxBy, + (fun () -> Array.Parallel.maxBy abs nullArray |> ignore), + (fun () -> FlatList.Parallel.maxBy abs defaultArray |> ignore)) + struct (nameof FlatList.Parallel.min, + (fun () -> Array.Parallel.min nullArray |> ignore), + (fun () -> FlatList.Parallel.min defaultArray |> ignore)) + struct (nameof FlatList.Parallel.minBy, + (fun () -> Array.Parallel.minBy abs nullArray |> ignore), + (fun () -> FlatList.Parallel.minBy abs defaultArray |> ignore)) + struct (nameof FlatList.Parallel.partition, + (fun () -> Array.Parallel.partition ((=) 1) nullArray |> ignore), + (fun () -> FlatList.Parallel.partition ((=) 1) defaultArray |> ignore)) + struct (nameof FlatList.Parallel.partitionWith, + (fun () -> Array.Parallel.partitionWith firstChoice nullArray |> ignore), + (fun () -> + FlatList.Parallel.partitionWith firstChoice defaultArray + |> ignore + )) + struct (nameof FlatList.Parallel.reduce, + (fun () -> Array.Parallel.reduce (+) nullArray |> ignore), + (fun () -> FlatList.Parallel.reduce (+) defaultArray |> ignore)) + struct (nameof FlatList.Parallel.reduceBy, + (fun () -> Array.Parallel.reduceBy abs (+) nullArray |> ignore), + (fun () -> FlatList.Parallel.reduceBy abs (+) defaultArray |> ignore)) + struct (nameof FlatList.Parallel.sort, + (fun () -> Array.Parallel.sort nullArray |> ignore), + (fun () -> FlatList.Parallel.sort defaultArray |> ignore)) + struct (nameof FlatList.Parallel.sortBy, + (fun () -> Array.Parallel.sortBy negate nullArray |> ignore), + (fun () -> FlatList.Parallel.sortBy negate defaultArray |> ignore)) + struct (nameof FlatList.Parallel.sortByDescending, + (fun () -> Array.Parallel.sortByDescending negate nullArray |> ignore), + (fun () -> + FlatList.Parallel.sortByDescending negate defaultArray + |> ignore + )) + struct (nameof FlatList.Parallel.sortDescending, + (fun () -> Array.Parallel.sortDescending nullArray |> ignore), + (fun () -> FlatList.Parallel.sortDescending defaultArray |> ignore)) + struct (nameof FlatList.Parallel.sortWith, + (fun () -> Array.Parallel.sortWith compare nullArray |> ignore), + (fun () -> FlatList.Parallel.sortWith compare defaultArray |> ignore)) + struct (nameof FlatList.Parallel.tryFind, + (fun () -> Array.Parallel.tryFind ((=) 1) nullArray |> ignore), + (fun () -> FlatList.Parallel.tryFind ((=) 1) defaultArray |> ignore)) + struct (nameof FlatList.Parallel.tryFindIndex, + (fun () -> Array.Parallel.tryFindIndex ((=) 1) nullArray |> ignore), + (fun () -> + FlatList.Parallel.tryFindIndex ((=) 1) defaultArray + |> ignore + )) + struct (nameof FlatList.Parallel.tryPick, + (fun () -> Array.Parallel.tryPick Some nullArray |> ignore), + (fun () -> FlatList.Parallel.tryPick ValueSome defaultArray |> ignore)) + // zip takes two flat lists; each of them is a case of its own, named after the parameter that is default + struct ($"%s{nameof FlatList.Parallel.zip} (array1)", + (fun () -> Array.Parallel.zip nullArray [| 1 |] |> ignore), + (fun () -> + FlatList.Parallel.zip defaultArray (FlatList.singleton 1) + |> ignore + )) + struct ($"%s{nameof FlatList.Parallel.zip} (array2)", + (fun () -> Array.Parallel.zip [| 1 |] nullArray |> ignore), + (fun () -> + FlatList.Parallel.zip (FlatList.singleton 1) defaultArray + |> ignore + )) + |] + |> Array.map (fun struct (name, oracle, actual) -> name, struct (oracle, actual)) + |> Map.ofArray + + /// The functions that reject a default flat list where Array.Parallel dereferences a null array, by name. + static let dereferences : Map unit> = + Map.ofArray [| + nameof FlatList.Parallel.sum, (fun () -> FlatList.Parallel.sum defaultArray |> ignore) + nameof FlatList.Parallel.sumBy, (fun () -> FlatList.Parallel.sumBy abs defaultArray |> ignore) + |] + + /// The inputs of the contracts and the larger input. + static member AllInputs = Seq.ofArray allInputs + + /// The inputs with at least one element. + static member NonEmptyInputs = + allInputs + |> Seq.filter (fun input -> input <> ParallelInput.Empty) + + /// Every input with each predicate. + static member InputsAndPredicates = seq { + for input in allInputs do + for predicate in predicates do + struct (input, predicate) + } + + /// Every input with each predicate, as it is and negated. + static member InputsAndForallPredicates = seq { + for input in allInputs do + for predicate in predicates do + for negated in [| false; true |] do + struct (input, predicate, negated) + } + + /// The counts of init: none, one, the length of the input with several elements and the length of the larger input. + static member InitCounts = seq { + 0 + 1 + Inputs.Several.Length + (large ()).Length + } + + /// Two inputs of different lengths, in both orders. + static member DifferentLengths = seq { + struct (Inputs.Singleton, Inputs.Several) + struct (Inputs.Several, Inputs.Singleton) + } + + /// The number of processors, the length of the arrays and the chunks of the parallel loop of zip. + static member ZipChunks = seq { + struct (4, 0, Array.empty) + struct (4, 255, [| struct (0, 255) |]) + struct (3, 1_000, [| struct (0, 334); struct (334, 668); struct (668, 1_000) |]) + struct (2, 1_500_000_000, [| struct (0, 750_000_000); struct (750_000_000, 1_500_000_000) |]) + } + + /// The names of the functions that mirror an Array.Parallel function, as the rows of their test. + static member ArrayRejections = arrayRejections.Keys + + /// The names of the functions that reject a default flat list where Array.Parallel dereferences a null array. + static member Dereferences = dereferences.Keys // ---- Creation ---- - [] - member _.``init creates the elements as the sequential init does`` () = - for count in [ 0; 1; Inputs.several.Length; large.Length ] do - Assert.AreEqual( - FlatList.toList (FlatList.init count (fun index -> index * 3)), - FlatList.toList (FlatList.Parallel.init count (fun index -> index * 3)), - $"FlatList.Parallel.init %d{count}" - ) + [] + member _.``init creates the elements as the sequential init does`` (count : int) = + CollectionAssert.AreEqual ( + FlatList.toArray (FlatList.init count (fun index -> index * 3)), + FlatList.toArray (FlatList.Parallel.init count (fun index -> index * 3)), + "FlatList.Parallel.init must create the elements as the sequential init does" + ) [] member _.``init throws for a negative count as Array.Parallel does`` () = @@ -84,103 +312,94 @@ type FlatListParallelTests () = // ---- Search ---- - [] - member _.``exists answers as the sequential exists does`` () = - forEach - allInputs - (fun array description -> - for predicate in predicates do - Assert.AreEqual( - FlatList.exists predicate array, - FlatList.Parallel.exists predicate array, - $"FlatList.Parallel.exists over %s{description}" - ) - ) + [] + member _.``exists answers as the sequential exists does`` (input : ParallelInput, predicate : ParallelPredicate) = + let array = flatListOf input - [] - member _.``forall answers as the sequential forall does`` () = - forEach - allInputs - (fun array description -> - for predicate in predicates do - let negated item = not (predicate item) - - for test in [ predicate; negated ] do - Assert.AreEqual( - FlatList.forall test array, - FlatList.Parallel.forall test array, - $"FlatList.Parallel.forall over %s{description}" - ) - ) + Assert.AreEqual( + FlatList.exists predicate.Matches array, + FlatList.Parallel.exists predicate.Matches array, + "FlatList.Parallel.exists must answer as the sequential exists does" + ) - [] - member _.``tryFind returns the first element that satisfies the predicate`` () = - forEach - allInputs - (fun array description -> - for predicate in predicates do - Assert.AreEqual( - FlatList.tryFind predicate array, - FlatList.Parallel.tryFind predicate array, - $"FlatList.Parallel.tryFind in %s{description}" - ) - ) + [] + member _.``forall answers as the sequential forall does`` + (input : ParallelInput, predicate : ParallelPredicate, negated : bool) + = + let array = flatListOf input + let test item = + if negated then + not (predicate.Matches item) + else + predicate.Matches item - [] - member _.``tryFindIndex returns the index of the first element that satisfies the predicate`` () = - forEach - allInputs - (fun array description -> - for predicate in predicates do - Assert.AreEqual( - FlatList.tryFindIndex predicate array, - FlatList.Parallel.tryFindIndex predicate array, - $"FlatList.Parallel.tryFindIndex in %s{description}" - ) - ) + Assert.AreEqual( + FlatList.forall test array, + FlatList.Parallel.forall test array, + "FlatList.Parallel.forall must answer as the sequential forall does" + ) - [] - member _.``tryPick returns the value for the first element that the chooser returns a value for`` () = - forEach - allInputs - (fun array description -> - for chooser in choosers do - Assert.AreEqual( - FlatList.tryPick chooser array, - FlatList.Parallel.tryPick chooser array, - $"FlatList.Parallel.tryPick in %s{description}" - ) - ) + [] + member _.``tryFind returns the first element that satisfies the predicate`` + (input : ParallelInput, predicate : ParallelPredicate) + = + let array = flatListOf input + + Assert.AreEqual( + FlatList.tryFind predicate.Matches array, + FlatList.Parallel.tryFind predicate.Matches array, + "FlatList.Parallel.tryFind must return the first element that satisfies the predicate" + ) + + [] + member _.``tryFindIndex returns the index of the first element that satisfies the predicate`` + (input : ParallelInput, predicate : ParallelPredicate) + = + let array = flatListOf input + + Assert.AreEqual( + FlatList.tryFindIndex predicate.Matches array, + FlatList.Parallel.tryFindIndex predicate.Matches array, + "FlatList.Parallel.tryFindIndex must return the index of the first element that satisfies the predicate" + ) + + [] + member _.``tryPick returns the value for the first element that the chooser returns a value for`` + (input : ParallelInput, predicate : ParallelPredicate) + = + let array = flatListOf input + + Assert.AreEqual( + FlatList.tryPick predicate.Choose array, + FlatList.Parallel.tryPick predicate.Choose array, + "FlatList.Parallel.tryPick must return the value for the first element that the chooser returns a value for" + ) // ---- Transformation ---- - [] - member _.``choose keeps the chosen values in the order of their elements`` () = - forEach - allInputs - (fun array description -> - for chooser in choosers do - Assert.AreEqual( - FlatList.toList (FlatList.choose chooser array), - FlatList.toList (FlatList.Parallel.choose chooser array), - $"FlatList.Parallel.choose of %s{description}" - ) - ) + [] + member _.``choose keeps the chosen values in the order of their elements`` + (input : ParallelInput, predicate : ParallelPredicate) + = + let array = flatListOf input - [] - member _.``collect concatenates the flat lists in the order of the elements`` () = + CollectionAssert.AreEqual ( + FlatList.toArray (FlatList.choose predicate.Choose array), + FlatList.toArray (FlatList.Parallel.choose predicate.Choose array), + "FlatList.Parallel.choose must keep the chosen values in the order of their elements" + ) + + [] + member _.``collect concatenates the flat lists in the order of the elements`` (input : ParallelInput) = + let array = flatListOf input // The flat lists of the mapping have zero, one or two elements let mapping (item : int) = FlatList.replicate (abs item % 3) item - forEach - allInputs - (fun array description -> - Assert.AreEqual( - FlatList.toList (FlatList.collect mapping array), - FlatList.toList (FlatList.Parallel.collect mapping array), - $"FlatList.Parallel.collect of %s{description}" - ) - ) + CollectionAssert.AreEqual ( + FlatList.toArray (FlatList.collect mapping array), + FlatList.toArray (FlatList.Parallel.collect mapping array), + "FlatList.Parallel.collect must concatenate the flat lists in the order of the elements" + ) [] member _.``collect throws ArgumentNullException for a default flat list of the mapping`` () = @@ -200,92 +419,75 @@ type FlatListParallelTests () = "FlatList.Parallel.collect must name the mapping as the source of the default flat list" ) - [] - member _.``filter keeps the elements that satisfy the predicate in their order`` () = - forEach - allInputs - (fun array description -> - for predicate in predicates do - Assert.AreEqual( - FlatList.toList (FlatList.filter predicate array), - FlatList.toList (FlatList.Parallel.filter predicate array), - $"FlatList.Parallel.filter of %s{description}" - ) - ) + [] + member _.``filter keeps the elements that satisfy the predicate in their order`` + (input : ParallelInput, predicate : ParallelPredicate) + = + let array = flatListOf input - [] - member _.``map transforms every element in its order`` () = - forEach - allInputs - (fun array description -> - Assert.AreEqual( - FlatList.toList (FlatList.map string array), - FlatList.toList (FlatList.Parallel.map string array), - $"FlatList.Parallel.map of %s{description}" - ) - ) + CollectionAssert.AreEqual ( + FlatList.toArray (FlatList.filter predicate.Matches array), + FlatList.toArray (FlatList.Parallel.filter predicate.Matches array), + "FlatList.Parallel.filter must keep the elements that satisfy the predicate in their order" + ) - [] - member _.``mapi transforms every element with its index`` () = + [] + member _.``map transforms every element in its order`` (input : ParallelInput) = + let array = flatListOf input + + CollectionAssert.AreEqual ( + FlatList.toArray (FlatList.map string array), + FlatList.toArray (FlatList.Parallel.map string array), + "FlatList.Parallel.map must transform every element in its order" + ) + + [] + member _.``mapi transforms every element with its index`` (input : ParallelInput) = + let array = flatListOf input let mapping (index : int) (item : int) = struct (index, item * 3) - forEach - allInputs - (fun array description -> - Assert.AreEqual( - FlatList.toList (FlatList.mapi mapping array), - FlatList.toList (FlatList.Parallel.mapi mapping array), - $"FlatList.Parallel.mapi of %s{description}" - ) - ) + CollectionAssert.AreEqual ( + FlatList.toArray (FlatList.mapi mapping array), + FlatList.toArray (FlatList.Parallel.mapi mapping array), + "FlatList.Parallel.mapi must transform every element with its index" + ) - [] - member _.``zip pairs the elements at the same index`` () = - forEach - allInputs - (fun array description -> - let other = FlatList.rev array - - Assert.AreEqual( - FlatList.toList (FlatList.zip array other), - FlatList.toList (FlatList.Parallel.zip array other), - $"FlatList.Parallel.zip of %s{description}" - ) - ) + [] + member _.``zip pairs the elements at the same index`` (input : ParallelInput) = + let array = flatListOf input + let other = FlatList.rev array - [] - member _.``zip throws for different lengths as Array.Parallel does`` () = - let short = List.toArray Inputs.singleton - let long = List.toArray Inputs.several - - for struct (first, second) in [ struct (short, long); struct (long, short) ] do - ExceptionAssert.same - id - (fun () -> Array.Parallel.zip first second |> ignore) - (fun () -> - FlatList.Parallel.zip (FlatList.ofArray first) (FlatList.ofArray second) - |> ignore - ) - $"FlatList.Parallel.zip of %d{first.Length} and %d{second.Length} elements must throw as Array.Parallel.zip does" + CollectionAssert.AreEqual ( + FlatList.toArray (FlatList.zip array other), + FlatList.toArray (FlatList.Parallel.zip array other), + "FlatList.Parallel.zip must pair the elements at the same index" + ) - [] - member _.``zip divides every array length into chunks without overflow`` () = + [] + member _.``zip throws for different lengths as Array.Parallel does`` (first : int array, second : int array) = + ExceptionAssert.same + id + (fun () -> Array.Parallel.zip first second |> ignore) + (fun () -> + FlatList.Parallel.zip (FlatList.ofArray first) (FlatList.ofArray second) + |> ignore + ) + "FlatList.Parallel.zip of flat lists of different lengths must throw as Array.Parallel.zip does" + + [] + member _.``zip divides every array length into chunks without overflow`` + (processors : int, length : int, expected : struct (int * int) array) + = // The chunks of the parallel loop of zip are checked directly: arrays of these lengths do not fit into the memory of // a test run. The length plus the chunk length overflowed for 1.5 billion elements, so the loop did nothing. - let cases = [ - struct (4, 0, []) - struct (4, 255, [ struct (0, 255) ]) - struct (3, 1_000, [ struct (0, 334); struct (334, 668); struct (668, 1_000) ]) - struct (2, 1_500_000_000, [ struct (0, 750_000_000); struct (750_000_000, 1_500_000_000) ]) - ] - - for struct (processors, length, expected) in cases do - Assert.AreEqual( - expected, - ParallelArrays.chunks processors length |> Array.toList, - $"The chunks of %d{length} elements on %d{processors} processors" - ) + CollectionAssert.AreEqual ( + expected, + ParallelArrays.chunks processors length, + "The chunks must divide the length among the processors without overflow" + ) + [] + member _.``zip divides the longest array into adjacent chunks that end at its length`` () = // On 64 processors, the first index plus the chunk length of the last chunk of the longest array is more than // Int32.MaxValue, so the chunk must be cut to the length before the addition let bounds = ParallelArrays.chunks 64 Array.MaxLength @@ -308,20 +510,16 @@ type FlatListParallelTests () = // ---- Folding ---- - [] - member _.``average returns the average of the elements`` () = + [] + member _.``average returns the average of the elements`` (input : ParallelInput) = // Whole numbers add up exactly in any order, so the parallel sum is the sequential one - forEach - nonEmptyInputs - (fun array description -> - let floats = FlatList.map float array - - Assert.AreEqual( - FlatList.average floats, - FlatList.Parallel.average floats, - $"FlatList.Parallel.average of %s{description}" - ) - ) + let floats = FlatList.map float (flatListOf input) + + Assert.AreEqual( + FlatList.average floats, + FlatList.Parallel.average floats, + "FlatList.Parallel.average must return the average of the elements" + ) [] member _.``average throws for an empty flat list as Array.Parallel does`` () = @@ -331,17 +529,15 @@ type FlatListParallelTests () = (fun () -> FlatList.Parallel.average FlatList.empty |> ignore) "FlatList.Parallel.average of an empty flat list must throw as Array.Parallel.average does" - [] - member _.``averageBy returns the average of the projections`` () = - forEach - nonEmptyInputs - (fun array description -> - Assert.AreEqual( - FlatList.averageBy float array, - FlatList.Parallel.averageBy float array, - $"FlatList.Parallel.averageBy of %s{description}" - ) - ) + [] + member _.``averageBy returns the average of the projections`` (input : ParallelInput) = + let array = flatListOf input + + Assert.AreEqual( + FlatList.averageBy float array, + FlatList.Parallel.averageBy float array, + "FlatList.Parallel.averageBy must return the average of the projections" + ) [] member _.``averageBy throws for an empty flat list as Array.Parallel does`` () = @@ -354,49 +550,40 @@ type FlatListParallelTests () = ) "FlatList.Parallel.averageBy of an empty flat list must throw as Array.Parallel.averageBy does" - [] - member _.``iter visits every element once`` () = - forEach - allInputs - (fun array description -> - let visited = ConcurrentQueue() - FlatList.Parallel.iter visited.Enqueue array - - Assert.AreEqual( - array |> Seq.sort |> Seq.toList, - visited |> Seq.sort |> Seq.toList, - $"FlatList.Parallel.iter over %s{description}" - ) - ) + [] + member _.``iter visits every element once`` (input : ParallelInput) = + let array = flatListOf input + let visited = ConcurrentQueue() + FlatList.Parallel.iter visited.Enqueue array - [] - member _.``iteri visits every element once with its index`` () = - forEach - allInputs - (fun array description -> - let visited = ConcurrentQueue() - FlatList.Parallel.iteri (fun index item -> visited.Enqueue (struct (index, item))) array - - Assert.AreEqual( - FlatList.toList (FlatList.indexed array), - visited - |> Seq.sortBy (fun struct (index, _) -> index) - |> Seq.toList, - $"FlatList.Parallel.iteri over %s{description}" - ) - ) + CollectionAssert.AreEqual ( + array |> Seq.sort |> Seq.toArray, + visited |> Seq.sort |> Seq.toArray, + "FlatList.Parallel.iter must visit every element once" + ) - [] - member _.``max returns the greatest element`` () = - forEach - nonEmptyInputs - (fun array description -> - Assert.AreEqual( - FlatList.max array, - FlatList.Parallel.max array, - $"FlatList.Parallel.max of %s{description}" - ) - ) + [] + member _.``iteri visits every element once with its index`` (input : ParallelInput) = + let array = flatListOf input + let visited = ConcurrentQueue() + FlatList.Parallel.iteri (fun index item -> visited.Enqueue (struct (index, item))) array + + CollectionAssert.AreEqual ( + FlatList.toArray (FlatList.indexed array), + visited + |> Seq.sortBy (fun struct (index, _) -> index) + |> Seq.toArray, + "FlatList.Parallel.iteri must visit every element once with its index" + ) + + [] + member _.``max returns the greatest element`` (input : ParallelInput) = + let array = flatListOf input + Assert.AreEqual( + FlatList.max array, + FlatList.Parallel.max array, + "FlatList.Parallel.max must return the greatest element" + ) [] member _.``max throws for an empty flat list as Array.Parallel does`` () = @@ -406,26 +593,24 @@ type FlatListParallelTests () = (fun () -> FlatList.Parallel.max FlatList.empty |> ignore) "FlatList.Parallel.max of an empty flat list must throw as Array.Parallel.max does" - [] - member _.``maxBy returns the element with the greatest projection`` () = + [] + member _.``maxBy returns the element with the greatest projection`` (input : ParallelInput) = // In every input, only equal elements share the greatest absolute value, so any of them is the expected one - forEach - nonEmptyInputs - (fun array description -> - Assert.AreEqual( - FlatList.maxBy abs array, - FlatList.Parallel.maxBy abs array, - $"FlatList.Parallel.maxBy of %s{description}" - ) - ) + let array = flatListOf input + + Assert.AreEqual( + FlatList.maxBy abs array, + FlatList.Parallel.maxBy abs array, + "FlatList.Parallel.maxBy must return the element with the greatest projection" + ) [] member _.``maxBy chooses among equal projections as Array.Parallel does`` () = let key (struct (key : int, _ : char)) = key Assert.AreEqual( - Array.Parallel.maxBy key (List.toArray Inputs.pairs), - FlatList.Parallel.maxBy key (FlatList.ofList Inputs.pairs), + Array.Parallel.maxBy key Inputs.Pairs, + FlatList.Parallel.maxBy key (FlatList.ofArray Inputs.Pairs), "FlatList.Parallel.maxBy must choose among equal keys as Array.Parallel.maxBy does" ) @@ -437,17 +622,14 @@ type FlatListParallelTests () = (fun () -> FlatList.Parallel.maxBy abs FlatList.empty |> ignore) "FlatList.Parallel.maxBy of an empty flat list must throw as Array.Parallel.maxBy does" - [] - member _.``min returns the smallest element`` () = - forEach - nonEmptyInputs - (fun array description -> - Assert.AreEqual( - FlatList.min array, - FlatList.Parallel.min array, - $"FlatList.Parallel.min of %s{description}" - ) - ) + [] + member _.``min returns the smallest element`` (input : ParallelInput) = + let array = flatListOf input + Assert.AreEqual( + FlatList.min array, + FlatList.Parallel.min array, + "FlatList.Parallel.min must return the smallest element" + ) [] member _.``min throws for an empty flat list as Array.Parallel does`` () = @@ -457,26 +639,24 @@ type FlatListParallelTests () = (fun () -> FlatList.Parallel.min FlatList.empty |> ignore) "FlatList.Parallel.min of an empty flat list must throw as Array.Parallel.min does" - [] - member _.``minBy returns the element with the smallest projection`` () = + [] + member _.``minBy returns the element with the smallest projection`` (input : ParallelInput) = // In every input, only equal elements share the smallest absolute value, so any of them is the expected one - forEach - nonEmptyInputs - (fun array description -> - Assert.AreEqual( - FlatList.minBy abs array, - FlatList.Parallel.minBy abs array, - $"FlatList.Parallel.minBy of %s{description}" - ) - ) + let array = flatListOf input + + Assert.AreEqual( + FlatList.minBy abs array, + FlatList.Parallel.minBy abs array, + "FlatList.Parallel.minBy must return the element with the smallest projection" + ) [] member _.``minBy chooses among equal projections as Array.Parallel does`` () = let key (struct (key : int, _ : char)) = key Assert.AreEqual( - Array.Parallel.minBy key (List.toArray Inputs.pairs), - FlatList.Parallel.minBy key (FlatList.ofList Inputs.pairs), + Array.Parallel.minBy key Inputs.Pairs, + FlatList.Parallel.minBy key (FlatList.ofArray Inputs.Pairs), "FlatList.Parallel.minBy must choose among equal keys as Array.Parallel.minBy does" ) @@ -488,17 +668,17 @@ type FlatListParallelTests () = (fun () -> FlatList.Parallel.minBy abs FlatList.empty |> ignore) "FlatList.Parallel.minBy of an empty flat list must throw as Array.Parallel.minBy does" - [] - member _.``reduce combines the elements as the sequential reduce does for an associative and commutative function`` () = - forEach - nonEmptyInputs - (fun array description -> - Assert.AreEqual( - FlatList.reduce (+) array, - FlatList.Parallel.reduce (+) array, - $"FlatList.Parallel.reduce of %s{description}" - ) - ) + [] + member _.``reduce combines the elements as the sequential reduce does for an associative and commutative function`` + (input : ParallelInput) + = + let array = flatListOf input + + Assert.AreEqual( + FlatList.reduce (+) array, + FlatList.Parallel.reduce (+) array, + "FlatList.Parallel.reduce must combine the elements as the sequential reduce does" + ) [] member _.``reduce throws for an empty flat list as Array.Parallel does`` () = @@ -508,26 +688,23 @@ type FlatListParallelTests () = (fun () -> FlatList.Parallel.reduce (+) FlatList.empty |> ignore) "FlatList.Parallel.reduce of an empty flat list must throw as Array.Parallel.reduce does" - [] - member _.``reduceBy combines the projections as Array.Parallel does`` () = + [] + member _.``reduceBy combines the projections as Array.Parallel does`` (input : ParallelInput) = // The sequential module has no reduceBy; it is reduce after map there - forEach - nonEmptyInputs - (fun array description -> - let combined = FlatList.Parallel.reduceBy abs (+) array - - Assert.AreEqual( - Array.Parallel.reduceBy abs (+) (FlatList.toArray array), - combined, - $"FlatList.Parallel.reduceBy of %s{description} must combine as Array.Parallel.reduceBy does" - ) - - Assert.AreEqual( - FlatList.map abs array |> FlatList.reduce (+), - combined, - $"FlatList.Parallel.reduceBy of %s{description} must combine as the sequential reduce after map does" - ) - ) + let array = flatListOf input + let combined = FlatList.Parallel.reduceBy abs (+) array + + Assert.AreEqual( + Array.Parallel.reduceBy abs (+) (FlatList.toArray array), + combined, + "FlatList.Parallel.reduceBy must combine as Array.Parallel.reduceBy does" + ) + + Assert.AreEqual( + FlatList.map abs array |> FlatList.reduce (+), + combined, + "FlatList.Parallel.reduceBy must combine as the sequential reduce after map does" + ) [] member _.``reduceBy throws for an empty flat list as Array.Parallel does`` () = @@ -540,194 +717,173 @@ type FlatListParallelTests () = ) "FlatList.Parallel.reduceBy of an empty flat list must throw as Array.Parallel.reduceBy does" - [] - member _.``sum adds the elements`` () = - forEach - allInputs - (fun array description -> - Assert.AreEqual( - FlatList.sum array, - FlatList.Parallel.sum array, - $"FlatList.Parallel.sum of %s{description}" - ) - ) - - [] - member _.``sumBy adds the projections`` () = - forEach - allInputs - (fun array description -> - Assert.AreEqual( - FlatList.sumBy abs array, - FlatList.Parallel.sumBy abs array, - $"FlatList.Parallel.sumBy of %s{description}" - ) - ) + [] + member _.``sum adds the elements`` (input : ParallelInput) = + let array = flatListOf input + Assert.AreEqual(FlatList.sum array, FlatList.Parallel.sum array, "FlatList.Parallel.sum must add the elements") + + [] + member _.``sumBy adds the projections`` (input : ParallelInput) = + let array = flatListOf input + Assert.AreEqual( + FlatList.sumBy abs array, + FlatList.Parallel.sumBy abs array, + "FlatList.Parallel.sumBy must add the projections" + ) // ---- Partitioning and grouping ---- - [] - member _.``groupBy groups the elements as the sequential groupBy does`` () = - forEach - allInputs - (fun array description -> - Assert.AreEqual( - normalized (FlatList.groupBy (fun item -> item % 3) array), - normalized (FlatList.Parallel.groupBy (fun item -> item % 3) array), - $"FlatList.Parallel.groupBy of %s{description}" - ) - ) + [] + member _.``groupBy groups the elements as the sequential groupBy does`` (input : ParallelInput) = + let array = flatListOf input + let struct (expectedKeys, expectedGroups) = normalized (FlatList.groupBy (fun item -> item % 3) array) + let struct (keys, groups) = normalized (FlatList.Parallel.groupBy (fun item -> item % 3) array) + CollectionAssert.AreEqual (expectedKeys, keys, "FlatList.Parallel.groupBy must find the keys of the sequential groupBy") + + CollectionAssert.AreEqual ( + expectedGroups, + groups, + "FlatList.Parallel.groupBy must group the elements of each key as the sequential groupBy does" + ) - [] - member _.``groupBy groups the elements of a null key`` () = + [] + member _.``groupBy groups the elements of a null key`` (input : ParallelInput) = + let array = flatListOf input let projection (item : int) : string | null = if item < 0 then null else string (item % 3) + let struct (expectedKeys, expectedGroups) = normalized (FlatList.groupBy projection array) + let struct (keys, groups) = normalized (FlatList.Parallel.groupBy projection array) - forEach - allInputs - (fun array description -> - Assert.AreEqual( - normalized (FlatList.groupBy projection array), - normalized (FlatList.Parallel.groupBy projection array), - $"FlatList.Parallel.groupBy with null keys of %s{description}" - ) - ) + CollectionAssert.AreEqual ( + expectedKeys, + keys, + "FlatList.Parallel.groupBy must find the keys of the sequential groupBy, the null key included" + ) + + CollectionAssert.AreEqual ( + expectedGroups, + groups, + "FlatList.Parallel.groupBy must group the elements of each key as the sequential groupBy does, the null key included" + ) [] member _.``groupBy puts the NaN keys of type float and float32 in one group as Array.Parallel does`` () = // Array.Parallel.groupBy compares these keys with ER semantics, while the sequential Array.groupBy makes a group of // each NaN. The keys are compared as text, because NaN is not equal to itself. let check (source : 'Key array) = - Assert.AreEqual<(string * int) list>( + CollectionAssert.AreEqual ( Array.Parallel.groupBy id source |> Seq.map (fun (key, items) -> string key, items.Length) |> Seq.sort - |> Seq.toList, + |> Seq.toArray, FlatList.Parallel.groupBy id (FlatList.ofArray source) |> Seq.map (fun struct (key, items) -> string key, items.Length) |> Seq.sort - |> Seq.toList, - $"FlatList.Parallel.groupBy of %A{source}" + |> Seq.toArray, + $"FlatList.Parallel.groupBy of %A{source} must group as Array.Parallel.groupBy does" ) check [| nan; 1.0; nan; 2.0; nan |] check [| nanf; 1.0f; nanf; 2.0f; nanf |] - [] - member _.``partition splits the elements in their order`` () = - forEach - allInputs - (fun array description -> - for predicate in predicates do - let struct (expectedAccepted, expectedRejected) = FlatList.partition predicate array - let struct (accepted, rejected) = FlatList.Parallel.partition predicate array - - Assert.AreEqual( - FlatList.toList expectedAccepted, - FlatList.toList accepted, - $"FlatList.Parallel.partition accepted of %s{description}" - ) - - Assert.AreEqual( - FlatList.toList expectedRejected, - FlatList.toList rejected, - $"FlatList.Parallel.partition rejected of %s{description}" - ) - ) + [] + member _.``partition splits the elements in their order`` (input : ParallelInput, predicate : ParallelPredicate) = + let array = flatListOf input + let struct (expectedAccepted, expectedRejected) = FlatList.partition predicate.Matches array + let struct (accepted, rejected) = FlatList.Parallel.partition predicate.Matches array + + CollectionAssert.AreEqual ( + FlatList.toArray expectedAccepted, + FlatList.toArray accepted, + "FlatList.Parallel.partition must return the elements that satisfy the predicate in their order" + ) + + CollectionAssert.AreEqual ( + FlatList.toArray expectedRejected, + FlatList.toArray rejected, + "FlatList.Parallel.partition must return the elements that do not satisfy the predicate in their order" + ) + + [] + member _.``partitionWith splits the elements by the choice in their order`` (input : ParallelInput) = + let array = flatListOf input - [] - member _.``partitionWith splits the elements by the choice in their order`` () = let partitioner (item : int) = if item > 0 then Choice1Of2 item else Choice2Of2 (string item) - forEach - allInputs - (fun array description -> - let struct (expectedFirst, expectedSecond) = FlatList.partitionWith partitioner array - let struct (first, second) = FlatList.Parallel.partitionWith partitioner array - - Assert.AreEqual( - FlatList.toList expectedFirst, - FlatList.toList first, - $"FlatList.Parallel.partitionWith first choices of %s{description}" - ) - - Assert.AreEqual( - FlatList.toList expectedSecond, - FlatList.toList second, - $"FlatList.Parallel.partitionWith second choices of %s{description}" - ) - ) + let struct (expectedFirst, expectedSecond) = FlatList.partitionWith partitioner array + let struct (first, second) = FlatList.Parallel.partitionWith partitioner array + + CollectionAssert.AreEqual ( + FlatList.toArray expectedFirst, + FlatList.toArray first, + "FlatList.Parallel.partitionWith must return the first choices in their order" + ) + + CollectionAssert.AreEqual ( + FlatList.toArray expectedSecond, + FlatList.toArray second, + "FlatList.Parallel.partitionWith must return the second choices in their order" + ) // ---- Sorting ---- - [] - member _.``sort orders the elements ascending`` () = - forEach - allInputs - (fun array description -> - Assert.AreEqual( - FlatList.toList (FlatList.sort array), - FlatList.toList (FlatList.Parallel.sort array), - $"FlatList.Parallel.sort of %s{description}" - ) - ) + [] + member _.``sort orders the elements ascending`` (input : ParallelInput) = + let array = flatListOf input - [] - member _.``sortBy orders the elements by the projection`` () = - forEach - allInputs - (fun array description -> - Assert.AreEqual( - FlatList.toList (FlatList.sortBy negate array), - FlatList.toList (FlatList.Parallel.sortBy negate array), - $"FlatList.Parallel.sortBy of %s{description}" - ) - ) + CollectionAssert.AreEqual ( + FlatList.toArray (FlatList.sort array), + FlatList.toArray (FlatList.Parallel.sort array), + "FlatList.Parallel.sort must order the elements ascending" + ) - [] - member _.``sortByDescending orders the elements by the projection descending`` () = - forEach - allInputs - (fun array description -> - Assert.AreEqual( - FlatList.toList (FlatList.sortByDescending negate array), - FlatList.toList (FlatList.Parallel.sortByDescending negate array), - $"FlatList.Parallel.sortByDescending of %s{description}" - ) - ) + [] + member _.``sortBy orders the elements by the projection`` (input : ParallelInput) = + let array = flatListOf input - [] - member _.``sortDescending orders the elements descending`` () = - forEach - allInputs - (fun array description -> - Assert.AreEqual( - FlatList.toList (FlatList.sortDescending array), - FlatList.toList (FlatList.Parallel.sortDescending array), - $"FlatList.Parallel.sortDescending of %s{description}" - ) - ) + CollectionAssert.AreEqual ( + FlatList.toArray (FlatList.sortBy negate array), + FlatList.toArray (FlatList.Parallel.sortBy negate array), + "FlatList.Parallel.sortBy must order the elements by the projection" + ) - [] - member _.``sortWith orders the elements with the comparison function`` () = + [] + member _.``sortByDescending orders the elements by the projection descending`` (input : ParallelInput) = + let array = flatListOf input + + CollectionAssert.AreEqual ( + FlatList.toArray (FlatList.sortByDescending negate array), + FlatList.toArray (FlatList.Parallel.sortByDescending negate array), + "FlatList.Parallel.sortByDescending must order the elements by the projection descending" + ) + + [] + member _.``sortDescending orders the elements descending`` (input : ParallelInput) = + let array = flatListOf input + + CollectionAssert.AreEqual ( + FlatList.toArray (FlatList.sortDescending array), + FlatList.toArray (FlatList.Parallel.sortDescending array), + "FlatList.Parallel.sortDescending must order the elements descending" + ) + + [] + member _.``sortWith orders the elements with the comparison function`` (input : ParallelInput) = + let array = flatListOf input let descending (left : int) (right : int) = compare right left - forEach - allInputs - (fun array description -> - Assert.AreEqual( - FlatList.toList (FlatList.sortWith descending array), - FlatList.toList (FlatList.Parallel.sortWith descending array), - $"FlatList.Parallel.sortWith of %s{description}" - ) - ) + CollectionAssert.AreEqual ( + FlatList.toArray (FlatList.sortWith descending array), + FlatList.toArray (FlatList.Parallel.sortWith descending array), + "FlatList.Parallel.sortWith must order the elements with the comparison function" + ) [] member _.``the sorting functions leave the shared array of the input unchanged`` () = - let array = FlatList.ofList large + let array = FlatList.ofArray (large ()) FlatList.Parallel.sort array |> ignore FlatList.Parallel.sortBy negate array |> ignore @@ -735,155 +891,29 @@ type FlatListParallelTests () = FlatList.Parallel.sortDescending array |> ignore FlatList.Parallel.sortWith compare array |> ignore - Assert.AreEqual(large, FlatList.toList array, "The sorting functions must sort a copy of the input") + CollectionAssert.AreEqual (large (), FlatList.toArray array, "The sorting functions must sort a copy of the input") // ---- Default flat lists ---- - [] - member _.``functions reject a default flat list as Array.Parallel rejects a null array`` () = - let firstChoice (item : int) : Choice = Choice1Of2 item + [] + member _.``functions reject a default flat list as Array.Parallel rejects a null array`` (name : string) = + let struct (oracle, actual) = arrayRejections[name] + ExceptionAssert.same id oracle actual $"FlatList.Parallel.%s{name} must reject a default flat list" - let cases : struct (string * (unit -> unit) * (unit -> unit)) list = [ - struct (nameof FlatList.Parallel.average, - (fun () -> - Array.Parallel.average (Unchecked.defaultof) - |> ignore - ), - (fun () -> - FlatList.Parallel.average (Unchecked.defaultof>) - |> ignore - )) - struct (nameof FlatList.Parallel.averageBy, - (fun () -> Array.Parallel.averageBy float nullArray |> ignore), - (fun () -> FlatList.Parallel.averageBy float defaultArray |> ignore)) - struct (nameof FlatList.Parallel.choose, - (fun () -> Array.Parallel.choose Some nullArray |> ignore), - (fun () -> FlatList.Parallel.choose ValueSome defaultArray |> ignore)) - struct (nameof FlatList.Parallel.collect, - (fun () -> Array.Parallel.collect Array.singleton nullArray |> ignore), - (fun () -> - FlatList.Parallel.collect FlatList.singleton defaultArray - |> ignore - )) - struct (nameof FlatList.Parallel.exists, - (fun () -> Array.Parallel.exists ((=) 1) nullArray |> ignore), - (fun () -> FlatList.Parallel.exists ((=) 1) defaultArray |> ignore)) - struct (nameof FlatList.Parallel.filter, - (fun () -> Array.Parallel.filter ((=) 1) nullArray |> ignore), - (fun () -> FlatList.Parallel.filter ((=) 1) defaultArray |> ignore)) - struct (nameof FlatList.Parallel.forall, - (fun () -> Array.Parallel.forall ((=) 1) nullArray |> ignore), - (fun () -> FlatList.Parallel.forall ((=) 1) defaultArray |> ignore)) - struct (nameof FlatList.Parallel.groupBy, - (fun () -> Array.Parallel.groupBy id nullArray |> ignore), - (fun () -> FlatList.Parallel.groupBy id defaultArray |> ignore)) - struct (nameof FlatList.Parallel.iter, - (fun () -> Array.Parallel.iter ignore nullArray), - (fun () -> FlatList.Parallel.iter ignore defaultArray)) - struct (nameof FlatList.Parallel.iteri, - (fun () -> Array.Parallel.iteri (fun _ _ -> ()) nullArray), - (fun () -> FlatList.Parallel.iteri (fun _ _ -> ()) defaultArray)) - struct (nameof FlatList.Parallel.map, - (fun () -> Array.Parallel.map id nullArray |> ignore), - (fun () -> FlatList.Parallel.map id defaultArray |> ignore)) - struct (nameof FlatList.Parallel.mapi, - (fun () -> Array.Parallel.mapi (fun _ item -> item) nullArray |> ignore), - (fun () -> - FlatList.Parallel.mapi (fun _ item -> item) defaultArray - |> ignore - )) - struct (nameof FlatList.Parallel.max, - (fun () -> Array.Parallel.max nullArray |> ignore), - (fun () -> FlatList.Parallel.max defaultArray |> ignore)) - struct (nameof FlatList.Parallel.maxBy, - (fun () -> Array.Parallel.maxBy abs nullArray |> ignore), - (fun () -> FlatList.Parallel.maxBy abs defaultArray |> ignore)) - struct (nameof FlatList.Parallel.min, - (fun () -> Array.Parallel.min nullArray |> ignore), - (fun () -> FlatList.Parallel.min defaultArray |> ignore)) - struct (nameof FlatList.Parallel.minBy, - (fun () -> Array.Parallel.minBy abs nullArray |> ignore), - (fun () -> FlatList.Parallel.minBy abs defaultArray |> ignore)) - struct (nameof FlatList.Parallel.partition, - (fun () -> Array.Parallel.partition ((=) 1) nullArray |> ignore), - (fun () -> FlatList.Parallel.partition ((=) 1) defaultArray |> ignore)) - struct (nameof FlatList.Parallel.partitionWith, - (fun () -> Array.Parallel.partitionWith firstChoice nullArray |> ignore), - (fun () -> - FlatList.Parallel.partitionWith firstChoice defaultArray - |> ignore - )) - struct (nameof FlatList.Parallel.reduce, - (fun () -> Array.Parallel.reduce (+) nullArray |> ignore), - (fun () -> FlatList.Parallel.reduce (+) defaultArray |> ignore)) - struct (nameof FlatList.Parallel.reduceBy, - (fun () -> Array.Parallel.reduceBy abs (+) nullArray |> ignore), - (fun () -> FlatList.Parallel.reduceBy abs (+) defaultArray |> ignore)) - struct (nameof FlatList.Parallel.sort, - (fun () -> Array.Parallel.sort nullArray |> ignore), - (fun () -> FlatList.Parallel.sort defaultArray |> ignore)) - struct (nameof FlatList.Parallel.sortBy, - (fun () -> Array.Parallel.sortBy negate nullArray |> ignore), - (fun () -> FlatList.Parallel.sortBy negate defaultArray |> ignore)) - struct (nameof FlatList.Parallel.sortByDescending, - (fun () -> Array.Parallel.sortByDescending negate nullArray |> ignore), - (fun () -> - FlatList.Parallel.sortByDescending negate defaultArray - |> ignore - )) - struct (nameof FlatList.Parallel.sortDescending, - (fun () -> Array.Parallel.sortDescending nullArray |> ignore), - (fun () -> FlatList.Parallel.sortDescending defaultArray |> ignore)) - struct (nameof FlatList.Parallel.sortWith, - (fun () -> Array.Parallel.sortWith compare nullArray |> ignore), - (fun () -> FlatList.Parallel.sortWith compare defaultArray |> ignore)) - struct (nameof FlatList.Parallel.tryFind, - (fun () -> Array.Parallel.tryFind ((=) 1) nullArray |> ignore), - (fun () -> FlatList.Parallel.tryFind ((=) 1) defaultArray |> ignore)) - struct (nameof FlatList.Parallel.tryFindIndex, - (fun () -> Array.Parallel.tryFindIndex ((=) 1) nullArray |> ignore), - (fun () -> - FlatList.Parallel.tryFindIndex ((=) 1) defaultArray - |> ignore - )) - struct (nameof FlatList.Parallel.tryPick, - (fun () -> Array.Parallel.tryPick Some nullArray |> ignore), - (fun () -> FlatList.Parallel.tryPick ValueSome defaultArray |> ignore)) - struct (nameof FlatList.Parallel.zip, - (fun () -> Array.Parallel.zip nullArray [| 1 |] |> ignore), - (fun () -> - FlatList.Parallel.zip defaultArray (FlatList.singleton 1) - |> ignore - )) - struct (nameof FlatList.Parallel.zip, - (fun () -> Array.Parallel.zip [| 1 |] nullArray |> ignore), - (fun () -> - FlatList.Parallel.zip (FlatList.singleton 1) defaultArray - |> ignore - )) - ] - - for struct (name, oracle, actual) in cases do - ExceptionAssert.same id oracle actual $"FlatList.Parallel.%s{name} must reject a default flat list" - - [] - member _.``sum and sumBy reject a default flat list where Array.Parallel dereferences a null array`` () = + [] + member _.``sum and sumBy reject a default flat list where Array.Parallel dereferences a null array`` (name : string) = // Array.Parallel.sum and sumBy throw NullReferenceException for a null array; their documentation promises // ArgumentNullException, which a default flat list throws - let cases : struct (string * (unit -> unit)) list = [ - struct (nameof FlatList.Parallel.sum, (fun () -> FlatList.Parallel.sum defaultArray |> ignore)) - struct (nameof FlatList.Parallel.sumBy, (fun () -> FlatList.Parallel.sumBy abs defaultArray |> ignore)) - ] - - for struct (name, actual) in cases do - let ex = - Assert.ThrowsExactly( - Action actual, - $"FlatList.Parallel.%s{name} must reject a default flat list" - ) - - Assert.AreEqual( - "array", - ex.ParamName, - $"FlatList.Parallel.%s{name} must name the parameter of the default flat list" + let actual = dereferences[name] + + let ex = + Assert.ThrowsExactly( + Action actual, + $"FlatList.Parallel.%s{name} must reject a default flat list" ) + + Assert.AreEqual( + "array", + ex.ParamName, + $"FlatList.Parallel.%s{name} must name the parameter of the default flat list" + ) diff --git a/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelTupleTests.fs b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelTupleTests.fs index 3da9500f..dab1f7d1 100644 --- a/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelTupleTests.fs +++ b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelTupleTests.fs @@ -11,25 +11,40 @@ open Microsoft.VisualStudio.TestTools.UnitTesting [] type FlatListParallelTupleTests () = - static let array = FlatList.ofList Inputs.several + static let array = FlatList.ofArray Inputs.Several - static let other = FlatList.ofList Inputs.other - - /// A flat list long enough for the functions to split their work among several threads. - static let large = FlatList.init 10_000 (fun index -> index % 1_000 - 500) + /// The elements of a flat list long enough for the functions to split their work among several threads. + static let large () = Array.init 10_000 (fun index -> index % 1_000 - 500) static let isPositive (item : int) = item > 0 - [] - member _.``zip returns reference tuples`` () = - for struct (first, second) in [ struct (array, other); struct (large, FlatList.rev large) ] do - let pairs : FlatList = FlatList.Parallel.zip first second - - Assert.AreEqual<(int * int) list>( - List.zip (FlatList.toList first) (FlatList.toList second), - FlatList.toList pairs, - $"Tuple.FlatList.Parallel.zip of %d{first.Length} elements must zip as List.zip does" - ) + /// The flat list of the elements of an input. + static let flatListOf (input : ParallelInput) = FlatList.ofArray (input.Elements large) + + /// The input with several elements of the contracts and the larger input. + static member Sources = seq { + ParallelInput.Several + ParallelInput.Large + } + + [] + member _.``zip returns reference tuples`` (input : ParallelInput) = + let first = flatListOf input + + // The input with several elements is zipped with the second input of the contracts, the larger input with its + // reverse + let second = + match input with + | ParallelInput.Several -> FlatList.ofArray Inputs.Other + | _ -> FlatList.rev first + + let pairs : FlatList = FlatList.Parallel.zip first second + + CollectionAssert.AreEqual ( + Array.zip (FlatList.toArray first) (FlatList.toArray second), + FlatList.toArray pairs, + "Tuple.FlatList.Parallel.zip must zip as Array.zip does" + ) [] member _.``zip throws for different lengths as the default does`` () = @@ -42,23 +57,35 @@ type FlatListParallelTupleTests () = (fun () -> FlatList.Parallel.zip array (FlatList.singleton 1) |> ignore) "Tuple.FlatList.Parallel.zip must throw as the default zip does" - [] - member _.``groupBy returns reference tuples`` () = - for source in [ array; large ] do - let groups : FlatList> = FlatList.Parallel.groupBy (fun item -> item % 3) source - - // The order of the keys and of the elements of a key is open, so both are sorted - Assert.AreEqual<(int * int list) list>( - List.groupBy (fun item -> item % 3) (FlatList.toList source) - |> Seq.map (fun (key, items) -> key, List.sort items) - |> Seq.sortBy fst - |> Seq.toList, - groups - |> Seq.map (fun (key, items) -> key, items |> Seq.sort |> Seq.toList) - |> Seq.sortBy fst - |> Seq.toList, - $"Tuple.FlatList.Parallel.groupBy of %d{source.Length} elements must group as List.groupBy does" - ) + [] + member _.``groupBy returns reference tuples`` (input : ParallelInput) = + let source = flatListOf input + let groups : FlatList> = FlatList.Parallel.groupBy (fun item -> item % 3) source + + // The order of the keys and of the elements of a key is open, so both are sorted; a group pairs its key with its + // elements, which a pair compares by reference, so the keys and the elements are compared apart + let expected = + Array.groupBy (fun item -> item % 3) (FlatList.toArray source) + |> Array.map (fun (key, items) -> key, Array.sort items) + |> Array.sortBy fst + + let actual = + groups + |> Seq.map (fun (key, items) -> key, items |> Seq.sort |> Seq.toArray) + |> Seq.sortBy fst + |> Seq.toArray + + CollectionAssert.AreEqual ( + Array.map fst expected, + Array.map fst actual, + "Tuple.FlatList.Parallel.groupBy must find the keys as Array.groupBy does" + ) + + CollectionAssert.AreEqual ( + Array.map snd expected, + Array.map snd actual, + "Tuple.FlatList.Parallel.groupBy must group the elements of each key as Array.groupBy does" + ) [] member _.``groupBy puts the NaN keys of type float and float32 in one group as Array.Parallel does`` () = @@ -66,64 +93,65 @@ type FlatListParallelTupleTests () = let check (source : 'Key array) = let groups : FlatList<'Key * FlatList<'Key>> = FlatList.Parallel.groupBy id (FlatList.ofArray source) - Assert.AreEqual<(string * int) list>( + CollectionAssert.AreEqual ( Array.Parallel.groupBy id source |> Seq.map (fun (key, items) -> string key, items.Length) |> Seq.sort - |> Seq.toList, + |> Seq.toArray, groups |> Seq.map (fun (key, items) -> string key, items.Length) |> Seq.sort - |> Seq.toList, - $"Tuple.FlatList.Parallel.groupBy of %A{source}" + |> Seq.toArray, + $"Tuple.FlatList.Parallel.groupBy of %A{source} must group as Array.Parallel.groupBy does" ) check [| nan; 1.0; nan; 2.0; nan |] check [| nanf; 1.0f; nanf; 2.0f; nanf |] - [] - member _.``partition returns a reference tuple`` () = - for source in [ array; large ] do - let (accepted : FlatList), (rejected : FlatList) = - FlatList.Parallel.partition isPositive source - let expectedAccepted, expectedRejected = List.partition isPositive (FlatList.toList source) - - Assert.AreEqual( - expectedAccepted, - FlatList.toList accepted, - $"Tuple.FlatList.Parallel.partition of %d{source.Length} elements must return the accepted elements" - ) - - Assert.AreEqual( - expectedRejected, - FlatList.toList rejected, - $"Tuple.FlatList.Parallel.partition of %d{source.Length} elements must return the rejected elements" - ) + [] + member _.``partition returns a reference tuple`` (input : ParallelInput) = + let source = flatListOf input + let (accepted : FlatList), (rejected : FlatList) = + FlatList.Parallel.partition isPositive source + let expectedAccepted, expectedRejected = Array.partition isPositive (FlatList.toArray source) + + CollectionAssert.AreEqual ( + expectedAccepted, + FlatList.toArray accepted, + "Tuple.FlatList.Parallel.partition must return the accepted elements" + ) + + CollectionAssert.AreEqual ( + expectedRejected, + FlatList.toArray rejected, + "Tuple.FlatList.Parallel.partition must return the rejected elements" + ) + + [] + member _.``partitionWith returns a reference tuple`` (input : ParallelInput) = + let source = flatListOf input - [] - member _.``partitionWith returns a reference tuple`` () = let partitioner (item : int) = if item > 0 then Choice1Of2 item else Choice2Of2 (string item) - for source in [ array; large ] do - let (first : FlatList), (second : FlatList) = - FlatList.Parallel.partitionWith partitioner source - let expectedFirst, expectedSecond = List.partitionWith partitioner (FlatList.toList source) - - Assert.AreEqual( - expectedFirst, - FlatList.toList first, - $"Tuple.FlatList.Parallel.partitionWith of %d{source.Length} elements must return the first choices" - ) - - Assert.AreEqual( - expectedSecond, - FlatList.toList second, - $"Tuple.FlatList.Parallel.partitionWith of %d{source.Length} elements must return the second choices" - ) + let (first : FlatList), (second : FlatList) = + FlatList.Parallel.partitionWith partitioner source + let expectedFirst, expectedSecond = Array.partitionWith partitioner (FlatList.toArray source) + + CollectionAssert.AreEqual ( + expectedFirst, + FlatList.toArray first, + "Tuple.FlatList.Parallel.partitionWith must return the first choices" + ) + + CollectionAssert.AreEqual ( + expectedSecond, + FlatList.toArray second, + "Tuple.FlatList.Parallel.partitionWith must return the second choices" + ) [] member _.``the functions without a tuple keep resolving to the default module`` () = From daede861176b3fc1d6169e12d982f66994a73b49 Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Sun, 18 Oct 2026 17:22:42 +0200 Subject: [PATCH 11/14] fixup! feat: `FlatList.Parallel` module with the vocabulary of the FSharp.Core `Array.Parallel` module A `` and a `` stay on one line each only when both are short (up to 60 characters of text); otherwise both span several lines. Co-Authored-By: Claude Opus 5.5 --- .../FlatList/FlatList.fsi | 140 +++++++++++++----- .../FlatList/FlatListOption.fsi | 32 +++- .../FlatList/FlatListTuple.fsi | 20 ++- .../Helper/ParallelArrays.fs | 28 +++- 4 files changed, 165 insertions(+), 55 deletions(-) diff --git a/src/FSharp.Collections.Immutable/FlatList/FlatList.fsi b/src/FSharp.Collections.Immutable/FlatList/FlatList.fsi index c97080ab..4f746557 100644 --- a/src/FSharp.Collections.Immutable/FlatList/FlatList.fsi +++ b/src/FSharp.Collections.Immutable/FlatList/FlatList.fsi @@ -2074,7 +2074,9 @@ val isDefault : array : FlatList<'T> -> bool [] val isDefaultOrEmpty : array : FlatList<'T> -> bool -/// Functions that process the elements of a flat list in parallel. +/// +/// Functions that process the elements of a flat list in parallel. +/// /// /// /// The module mirrors : each function has the semantics, @@ -2102,7 +2104,9 @@ module Parallel = // ---- Creation ---- - /// Creates a flat list from a function of the index, in parallel. + /// + /// Creates a flat list from a function of the index, in parallel. + /// /// The length of the flat list. /// The function that computes the element at each index, called in parallel. /// The created flat list. @@ -2117,7 +2121,9 @@ module Parallel = // ---- Search ---- - /// Returns whether any element satisfies the predicate. + /// + /// Returns whether any element satisfies the predicate. + /// /// The function to test the elements, called in parallel. /// The input flat list. /// when an element satisfies the predicate. @@ -2127,7 +2133,9 @@ module Parallel = [] val exists : predicate : ('T -> bool) -> array : FlatList<'T> -> bool - /// Returns whether all elements satisfy the predicate. + /// + /// Returns whether all elements satisfy the predicate. + /// /// The function to test the elements, called in parallel. /// The input flat list. /// when every element satisfies the predicate. @@ -2137,48 +2145,68 @@ module Parallel = [] val forall : predicate : ('T -> bool) -> array : FlatList<'T> -> bool - /// Returns the first element that satisfies the predicate, or ValueNone. + /// + /// Returns the first element that satisfies the predicate, or ValueNone. + /// /// The function to test the elements, called in parallel. /// The input flat list. /// The first element that satisfies the predicate, or ValueNone. - /// O(n). The predicate can also run for elements after the first one that satisfies it. + /// + /// O(n). The predicate can also run for elements after the first one that satisfies it. + /// [] val tryFind : predicate : ('T -> bool) -> array : FlatList<'T> -> 'T voption - /// Returns the index of the first element that satisfies the predicate, or ValueNone. + /// + /// Returns the index of the first element that satisfies the predicate, or ValueNone. + /// /// The function to test the elements, called in parallel. /// The input flat list. /// The index of the first element that satisfies the predicate, or ValueNone. - /// O(n). The predicate can also run for elements after the first one that satisfies it. + /// + /// O(n). The predicate can also run for elements after the first one that satisfies it. + /// [] val tryFindIndex : predicate : ('T -> bool) -> array : FlatList<'T> -> int voption - /// Returns the first value that the chooser returns for the elements, or ValueNone. + /// + /// Returns the first value that the chooser returns for the elements, or ValueNone. + /// /// The function that returns a value, or ValueNone to go on, called in parallel. /// The input flat list. /// The value for the first element that the chooser returns a value for, or ValueNone. - /// O(n). The chooser can also run for elements after the first one that it returns a value for. + /// + /// O(n). The chooser can also run for elements after the first one that it returns a value for. + /// [] val tryPick : chooser : ('T -> 'U voption) -> array : FlatList<'T> -> 'U voption // ---- Transformation ---- - /// Returns the values that the chooser returns for the elements. + /// + /// Returns the values that the chooser returns for the elements. + /// /// /// The function that returns a value, or ValueNone to skip the element, called in parallel. /// /// The input flat list. /// The chosen values, in the order of their elements. - /// O(n). + /// + /// O(n). + /// [] val choose : chooser : ('T -> 'U voption) -> array : FlatList<'T> -> FlatList<'U> - /// Concatenates the flat lists that a function returns for the elements. + /// + /// Concatenates the flat lists that a function returns for the elements. + /// /// The function that returns a flat list for each element, called in parallel. /// The input flat list. /// The concatenation of the returned flat lists, in the order of the elements. /// Thrown when the mapping returns a default flat list. - /// O(n + the total length of the returned flat lists). + /// + /// O(n + the total length of the returned flat lists). + /// [] val collect : mapping : ('T -> FlatList<'U>) -> array : FlatList<'T> -> FlatList<'U> @@ -2198,26 +2226,36 @@ module Parallel = [] val map : mapping : ('T -> 'U) -> array : FlatList<'T> -> FlatList<'U> - /// Returns the results of a function applied to each element and its index. + /// + /// Returns the results of a function applied to each element and its index. + /// /// The function to transform the elements, called in parallel. /// The input flat list. /// The results, in the order of the elements. - /// O(n). + /// + /// O(n). + /// [] val mapi : mapping : (int -> 'T -> 'U) -> array : FlatList<'T> -> FlatList<'U> - /// Combines two flat lists into a flat list of pairs, in parallel. + /// + /// Combines two flat lists into a flat list of pairs, in parallel. + /// /// The first flat list. /// The second flat list. /// The pairs of elements at the same index. /// Thrown when the flat lists have different lengths. - /// O(n). + /// + /// O(n). + /// [] val zip : array1 : FlatList<'T1> -> array2 : FlatList<'T2> -> FlatList // ---- Folding ---- - /// Returns the average of the elements, which are added in parallel. + /// + /// Returns the average of the elements, which are added in parallel. + /// /// The input flat list. /// The average. /// Thrown when the flat list is empty. @@ -2230,7 +2268,9 @@ module Parallel = array : FlatList< ^T > -> ^T when ^T : (static member (+) : ^T * ^T -> ^T) and ^T : (static member DivideByInt : ^T * int -> ^T) - /// Returns the average of the results of a function applied to each element. + /// + /// Returns the average of the results of a function applied to each element. + /// /// The function to transform the elements, called in parallel. /// The input flat list. /// The average of the results. @@ -2251,10 +2291,14 @@ module Parallel = [] val iter : action : ('T -> unit) -> array : FlatList<'T> -> unit - /// Applies an action to each element and its index, in parallel. + /// + /// Applies an action to each element and its index, in parallel. + /// /// The action, called in parallel. /// The input flat list. - /// O(n). + /// + /// O(n). + /// [] val iteri : action : (int -> 'T -> unit) -> array : FlatList<'T> -> unit @@ -2304,7 +2348,9 @@ module Parallel = [] val reduce : reduction : ('T -> 'T -> 'T) -> array : FlatList<'T> -> 'T - /// Combines the results of a function applied to each element with another function, in parallel. + /// + /// Combines the results of a function applied to each element with another function, in parallel. + /// /// The function to transform the elements, called in parallel. /// /// The function that combines two results. It combines the results in parallel chunks and then the results of the @@ -2313,11 +2359,15 @@ module Parallel = /// The input flat list. /// The combined value. /// Thrown when the flat list is empty. - /// O(n). The sequential module has no counterpart: the value is the reduction of the mapped elements. + /// + /// O(n). The sequential module has no counterpart: the value is the reduction of the mapped elements. + /// [] val reduceBy : projection : ('T -> 'U) -> reduction : ('U -> 'U -> 'U) -> array : FlatList<'T> -> 'U - /// Returns the sum of the elements, which are added in parallel. + /// + /// Returns the sum of the elements, which are added in parallel. + /// /// The input flat list. /// The sum; zero for an empty flat list. /// @@ -2327,7 +2377,9 @@ module Parallel = [] val inline sum : array : FlatList< ^T > -> ^T when ^T : (static member (+) : ^T * ^T -> ^T) and ^T : (static member Zero : ^T) - /// Returns the sum of the results of a function applied to each element. + /// + /// Returns the sum of the results of a function applied to each element. + /// /// The function to transform the elements, called in parallel. /// The input flat list. /// The sum of the results; zero for an empty flat list. @@ -2342,7 +2394,9 @@ module Parallel = // ---- Partitioning and grouping ---- - /// Groups the elements by the key of a projection. + /// + /// Groups the elements by the key of a projection. + /// /// The function that returns the key of an element, called in parallel. /// The input flat list. /// @@ -2365,19 +2419,25 @@ module Parallel = [] val partition : predicate : ('T -> bool) -> array : FlatList<'T> -> struct (FlatList<'T> * FlatList<'T>) - /// Splits the elements by the choice that a function returns for them. + /// + /// Splits the elements by the choice that a function returns for them. + /// /// /// The function that returns Choice1Of2 or Choice2Of2 for an element, called in parallel. /// /// The input flat list. /// The values of the first choices and of the second choices, both in the order of the elements. - /// O(n). + /// + /// O(n). + /// [] val partitionWith : partitioner : ('T -> Choice<'T1, 'T2>) -> array : FlatList<'T> -> struct (FlatList<'T1> * FlatList<'T2>) // ---- Sorting ---- - /// Sorts the elements in parallel, in the order of F# generic comparison. + /// + /// Sorts the elements in parallel, in the order of F# generic comparison. + /// /// The input flat list. /// The sorted flat list. /// @@ -2393,22 +2453,32 @@ module Parallel = /// The function that returns the key of an element, called in parallel. /// The input flat list. /// The sorted flat list. - /// O(n log n). This is not a stable sort. + /// + /// O(n log n). This is not a stable sort. + /// [] val sortBy : projection : ('T -> 'Key) -> array : FlatList<'T> -> FlatList<'T> when 'Key : comparison - /// Sorts the elements in parallel by the result of a projection, in descending order. + /// + /// Sorts the elements in parallel by the result of a projection, in descending order. + /// /// The function that returns the key of an element, called in parallel. /// The input flat list. /// The sorted flat list. - /// O(n log n). This is not a stable sort. + /// + /// O(n log n). This is not a stable sort. + /// [] val sortByDescending : projection : ('T -> 'Key) -> array : FlatList<'T> -> FlatList<'T> when 'Key : comparison - /// Sorts the elements in parallel, in descending order of F# generic comparison. + /// + /// Sorts the elements in parallel, in descending order of F# generic comparison. + /// /// The input flat list. /// The sorted flat list. - /// O(n log n). This is not a stable sort. + /// + /// O(n log n). This is not a stable sort. + /// [] val sortDescending : array : FlatList<'T> -> FlatList<'T> when 'T : comparison diff --git a/src/FSharp.Collections.Immutable/FlatList/FlatListOption.fsi b/src/FSharp.Collections.Immutable/FlatList/FlatListOption.fsi index d81c4522..e4b88802 100644 --- a/src/FSharp.Collections.Immutable/FlatList/FlatListOption.fsi +++ b/src/FSharp.Collections.Immutable/FlatList/FlatListOption.fsi @@ -189,38 +189,54 @@ module Parallel = // ---- Search ---- - /// Returns the first element that satisfies the predicate, or None. + /// + /// Returns the first element that satisfies the predicate, or None. + /// /// The function to test the elements, called in parallel. /// The input flat list. /// The first element that satisfies the predicate, or None. - /// O(n). The predicate can also run for elements after the first one that satisfies it. + /// + /// O(n). The predicate can also run for elements after the first one that satisfies it. + /// [] val tryFind : predicate : ('T -> bool) -> array : FlatList<'T> -> 'T option - /// Returns the index of the first element that satisfies the predicate, or None. + /// + /// Returns the index of the first element that satisfies the predicate, or None. + /// /// The function to test the elements, called in parallel. /// The input flat list. /// The index of the first element that satisfies the predicate, or None. - /// O(n). The predicate can also run for elements after the first one that satisfies it. + /// + /// O(n). The predicate can also run for elements after the first one that satisfies it. + /// [] val tryFindIndex : predicate : ('T -> bool) -> array : FlatList<'T> -> int option - /// Returns the first value that the chooser returns for the elements, or None. + /// + /// Returns the first value that the chooser returns for the elements, or None. + /// /// The function that returns a value, or None to go on, called in parallel. /// The input flat list. /// The value for the first element that the chooser returns a value for, or None. - /// O(n). The chooser can also run for elements after the first one that it returns a value for. + /// + /// O(n). The chooser can also run for elements after the first one that it returns a value for. + /// [] val tryPick : chooser : ('T -> 'U option) -> array : FlatList<'T> -> 'U option // ---- Transformation ---- - /// Returns the values that the chooser returns for the elements. + /// + /// Returns the values that the chooser returns for the elements. + /// /// /// The function that returns a value, or None to skip the element, called in parallel. /// /// The input flat list. /// The chosen values, in the order of their elements. - /// O(n). + /// + /// O(n). + /// [] val choose : chooser : ('T -> 'U option) -> array : FlatList<'T> -> FlatList<'U> diff --git a/src/FSharp.Collections.Immutable/FlatList/FlatListTuple.fsi b/src/FSharp.Collections.Immutable/FlatList/FlatListTuple.fsi index 64970702..de5ce621 100644 --- a/src/FSharp.Collections.Immutable/FlatList/FlatListTuple.fsi +++ b/src/FSharp.Collections.Immutable/FlatList/FlatListTuple.fsi @@ -184,18 +184,24 @@ module Parallel = // ---- Transformation ---- - /// Combines two flat lists into a flat list of pairs, in parallel. + /// + /// Combines two flat lists into a flat list of pairs, in parallel. + /// /// The first flat list. /// The second flat list. /// The pairs of elements at the same index. /// Thrown when the flat lists have different lengths. - /// O(n). + /// + /// O(n). + /// [] val zip : array1 : FlatList<'T1> -> array2 : FlatList<'T2> -> FlatList<'T1 * 'T2> // ---- Partitioning and grouping ---- - /// Groups the elements by the key of a projection. + /// + /// Groups the elements by the key of a projection. + /// /// The function that returns the key of an element, called in parallel. /// The input flat list. /// @@ -218,12 +224,16 @@ module Parallel = [] val partition : predicate : ('T -> bool) -> array : FlatList<'T> -> FlatList<'T> * FlatList<'T> - /// Splits the elements by the choice that a function returns for them. + /// + /// Splits the elements by the choice that a function returns for them. + /// /// /// The function that returns Choice1Of2 or Choice2Of2 for an element, called in parallel. /// /// The input flat list. /// The values of the first choices and of the second choices, both in the order of the elements. - /// O(n). + /// + /// O(n). + /// [] val partitionWith : partitioner : ('T -> Choice<'T1, 'T2>) -> array : FlatList<'T> -> FlatList<'T1> * FlatList<'T2> diff --git a/src/FSharp.Collections.Immutable/Helper/ParallelArrays.fs b/src/FSharp.Collections.Immutable/Helper/ParallelArrays.fs index f004267b..92c83f6c 100644 --- a/src/FSharp.Collections.Immutable/Helper/ParallelArrays.fs +++ b/src/FSharp.Collections.Immutable/Helper/ParallelArrays.fs @@ -45,7 +45,9 @@ let chunks (processors : int) (length : int) : struct (int * int) array = struct (first, first + min chunk (length - first)) ) -/// Runs an action on the chunks of the indices from 0 to in parallel. +/// +/// Runs an action on the chunks of the indices from 0 to in parallel. +/// /// /// The action takes the first index of its chunk and the index after its last one. A loop per chunk calls no delegate per /// element. @@ -62,7 +64,9 @@ let inline forEachChunk (length : int) ([] action : int -> int - ) |> ignore -/// The values that the chooser returns for the elements, in the order of the elements. +/// +/// The values that the chooser returns for the elements, in the order of the elements. +/// /// /// The chooser runs in parallel into an array of value options, which a sequential pass compacts. Calling /// @@ -88,7 +92,9 @@ let choose (chooser : 'T -> 'U voption) (array : 'T array) : 'U array = result -/// The comparer of the keys of a parallel grouping. +/// +/// The comparer of the keys of a parallel grouping. +/// /// /// /// Keys of type and compare as in @@ -112,7 +118,9 @@ let groupingComparer<'Key when 'Key : equality> () : IEqualityComparer<'Key> = else HashIdentity.Structural<'Key> -/// The keys of the projection, each with its elements; the order of both is not specified. +/// +/// The keys of the projection, each with its elements; the order of both is not specified. +/// /// /// The projection, the work of the caller, runs in parallel. The grouping then runs over the computed keys with /// @@ -144,7 +152,9 @@ let inline groupBy result -/// The index of the first element that satisfies the predicate, or ValueNone. +/// +/// The index of the first element that satisfies the predicate, or ValueNone. +/// /// /// The predicate runs in parallel. A match breaks the loop, which then starts only the elements before it, and the lowest /// index that broke the loop wins, as in @@ -167,7 +177,9 @@ let tryFindIndex (predicate : 'T -> bool) (array : 'T array) : int voption = else ValueNone -/// The value that the chooser returns for the first element with a value, or ValueNone. +/// +/// The value that the chooser returns for the first element with a value, or ValueNone. +/// /// /// The chooser runs in parallel. A value breaks the loop, which then starts only the elements before it, and the value of /// the lowest index wins. The function of FSharp.Core, @@ -201,7 +213,9 @@ let tryPick (chooser : 'T -> 'U voption) (array : 'T array) : 'U voption = chosen.Value -/// Pairs the elements of two arrays of the same length in parallel. +/// +/// Pairs the elements of two arrays of the same length in parallel. +/// /// /// The chunks are those of . /// From 0df112d4b1b17365ee48c15a0e8a687e52b3e56d Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Sun, 18 Oct 2026 17:22:43 +0200 Subject: [PATCH 12/14] fixup! test: `FlatList.Parallel` against the sequential functions and `Array.Parallel` A `` and a `` stay on one line each only when both are short; otherwise both span several lines. Co-Authored-By: Claude Opus 5.5 --- .../FlatList/FlatListParallelTests.fs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelTests.fs b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelTests.fs index c0723696..743f3e6f 100644 --- a/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelTests.fs +++ b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelTests.fs @@ -5,7 +5,9 @@ open System.Collections.Concurrent open FSharp.Collections.Immutable open Microsoft.VisualStudio.TestTools.UnitTesting -/// The inputs of the tests of FlatList.Parallel, as the cases of their rows. +/// +/// The inputs of the tests of FlatList.Parallel, as the cases of their rows. +/// /// /// MSTest names a row after its values, and the larger input would write ten thousand numbers into the name of each of /// its rows, so a row holds a case and the test takes the elements of the case. From fe67409509890ab911920988f354ca4523c67fb1 Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Sun, 18 Oct 2026 20:43:57 +0200 Subject: [PATCH 13/14] fixup! test: `FlatList.Parallel` against the sequential functions and `Array.Parallel` The assertion helpers of `main` are static classes with tupled arguments, so the calls of the `FlatList.Parallel` tests use them: `ExceptionAssert.Same` and `OverrideAssert.OptionMatches`. Co-Authored-By: Claude Sonnet 5.5 --- .../FlatList/FlatListParallelOptionTests.fs | 16 ++-- .../FlatList/FlatListParallelTests.fs | 92 ++++++++++--------- .../FlatList/FlatListParallelTupleTests.fs | 9 +- 3 files changed, 65 insertions(+), 52 deletions(-) diff --git a/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelOptionTests.fs b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelOptionTests.fs index 07f0657d..7e7d327d 100644 --- a/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelOptionTests.fs +++ b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelOptionTests.fs @@ -72,20 +72,22 @@ type FlatListParallelOptionTests () = let source = flatListOf input let found : int option = FlatList.Parallel.tryFind (isAbove 6) source - OverrideAssert.optionMatches - (FSharp.Collections.Immutable.FlatList.Parallel.tryFind (isAbove 6) source) - found + OverrideAssert.OptionMatches ( + FSharp.Collections.Immutable.FlatList.Parallel.tryFind (isAbove 6) source, + found, "Option.FlatList.Parallel.tryFind must match the default" + ) [] member _.``tryFindIndex returns an option`` (input : ParallelInput) = let source = flatListOf input let found : int option = FlatList.Parallel.tryFindIndex (isAbove 6) source - OverrideAssert.optionMatches - (FSharp.Collections.Immutable.FlatList.Parallel.tryFindIndex (isAbove 6) source) - found + OverrideAssert.OptionMatches ( + FSharp.Collections.Immutable.FlatList.Parallel.tryFindIndex (isAbove 6) source, + found, "Option.FlatList.Parallel.tryFindIndex must match the default" + ) [] member _.``tryPick takes a chooser of options and returns an option`` (input : ParallelInput) = @@ -112,7 +114,7 @@ type FlatListParallelOptionTests () = [] member _.``the functions reject a default flat list as Array.Parallel rejects a null array`` (name : string) = let struct (oracle, actual) = arrayRejections[name] - ExceptionAssert.same id oracle actual $"Option.FlatList.Parallel.%s{name} must reject a default flat list" + ExceptionAssert.Same (id, oracle, actual, $"Option.FlatList.Parallel.%s{name} must reject a default flat list") [] member _.``the functions without an option keep resolving to the default module`` () = diff --git a/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelTests.fs b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelTests.fs index 743f3e6f..f901e7f6 100644 --- a/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelTests.fs +++ b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelTests.fs @@ -306,11 +306,12 @@ type FlatListParallelTests () = [] member _.``init throws for a negative count as Array.Parallel does`` () = - ExceptionAssert.same - id - (fun () -> Array.Parallel.init -1 id |> ignore) - (fun () -> FlatList.Parallel.init -1 id |> ignore) + ExceptionAssert.Same ( + id, + (fun () -> Array.Parallel.init -1 id |> ignore), + (fun () -> FlatList.Parallel.init -1 id |> ignore), "FlatList.Parallel.init -1 must throw as Array.Parallel.init does" + ) // ---- Search ---- @@ -467,14 +468,15 @@ type FlatListParallelTests () = [] member _.``zip throws for different lengths as Array.Parallel does`` (first : int array, second : int array) = - ExceptionAssert.same - id - (fun () -> Array.Parallel.zip first second |> ignore) + ExceptionAssert.Same ( + id, + (fun () -> Array.Parallel.zip first second |> ignore), (fun () -> FlatList.Parallel.zip (FlatList.ofArray first) (FlatList.ofArray second) |> ignore - ) + ), "FlatList.Parallel.zip of flat lists of different lengths must throw as Array.Parallel.zip does" + ) [] member _.``zip divides every array length into chunks without overflow`` @@ -525,11 +527,12 @@ type FlatListParallelTests () = [] member _.``average throws for an empty flat list as Array.Parallel does`` () = - ExceptionAssert.same - id - (fun () -> Array.Parallel.average Array.empty |> ignore) - (fun () -> FlatList.Parallel.average FlatList.empty |> ignore) + ExceptionAssert.Same ( + id, + (fun () -> Array.Parallel.average Array.empty |> ignore), + (fun () -> FlatList.Parallel.average FlatList.empty |> ignore), "FlatList.Parallel.average of an empty flat list must throw as Array.Parallel.average does" + ) [] member _.``averageBy returns the average of the projections`` (input : ParallelInput) = @@ -543,14 +546,15 @@ type FlatListParallelTests () = [] member _.``averageBy throws for an empty flat list as Array.Parallel does`` () = - ExceptionAssert.same - id - (fun () -> Array.Parallel.averageBy float Array.empty |> ignore) + ExceptionAssert.Same ( + id, + (fun () -> Array.Parallel.averageBy float Array.empty |> ignore), (fun () -> FlatList.Parallel.averageBy float FlatList.empty |> ignore - ) + ), "FlatList.Parallel.averageBy of an empty flat list must throw as Array.Parallel.averageBy does" + ) [] member _.``iter visits every element once`` (input : ParallelInput) = @@ -589,11 +593,12 @@ type FlatListParallelTests () = [] member _.``max throws for an empty flat list as Array.Parallel does`` () = - ExceptionAssert.same - id - (fun () -> Array.Parallel.max Array.empty |> ignore) - (fun () -> FlatList.Parallel.max FlatList.empty |> ignore) + ExceptionAssert.Same ( + id, + (fun () -> Array.Parallel.max Array.empty |> ignore), + (fun () -> FlatList.Parallel.max FlatList.empty |> ignore), "FlatList.Parallel.max of an empty flat list must throw as Array.Parallel.max does" + ) [] member _.``maxBy returns the element with the greatest projection`` (input : ParallelInput) = @@ -618,11 +623,12 @@ type FlatListParallelTests () = [] member _.``maxBy throws for an empty flat list as Array.Parallel does`` () = - ExceptionAssert.same - id - (fun () -> Array.Parallel.maxBy abs Array.empty |> ignore) - (fun () -> FlatList.Parallel.maxBy abs FlatList.empty |> ignore) + ExceptionAssert.Same ( + id, + (fun () -> Array.Parallel.maxBy abs Array.empty |> ignore), + (fun () -> FlatList.Parallel.maxBy abs FlatList.empty |> ignore), "FlatList.Parallel.maxBy of an empty flat list must throw as Array.Parallel.maxBy does" + ) [] member _.``min returns the smallest element`` (input : ParallelInput) = @@ -635,11 +641,12 @@ type FlatListParallelTests () = [] member _.``min throws for an empty flat list as Array.Parallel does`` () = - ExceptionAssert.same - id - (fun () -> Array.Parallel.min Array.empty |> ignore) - (fun () -> FlatList.Parallel.min FlatList.empty |> ignore) + ExceptionAssert.Same ( + id, + (fun () -> Array.Parallel.min Array.empty |> ignore), + (fun () -> FlatList.Parallel.min FlatList.empty |> ignore), "FlatList.Parallel.min of an empty flat list must throw as Array.Parallel.min does" + ) [] member _.``minBy returns the element with the smallest projection`` (input : ParallelInput) = @@ -664,11 +671,12 @@ type FlatListParallelTests () = [] member _.``minBy throws for an empty flat list as Array.Parallel does`` () = - ExceptionAssert.same - id - (fun () -> Array.Parallel.minBy abs Array.empty |> ignore) - (fun () -> FlatList.Parallel.minBy abs FlatList.empty |> ignore) + ExceptionAssert.Same ( + id, + (fun () -> Array.Parallel.minBy abs Array.empty |> ignore), + (fun () -> FlatList.Parallel.minBy abs FlatList.empty |> ignore), "FlatList.Parallel.minBy of an empty flat list must throw as Array.Parallel.minBy does" + ) [] member _.``reduce combines the elements as the sequential reduce does for an associative and commutative function`` @@ -684,11 +692,12 @@ type FlatListParallelTests () = [] member _.``reduce throws for an empty flat list as Array.Parallel does`` () = - ExceptionAssert.same - id - (fun () -> Array.Parallel.reduce (+) Array.empty |> ignore) - (fun () -> FlatList.Parallel.reduce (+) FlatList.empty |> ignore) + ExceptionAssert.Same ( + id, + (fun () -> Array.Parallel.reduce (+) Array.empty |> ignore), + (fun () -> FlatList.Parallel.reduce (+) FlatList.empty |> ignore), "FlatList.Parallel.reduce of an empty flat list must throw as Array.Parallel.reduce does" + ) [] member _.``reduceBy combines the projections as Array.Parallel does`` (input : ParallelInput) = @@ -710,14 +719,15 @@ type FlatListParallelTests () = [] member _.``reduceBy throws for an empty flat list as Array.Parallel does`` () = - ExceptionAssert.same - id - (fun () -> Array.Parallel.reduceBy abs (+) Array.empty |> ignore) + ExceptionAssert.Same ( + id, + (fun () -> Array.Parallel.reduceBy abs (+) Array.empty |> ignore), (fun () -> FlatList.Parallel.reduceBy abs (+) FlatList.empty |> ignore - ) + ), "FlatList.Parallel.reduceBy of an empty flat list must throw as Array.Parallel.reduceBy does" + ) [] member _.``sum adds the elements`` (input : ParallelInput) = @@ -900,7 +910,7 @@ type FlatListParallelTests () = [] member _.``functions reject a default flat list as Array.Parallel rejects a null array`` (name : string) = let struct (oracle, actual) = arrayRejections[name] - ExceptionAssert.same id oracle actual $"FlatList.Parallel.%s{name} must reject a default flat list" + ExceptionAssert.Same (id, oracle, actual, $"FlatList.Parallel.%s{name} must reject a default flat list") [] member _.``sum and sumBy reject a default flat list where Array.Parallel dereferences a null array`` (name : string) = diff --git a/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelTupleTests.fs b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelTupleTests.fs index dab1f7d1..be6af851 100644 --- a/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelTupleTests.fs +++ b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListParallelTupleTests.fs @@ -48,14 +48,15 @@ type FlatListParallelTupleTests () = [] member _.``zip throws for different lengths as the default does`` () = - ExceptionAssert.same - id + ExceptionAssert.Same ( + id, (fun () -> FSharp.Collections.Immutable.FlatList.Parallel.zip array (FlatList.singleton 1) |> ignore - ) - (fun () -> FlatList.Parallel.zip array (FlatList.singleton 1) |> ignore) + ), + (fun () -> FlatList.Parallel.zip array (FlatList.singleton 1) |> ignore), "Tuple.FlatList.Parallel.zip must throw as the default zip does" + ) [] member _.``groupBy returns reference tuples`` (input : ParallelInput) = From 46729a264a08d7ddfc7b049883291e0782bbdfcc Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Mon, 19 Oct 2026 18:14:39 +0200 Subject: [PATCH 14/14] fixup! feat: `FlatList.Parallel` module with the vocabulary of the FSharp.Core `Array.Parallel` module The comments of ParallelArrays are the same kind of documentation as the ones of Arrays: the summary of the FSharp.Core function word for word, the parameters, the result, and remarks that give the complexity and say why the algorithm exists (the options or the reference tuples that FSharp.Core takes or returns, the loop that calls no delegate per element, the comparer of the keys). The callers of each algorithm are seealso. The functions without a counterpart in FSharp.Core say so. The code does not change. Co-Authored-By: Claude Sonnet 5.5 --- .../Helper/ParallelArrays.fs | 161 +++++++++++++++--- 1 file changed, 137 insertions(+), 24 deletions(-) diff --git a/src/FSharp.Collections.Immutable/Helper/ParallelArrays.fs b/src/FSharp.Collections.Immutable/Helper/ParallelArrays.fs index 92c83f6c..12ca9ff3 100644 --- a/src/FSharp.Collections.Immutable/Helper/ParallelArrays.fs +++ b/src/FSharp.Collections.Immutable/Helper/ParallelArrays.fs @@ -1,11 +1,29 @@ /// -/// Parallel algorithms over arrays for the functions of -/// that take or return options or reference tuples (ADR 0001, D3 and D11). +/// Parallel algorithms over arrays for the functions of +/// that take or return options or reference tuples +/// (ADR 0001, D3 and D11). /// /// -/// The algorithms take value options and tuple constructors instead, so that the default modules and the Tuple modules -/// share them and nothing is allocated per element. The callers check their arguments first, with the exceptions of -/// . +/// +/// The default functions of take and return value options and struct +/// tuples, and the functions of the module return reference +/// tuples, where the FSharp.Core functions take and return options and return reference tuples. Calling the FSharp.Core +/// function and converting would allocate for every element. The algorithms here take value options and tuple constructors +/// instead, so that the default modules and the modules of the namespace +/// share them and nothing is allocated per element. +/// +/// +/// The callers check their arguments first, with the exceptions of +/// , so the algorithms check nothing and document no +/// exceptions. +/// +/// +/// The summary of each function that mirrors a function of the FSharp.Core +/// module is the summary of that function word for word +/// (FSharp.Core 10.1), with ValueSome and ValueNone where it says Some and None, and so are the +/// descriptions of the parameters and of the result where the signatures agree. The remarks say what differs and why the +/// function exists. +/// /// module internal FSharp.Collections.Immutable.ParallelArrays @@ -18,6 +36,9 @@ open System.Threading.Tasks /// The chunks of a chunked parallel loop over elements on /// processors, each as its first index and the index after its last one. /// +/// The number of processors. +/// The number of elements. +/// The chunks, each as a struct tuple of its first index and the index after its last one. /// /// /// The chunks are those of the chunked loops of : one chunk @@ -29,6 +50,11 @@ open System.Threading.Tasks /// of a chunk adds only the elements that remain to its first index, because the first index plus the full chunk length /// can overflow for arrays near the maximum length on machines with more than 56 processors. /// +/// +/// FSharp.Core has no such function in its public API. It is a function of its own, so that +/// +/// and the tests share it, and the tests can check the arithmetic for lengths that no array can have. +/// /// let chunks (processors : int) (length : int) : struct (int * int) array = let chunk = @@ -48,9 +74,17 @@ let chunks (processors : int) (length : int) : struct (int * int) array = /// /// Runs an action on the chunks of the indices from 0 to in parallel. /// +/// The number of elements. +/// The action, which takes the first index of its chunk and the index after its last one. /// -/// The action takes the first index of its chunk and the index after its last one. A loop per chunk calls no delegate per -/// element. +/// +/// A loop per chunk calls no delegate per element. +/// +/// +/// FSharp.Core has no such function in its public API. It exists because a +/// over the +/// indices calls a delegate for every element, where the action of a chunk is inlined and loops over its indices. +/// /// let inline forEachChunk (length : int) ([] action : int -> int -> unit) = let bounds = chunks Environment.ProcessorCount length @@ -65,13 +99,26 @@ let inline forEachChunk (length : int) ([] action : int -> int - |> ignore /// -/// The values that the chooser returns for the elements, in the order of the elements. +/// Apply the given function to each element of the array. Return the array comprised of the results x for each +/// element where the function returns ValueSome x. /// +/// The function to generate value options from the elements. +/// The input array. +/// The array of results. /// -/// The chooser runs in parallel into an array of value options, which a sequential pass compacts. Calling +/// +/// Performs the operation in parallel using . The order in which the +/// given function is applied to elements of the input array is not specified. This is an O(n) operation, where n is the +/// length of the array. +/// +/// +/// It exists because /// -/// instead would convert every value option to an option. +/// takes a chooser that returns options, and would convert every value option to an option. The chooser runs in parallel +/// into an array of value options, which a sequential pass compacts. +/// /// +/// let choose (chooser : 'T -> 'U voption) (array : 'T array) : 'U array = let chosen = Array.Parallel.map chooser array let mutable count = 0 @@ -95,6 +142,10 @@ let choose (chooser : 'T -> 'U voption) (array : 'T array) : 'U array = /// /// The comparer of the keys of a parallel grouping. /// +/// +/// The comparer of F# structural equality, except that the keys of the types and +/// compare with . +/// /// /// /// Keys of type and compare as in @@ -108,6 +159,11 @@ let choose (chooser : 'T -> 'U voption) (array : 'T array) : 'U array = /// The function of FSharp.Core throws for such a key: its second pass cannot find /// the key that its first pass added. That failure is not mirrored. /// +/// +/// FSharp.Core has no such function in its public API. It exists because the grouping here runs over the keys with +/// , +/// which takes the comparer of the keys, and the comparer has to be that of FSharp.Core. +/// /// let groupingComparer<'Key when 'Key : equality> () : IEqualityComparer<'Key> = if @@ -119,10 +175,24 @@ let groupingComparer<'Key when 'Key : equality> () : IEqualityComparer<'Key> = HashIdentity.Structural<'Key> /// -/// The keys of the projection, each with its elements; the order of both is not specified. +/// Applies a key-generating function to each element of an array in parallel and yields an array of unique keys. Each +/// unique key contains an array of all elements that match to this key. /// +/// The function that builds an element of the result from a key and the array of its elements. +/// A function that transforms an element of the array into a comparable key. +/// The input array. +/// The result array. /// -/// The projection, the work of the caller, runs in parallel. The grouping then runs over the computed keys with +/// +/// Performs the operation in parallel using . The order in which the +/// given function is applied to elements of the input array is not specified. The order of the keys and values in the +/// result is also not specified. This is an O(n) operation, where n is the length of the array. +/// +/// +/// It exists because +/// +/// returns reference tuples. The projection, the work of the caller, runs in parallel. The grouping then runs over the +/// computed keys with /// /// and the comparer of , so that a /// key is a key like any other. The result has the order of @@ -130,7 +200,10 @@ let groupingComparer<'Key when 'Key : equality> () : IEqualityComparer<'Key> = /// one of the orders that /// /// allows. +/// /// +/// +/// let inline groupBy ([] tuple : 'Key -> 'T array -> 'R) (projection : 'T -> 'Key) @@ -153,13 +226,26 @@ let inline groupBy result /// -/// The index of the first element that satisfies the predicate, or ValueNone. +/// Returns the index of the first element in the array that satisfies the given predicate. Returns ValueNone if no +/// such element exists. /// +/// The function to test the input elements. +/// The input array. +/// The index of the first element that satisfies the predicate, or ValueNone. /// -/// The predicate runs in parallel. A match breaks the loop, which then starts only the elements before it, and the lowest -/// index that broke the loop wins, as in -/// . +/// +/// This is an O(n) operation in the worst case, where n is the length of the array. +/// +/// +/// It exists because +/// +/// returns an option, where the default functions return a value option. The predicate runs in parallel. A match breaks the +/// loop, which then starts only the elements before it, and the lowest index that broke the loop wins, as in the function +/// of FSharp.Core. +/// /// +/// +/// let tryFindIndex (predicate : 'T -> bool) (array : 'T array) : int voption = let result = Parallel.For ( @@ -178,15 +264,27 @@ let tryFindIndex (predicate : 'T -> bool) (array : 'T array) : int voption = ValueNone /// -/// The value that the chooser returns for the first element with a value, or ValueNone. +/// Applies the given function to successive elements, returning the first result where the function returns +/// ValueSome x for some x. If the function never returns ValueSome x then ValueNone is +/// returned. /// +/// The function to transform the array elements into value options. +/// The input array. +/// The first transformed element that is ValueSome x. /// -/// The chooser runs in parallel. A value breaks the loop, which then starts only the elements before it, and the value of -/// the lowest index wins. The function of FSharp.Core, -/// , -/// keeps every value in a concurrent dictionary by index; here a lock guards the value of the lowest index so far, and -/// only the elements that have a value take it. +/// +/// This is an O(n) operation in the worst case, where n is the length of the array. +/// +/// +/// It exists because +/// +/// takes a chooser that returns options, where the default functions use value options. The chooser runs in parallel. A +/// value breaks the loop, which then starts only the elements before it, and the value of the lowest index wins. The +/// function of FSharp.Core keeps every value in a concurrent dictionary by index; here a lock guards the value of the +/// lowest index so far, and only the elements that have a value take it. +/// /// +/// let tryPick (chooser : 'T -> 'U voption) (array : 'T array) : 'U voption = let gate = obj () let lowest = ref Int32.MaxValue @@ -214,11 +312,26 @@ let tryPick (chooser : 'T -> 'U voption) (array : 'T array) : 'U voption = chosen.Value /// -/// Pairs the elements of two arrays of the same length in parallel. +/// Combines the two arrays into an array of pairs. The two arrays must have equal lengths, otherwise an +/// is raised. /// +/// The function that builds an element of the result from the elements at the same index. +/// The first input array. +/// The second input array. +/// The array of tupled elements. /// -/// The chunks are those of . +/// +/// This is an O(n) operation, where n is the length of the arrays. The chunks are those of +/// . The callers check the lengths first +/// and raise its exception. +/// +/// +/// It exists because that function returns reference tuples, which allocate for every element, where the default function +/// returns struct tuples. +/// /// +/// +/// let inline zip ([] tuple : 'T1 -> 'T2 -> 'R) (array1 : 'T1 array) (array2 : 'T2 array) : 'R array = let result = Array.zeroCreate array1.Length