Skip to content

Array.Parallel - search functions (tryFindIndex,tryFind,tryPick) #14827

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Mar 14, 2023
40 changes: 40 additions & 0 deletions src/FSharp.Core/array.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1934,6 +1934,46 @@ module Array =
module Parallel =
open System.Threading.Tasks

[<CompiledName("TryFindIndex")>]
let tryFindIndex predicate (array: _[]) =
checkNonNull "array" array

let pResult =
Parallel.For(
0,
array.Length,
(fun i pState ->
if predicate array[i] then
pState.Break())
)

pResult.LowestBreakIteration |> Option.ofNullable |> Option.map int

[<CompiledName("TryFind")>]
let tryFind predicate (array: _[]) =
array |> tryFindIndex predicate |> Option.map (fun i -> array[i])
Comment on lines +1937 to +1954
Copy link
Contributor

@brianrourkeboll brianrourkeboll Mar 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not that it would matter most of the time (especially since this is Array.Parallel and not regular Array, and allocations are probably gonna happen anyway), but given that this is standard library code that probably won't change very often, it might not hurt to avoid the extra allocations here123:

Suggested change
[<CompiledName("TryFindIndex")>]
let tryFindIndex predicate (array: _[]) =
checkNonNull "array" array
let pResult =
Parallel.For(
0,
array.Length,
(fun i pState ->
if predicate array[i] then
pState.Break())
)
pResult.LowestBreakIteration |> Option.ofNullable |> Option.map int
[<CompiledName("TryFind")>]
let tryFind predicate (array: _[]) =
array |> tryFindIndex predicate |> Option.map (fun i -> array[i])
let inline tryFindIndexAux predicate (array: _ array) =
checkNonNull (nameof array) array
let pResult =
Parallel.For(
0,
array.Length,
(fun i pState ->
if predicate array[i] then
pState.Break())
)
pResult.LowestBreakIteration
[<CompiledName("TryFindIndex")>]
let tryFindIndex predicate (array: _ array) =
let i = tryFindIndexAux predicate array
if i.HasValue then Some (int (i.GetValueOrDefault()))
else None
[<CompiledName("TryFind")>]
let tryFind predicate (array: _ array) =
let i = tryFindIndexAux predicate array
if i.HasValue then Some array[int (i.GetValueOrDefault()]
else None

Footnotes

  1. 2 for tryFindIndex: one Some from Option.ofNullable and another from Option.map.

  2. 3 for tryFind: one Some from Option.ofNullable, another from the first Option.map, and a third from the outer Option.map.

  3. Or maybe I've just read too many Stephen Toub PRs 🙃.


[<CompiledName("TryPick")>]
let tryPick chooser (array: _[]) =
checkNonNull "array" array
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Someday, we should add a nullArg (nameof arg) codefix :)

Suggested change
checkNonNull "array" array
checkNonNull (nameof array) array

let allChosen = new System.Collections.Concurrent.ConcurrentDictionary<_, _>()

let pResult =
Parallel.For(
0,
array.Length,
(fun i pState ->
match chooser array[i] with
| None -> ()
| chosenElement ->
allChosen[i] <- chosenElement
pState.Break())
)

pResult.LowestBreakIteration
|> Option.ofNullable
|> Option.bind (fun i -> allChosen[int i])

[<CompiledName("Choose")>]
let choose chooser (array: 'T[]) =
checkNonNull "array" array
Expand Down
96 changes: 96 additions & 0 deletions src/FSharp.Core/array.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -3094,6 +3094,102 @@ module Array =
/// <summary>Provides parallel operations on arrays </summary>
module Parallel =

/// <summary>Returns the first element for which the given function returns <c>True</c>.
/// Returns None if no such element exists.</summary>
///
/// <param name="predicate">The function to test the input elements.</param>
/// <param name="array">The input array.</param>
///
/// <returns>The first element that satisfies the predicate, or None.</returns>
///
/// <exception cref="T:System.ArgumentNullException">Thrown when the input array is null.</exception>
///
/// <example id="para-tryfind-1">Try to find the first even number:
/// <code lang="fsharp">
/// let inputs = [| 1; 2; 3 |]
///
/// inputs |> Array.Parallel.tryFind (fun elm -> elm % 2 = 0)
/// </code>
/// Evaluates to <c>Some 2</c>.
/// </example>
///
/// <example id="para-tryfind-2">Try to find the first even number:
/// <code lang="fsharp">
/// let inputs = [| 1; 5; 3 |]
///
/// inputs |> Array.Parallel.tryFind (fun elm -> elm % 2 = 0)
/// </code>
/// Evaluates to <c>None</c>
/// </example>
[<CompiledName("TryFind")>]
[<Experimental("Experimental library feature, requires '--langversion:preview'")>]
val tryFind: predicate:('T -> bool) -> array:'T[] -> 'T option


/// <summary>Returns the index of the first element in the array
/// that satisfies the given predicate.
/// Returns <c>None</c> if no such element exists.</summary>
/// <param name="predicate">The function to test the input elements.</param>
/// <param name="array">The input array.</param>
///
/// <exception cref="T:System.ArgumentNullException">Thrown when the input array is null.</exception>
///
/// <returns>The index of the first element that satisfies the predicate, or None.</returns>
///
/// <example id="para-tryfindindex-1">Try to find the index of the first even number:
/// <code lang="fsharp">
/// let inputs = [| 1; 2; 3; 4; 5 |]
///
/// inputs |> Array.Parallel.tryFindIndex (fun elm -> elm % 2 = 0)
/// </code>
/// Evaluates to <c>Some 1</c>
/// </example>
///
/// <example id="para-tryfindindex-2">Try to find the index of the first even number:
/// <code lang="fsharp">
/// let inputs = [| 1; 3; 5; 7 |]
///
/// inputs |> Array.Parallel.tryFindIndex (fun elm -> elm % 2 = 0)
/// </code>
/// Evaluates to <c>None</c>
/// </example>
[<CompiledName("TryFindIndex")>]
[<Experimental("Experimental library feature, requires '--langversion:preview'")>]
val tryFindIndex : predicate:('T -> bool) -> array:'T[] -> int option

/// <summary>Applies the given function to successive elements, returning the first
/// result where the function returns <c>Some(x)</c> for some <c>x</c>. If the function
/// never returns <c>Some(x)</c> then <c>None</c> is returned.</summary>
///
/// <param name="chooser">The function to transform the array elements into options.</param>
/// <param name="array">The input array.</param>
///
/// <returns>The first transformed element that is <c>Some(x)</c>.</returns>
///
/// <exception cref="T:System.ArgumentNullException">Thrown when the input array is null.</exception>
///
/// <example id="para-trypick-1">
/// <code lang="fsharp">
/// let input = [| 1; 2; 3 |]
///
/// input |> Array.Parallel.tryPick (fun n -> if n % 2 = 0 then Some (string n) else None)
/// </code>
/// Evaluates to <c>Some 2</c>.
/// </example>
///
/// <example id="para-trypick-2">
/// <code lang="fsharp">
/// let input = [| 1; 2; 3 |]
///
/// input |> Array.Parallel.tryPick (fun n -> if n > 3 = 0 then Some (string n) else None)
/// </code>
/// Evaluates to <c>None</c>.
/// </example>
///
[<CompiledName("TryPick")>]
[<Experimental("Experimental library feature, requires '--langversion:preview'")>]
val tryPick: chooser:('T -> 'U option) -> array:'T[] -> 'U option

/// <summary>Apply the given function to each element of the array. Return
/// the array comprised of the results <c>x</c> for each element where
/// the function returns <c>Some(x)</c>.</summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ Microsoft.FSharp.Collections.Array4DModule: T[,,,] Create[T](Int32, Int32, Int32
Microsoft.FSharp.Collections.Array4DModule: T[,,,] Initialize[T](Int32, Int32, Int32, Int32, Microsoft.FSharp.Core.FSharpFunc`2[System.Int32,Microsoft.FSharp.Core.FSharpFunc`2[System.Int32,Microsoft.FSharp.Core.FSharpFunc`2[System.Int32,Microsoft.FSharp.Core.FSharpFunc`2[System.Int32,T]]]])
Microsoft.FSharp.Collections.Array4DModule: T[,,,] ZeroCreate[T](Int32, Int32, Int32, Int32)
Microsoft.FSharp.Collections.Array4DModule: Void Set[T](T[,,,], Int32, Int32, Int32, Int32, T)
Microsoft.FSharp.Collections.ArrayModule+Parallel: Microsoft.FSharp.Core.FSharpOption`1[System.Int32] TryFindIndex[T](Microsoft.FSharp.Core.FSharpFunc`2[T,System.Boolean], T[])
Microsoft.FSharp.Collections.ArrayModule+Parallel: Microsoft.FSharp.Core.FSharpOption`1[TResult] TryPick[T,TResult](Microsoft.FSharp.Core.FSharpFunc`2[T,Microsoft.FSharp.Core.FSharpOption`1[TResult]], T[])
Microsoft.FSharp.Collections.ArrayModule+Parallel: Microsoft.FSharp.Core.FSharpOption`1[T] TryFind[T](Microsoft.FSharp.Core.FSharpFunc`2[T,System.Boolean], T[])
Microsoft.FSharp.Collections.ArrayModule+Parallel: System.Tuple`2[T[],T[]] Partition[T](Microsoft.FSharp.Core.FSharpFunc`2[T,System.Boolean], T[])
Microsoft.FSharp.Collections.ArrayModule+Parallel: TResult[] Choose[T,TResult](Microsoft.FSharp.Core.FSharpFunc`2[T,Microsoft.FSharp.Core.FSharpOption`1[TResult]], T[])
Microsoft.FSharp.Collections.ArrayModule+Parallel: TResult[] Collect[T,TResult](Microsoft.FSharp.Core.FSharpFunc`2[T,TResult[]], T[])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ Microsoft.FSharp.Collections.Array4DModule: T[,,,] Create[T](Int32, Int32, Int32
Microsoft.FSharp.Collections.Array4DModule: T[,,,] Initialize[T](Int32, Int32, Int32, Int32, Microsoft.FSharp.Core.FSharpFunc`2[System.Int32,Microsoft.FSharp.Core.FSharpFunc`2[System.Int32,Microsoft.FSharp.Core.FSharpFunc`2[System.Int32,Microsoft.FSharp.Core.FSharpFunc`2[System.Int32,T]]]])
Microsoft.FSharp.Collections.Array4DModule: T[,,,] ZeroCreate[T](Int32, Int32, Int32, Int32)
Microsoft.FSharp.Collections.Array4DModule: Void Set[T](T[,,,], Int32, Int32, Int32, Int32, T)
Microsoft.FSharp.Collections.ArrayModule+Parallel: Microsoft.FSharp.Core.FSharpOption`1[System.Int32] TryFindIndex[T](Microsoft.FSharp.Core.FSharpFunc`2[T,System.Boolean], T[])
Microsoft.FSharp.Collections.ArrayModule+Parallel: Microsoft.FSharp.Core.FSharpOption`1[TResult] TryPick[T,TResult](Microsoft.FSharp.Core.FSharpFunc`2[T,Microsoft.FSharp.Core.FSharpOption`1[TResult]], T[])
Microsoft.FSharp.Collections.ArrayModule+Parallel: Microsoft.FSharp.Core.FSharpOption`1[T] TryFind[T](Microsoft.FSharp.Core.FSharpFunc`2[T,System.Boolean], T[])
Microsoft.FSharp.Collections.ArrayModule+Parallel: System.Tuple`2[T[],T[]] Partition[T](Microsoft.FSharp.Core.FSharpFunc`2[T,System.Boolean], T[])
Microsoft.FSharp.Collections.ArrayModule+Parallel: TResult[] Choose[T,TResult](Microsoft.FSharp.Core.FSharpFunc`2[T,Microsoft.FSharp.Core.FSharpOption`1[TResult]], T[])
Microsoft.FSharp.Collections.ArrayModule+Parallel: TResult[] Collect[T,TResult](Microsoft.FSharp.Core.FSharpFunc`2[T,TResult[]], T[])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ Microsoft.FSharp.Collections.Array4DModule: T[,,,] Create[T](Int32, Int32, Int32
Microsoft.FSharp.Collections.Array4DModule: T[,,,] Initialize[T](Int32, Int32, Int32, Int32, Microsoft.FSharp.Core.FSharpFunc`2[System.Int32,Microsoft.FSharp.Core.FSharpFunc`2[System.Int32,Microsoft.FSharp.Core.FSharpFunc`2[System.Int32,Microsoft.FSharp.Core.FSharpFunc`2[System.Int32,T]]]])
Microsoft.FSharp.Collections.Array4DModule: T[,,,] ZeroCreate[T](Int32, Int32, Int32, Int32)
Microsoft.FSharp.Collections.Array4DModule: Void Set[T](T[,,,], Int32, Int32, Int32, Int32, T)
Microsoft.FSharp.Collections.ArrayModule+Parallel: Microsoft.FSharp.Core.FSharpOption`1[System.Int32] TryFindIndex[T](Microsoft.FSharp.Core.FSharpFunc`2[T,System.Boolean], T[])
Microsoft.FSharp.Collections.ArrayModule+Parallel: Microsoft.FSharp.Core.FSharpOption`1[TResult] TryPick[T,TResult](Microsoft.FSharp.Core.FSharpFunc`2[T,Microsoft.FSharp.Core.FSharpOption`1[TResult]], T[])
Microsoft.FSharp.Collections.ArrayModule+Parallel: Microsoft.FSharp.Core.FSharpOption`1[T] TryFind[T](Microsoft.FSharp.Core.FSharpFunc`2[T,System.Boolean], T[])
Microsoft.FSharp.Collections.ArrayModule+Parallel: System.Tuple`2[T[],T[]] Partition[T](Microsoft.FSharp.Core.FSharpFunc`2[T,System.Boolean], T[])
Microsoft.FSharp.Collections.ArrayModule+Parallel: TResult[] Choose[T,TResult](Microsoft.FSharp.Core.FSharpFunc`2[T,Microsoft.FSharp.Core.FSharpOption`1[TResult]], T[])
Microsoft.FSharp.Collections.ArrayModule+Parallel: TResult[] Collect[T,TResult](Microsoft.FSharp.Core.FSharpFunc`2[T,TResult[]], T[])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ Microsoft.FSharp.Collections.Array4DModule: T[,,,] Create[T](Int32, Int32, Int32
Microsoft.FSharp.Collections.Array4DModule: T[,,,] Initialize[T](Int32, Int32, Int32, Int32, Microsoft.FSharp.Core.FSharpFunc`2[System.Int32,Microsoft.FSharp.Core.FSharpFunc`2[System.Int32,Microsoft.FSharp.Core.FSharpFunc`2[System.Int32,Microsoft.FSharp.Core.FSharpFunc`2[System.Int32,T]]]])
Microsoft.FSharp.Collections.Array4DModule: T[,,,] ZeroCreate[T](Int32, Int32, Int32, Int32)
Microsoft.FSharp.Collections.Array4DModule: Void Set[T](T[,,,], Int32, Int32, Int32, Int32, T)
Microsoft.FSharp.Collections.ArrayModule+Parallel: Microsoft.FSharp.Core.FSharpOption`1[System.Int32] TryFindIndex[T](Microsoft.FSharp.Core.FSharpFunc`2[T,System.Boolean], T[])
Microsoft.FSharp.Collections.ArrayModule+Parallel: Microsoft.FSharp.Core.FSharpOption`1[TResult] TryPick[T,TResult](Microsoft.FSharp.Core.FSharpFunc`2[T,Microsoft.FSharp.Core.FSharpOption`1[TResult]], T[])
Microsoft.FSharp.Collections.ArrayModule+Parallel: Microsoft.FSharp.Core.FSharpOption`1[T] TryFind[T](Microsoft.FSharp.Core.FSharpFunc`2[T,System.Boolean], T[])
Microsoft.FSharp.Collections.ArrayModule+Parallel: System.Tuple`2[T[],T[]] Partition[T](Microsoft.FSharp.Core.FSharpFunc`2[T,System.Boolean], T[])
Microsoft.FSharp.Collections.ArrayModule+Parallel: TResult[] Choose[T,TResult](Microsoft.FSharp.Core.FSharpFunc`2[T,Microsoft.FSharp.Core.FSharpOption`1[TResult]], T[])
Microsoft.FSharp.Collections.ArrayModule+Parallel: TResult[] Collect[T,TResult](Microsoft.FSharp.Core.FSharpFunc`2[T,TResult[]], T[])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -955,17 +955,16 @@ type ArrayModule() =
let intArr = [| 1..10 |]
let seq = Array.toSeq intArr
let sum = Seq.sum seq
Assert.AreEqual(55, sum)

[<Fact>]
member this.TryPick() =
Assert.AreEqual(55, sum)

member private _.TryPickTester tryPickInt tryPickString =
// integer array
let intArr = [| 1..10 |]
let funcInt x =
match x with
| _ when x % 3 = 0 -> Some (x.ToString())
| _ -> None
let resultInt = Array.tryPick funcInt intArr
let resultInt = tryPickInt funcInt intArr
if resultInt <> Some "3" then Assert.Fail()

// string array
Expand All @@ -974,20 +973,26 @@ type ArrayModule() =
match x with
| "good" -> Some (x.ToString())
| _ -> None
let resultStr = Array.tryPick funcStr strArr
let resultStr = tryPickString funcStr strArr
if resultStr <> None then Assert.Fail()

// empty array
let emptyArr:int[] = [| |]
let resultEpt = Array.tryPick funcInt emptyArr
let resultEpt = tryPickInt funcInt emptyArr
if resultEpt <> None then Assert.Fail()

// null array
let nullArr = null:string[]
CheckThrowsArgumentNullException (fun () -> Array.tryPick funcStr nullArr |> ignore)
CheckThrowsArgumentNullException (fun () -> tryPickString funcStr nullArr |> ignore)

()

[<Fact>]
member this.TryPick() = this.TryPickTester Array.tryPick Array.tryPick

[<Fact>]
member this.ParallelTryPick() = this.TryPickTester Array.Parallel.tryPick Array.Parallel.tryPick

[<Fact>]
member this.Fold() =
// integer array
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -972,25 +972,31 @@ type ArrayModule2() =

()

[<Fact>]
member this.TryFind() =
member private _.TryFindTester tryFindInts tryFindStrings =
// integer array
let resultInt = [|1..10|] |> Array.tryFind (fun x -> x%7 = 0)
let resultInt = [|1..10|] |> tryFindInts (fun x -> x%7 = 0)
if resultInt <> Some 7 then Assert.Fail()

// string array
let resultStr = [|"Lists"; "are"; "commonly" ; "list" |] |> Array.tryFind (fun (x:string) -> x.Length > 4)
let resultStr = [|"Lists"; "are"; "commonly" ; "list" |] |> tryFindStrings (fun (x:string) -> x.Length > 4)
if resultStr <> Some "Lists" then Assert.Fail()

// empty array
let resultEpt =[||] |> Array.tryFind (fun x -> x%7 = 0)
let resultEpt =[||] |> tryFindInts (fun x -> x%7 = 0)
if resultEpt <> None then Assert.Fail()

// null array
let nullArr = null:string[]
CheckThrowsArgumentNullException (fun () -> Array.tryFind (fun (x:string) -> x.Length > 4) nullArr |> ignore)
CheckThrowsArgumentNullException (fun () -> tryFindStrings (fun (x:string) -> x.Length > 4) nullArr |> ignore)

()

[<Fact>]
member this.TryFind() = this.TryFindTester Array.tryFind Array.tryFind

[<Fact>]
member this.ParallelTryFind() = this.TryFindTester Array.Parallel.tryFind Array.Parallel.tryFind


[<Fact>]
member this.TryFindBack() =
Expand All @@ -1016,26 +1022,30 @@ type ArrayModule2() =

()

[<Fact>]
member this.TryFindIndex() =
member private _.TryFindIndexTester tryFindIdxInt tryFindIdxString =
// integer array
let resultInt = [|1..10|] |> Array.tryFindIndex (fun x -> x%7 = 0)
let resultInt = [|1..10|] |> tryFindIdxInt (fun x -> x%7 = 0)
if resultInt <> Some 6 then Assert.Fail()

// string array
let resultStr = [|"Lists"; "are"; "commonly" ; "list" |] |> Array.tryFindIndex (fun (x:string) -> x.Length > 4)
let resultStr = [|"Lists"; "are"; "commonly" ; "list" |] |> tryFindIdxString (fun (x:string) -> x.Length > 4)
if resultStr <> Some 0 then Assert.Fail()

// empty array
let resultEpt =[||] |> Array.tryFindIndex (fun x -> x % 7 = 0)
let resultEpt =[||] |> tryFindIdxInt (fun x -> x % 7 = 0)
if resultEpt <> None then Assert.Fail()

// null array
let nullArr = null:string[]
CheckThrowsArgumentNullException (fun () -> Array.tryFindIndex (fun (x:string) -> x.Length > 4) nullArr |> ignore)
CheckThrowsArgumentNullException (fun () -> tryFindIdxString (fun (x:string) -> x.Length > 4) nullArr |> ignore)

()

[<Fact>]
member this.TryFindIndex() = this.TryFindIndexTester Array.tryFindIndex Array.tryFindIndex

[<Fact>]
member this.ParallelTryFindIndex() = this.TryFindIndexTester Array.Parallel.tryFindIndex Array.Parallel.tryFindIndex
[<Fact>]
member this.TryFindIndexBack() =
// integer array
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ let consistency name sqs ls arr =
(sqs = arr) |@ (sprintf "Seq.%s = '%A', Array.%s = '%A'" name sqs name arr) .&.
(ls = arr) |@ (sprintf "List.%s = '%A', Array.%s = '%A'" name ls name arr)

let consistencyIncludingParallel name sqs ls arr paraArr =
consistency name sqs ls arr .&.
(paraArr = arr) |@ (sprintf "Parallel.%s = '%A', Array.%s = '%A'" name paraArr name arr)

let allPairs<'a when 'a : equality> (xs : list<'a>) (xs2 : list<'a>) =
let s = xs |> Seq.allPairs xs2 |> Seq.toArray
Expand Down Expand Up @@ -1104,7 +1107,8 @@ let tryFind<'a when 'a : equality> (xs : 'a []) predicate =
let s = xs |> Seq.tryFind predicate
let l = xs |> List.ofArray |> List.tryFind predicate
let a = xs |> Array.tryFind predicate
consistency "tryFind" s l a
let pa = xs |> Array.Parallel.tryFind predicate
consistencyIncludingParallel "tryFind" s l a pa

[<Fact>]
let ``tryFind is consistent`` () =
Expand All @@ -1128,7 +1132,8 @@ let tryFindIndex<'a when 'a : equality> (xs : 'a []) predicate =
let s = xs |> Seq.tryFindIndex predicate
let l = xs |> List.ofArray |> List.tryFindIndex predicate
let a = xs |> Array.tryFindIndex predicate
consistency "tryFindIndex" s l a
let pa = xs |> Array.Parallel.tryFindIndex predicate
consistencyIncludingParallel "tryFindIndex" s l a pa

[<Fact>]
let ``tryFindIndex is consistent`` () =
Expand Down Expand Up @@ -1188,7 +1193,8 @@ let tryPick<'a when 'a : comparison> (xs : 'a []) f =
let s = xs |> Seq.tryPick f
let l = xs |> List.ofArray |> List.tryPick f
let a = xs |> Array.tryPick f
consistency "tryPick" s l a
let pa = xs |> Array.Parallel.tryPick f
consistencyIncludingParallel "tryPick" s l a pa

[<Fact>]
let ``tryPick is consistent`` () =
Expand Down