diff --git a/README.md b/README.md index dcc1cbf..86f4c92 100644 --- a/README.md +++ b/README.md @@ -214,7 +214,7 @@ The `TaskSeq` project already has a wide array of functions and functionalities, - [x] `chunkBySize` / `windowed` (see [#258]) - [ ] `compareWith` - [ ] `distinct` - - [ ] `exists2` / `map2` / `fold2` / `iter2` and related '2'-functions + - [ ] `exists2` / `map2` / `iter2` and related '2'-functions (partial: `exists2`, `forall2`, `forall2Async`, `fold2`, `fold2Async` done) - [ ] `mapFold` - [x] `pairwise` (see [#293]) - [ ] `allpairs` / `permute` / `distinct` / `distinctBy` @@ -283,18 +283,18 @@ This is what has been implemented so far, is planned or skipped: | ✅ [#83][] | `except` | `except` | | | | ✅ [#83][] | | `exceptOfSeq` | | | | ✅ [#70][] | `exists` | `exists` | `existsAsync` | | -| | `exists2` | `exists2` | | | +| ✅ | | `exists2` | | | | ✅ [#23][] | `filter` | `filter` | `filterAsync` | | | ✅ [#23][] | `find` | `find` | `findAsync` | | | 🚫 | `findBack` | | | [note #2](#note2 "Because of the async nature of TaskSeq sequences, iterating from the back would be bad practice. Instead, materialize the sequence to a list or array and then apply the 'Back' iterators.") | | ✅ [#68][] | `findIndex` | `findIndex` | `findIndexAsync` | | | 🚫 | `findIndexBack` | n/a | n/a | [note #2](#note2 "Because of the async nature of TaskSeq sequences, iterating from the back would be bad practice. Instead, materialize the sequence to a list or array and then apply the 'Back' iterators.") | | ✅ [#2][] | `fold` | `fold` | `foldAsync` | | -| | `fold2` | `fold2` | `fold2Async` | | +| ✅ | | `fold2` | `fold2Async` | | | 🚫 | `foldBack` | | | [note #2](#note2 "Because of the async nature of TaskSeq sequences, iterating from the back would be bad practice. Instead, materialize the sequence to a list or array and then apply the 'Back' iterators.") | | 🚫 | `foldBack2` | | | [note #2](#note2 "Because of the async nature of TaskSeq sequences, iterating from the back would be bad practice. Instead, materialize the sequence to a list or array and then apply the 'Back' iterators.") | | ✅ [#240][]| `forall` | `forall` | `forallAsync` | | -| | `forall2` | `forall2` | `forall2Async` | | +| ✅ | | `forall2` | `forall2Async` | | | ❓ | `groupBy` | `groupBy` | `groupByAsync` | [note #1](#note1 "These functions require a form of pre-materializing through 'TaskSeq.cache', similar to the approach taken in the corresponding 'Seq' functions. It doesn't make much sense to have a cached async sequence. However, 'AsyncSeq' does implement these, so we'll probably do so eventually as well.") | | ✅ [#23][] | `head` | `head` | | | | ✅ [#68][] | `indexed` | `indexed` | | | diff --git a/release-notes.txt b/release-notes.txt index 5c0b97e..377f80f 100644 --- a/release-notes.txt +++ b/release-notes.txt @@ -17,6 +17,7 @@ Release notes: - adds TaskSeq.unfold and TaskSeq.unfoldAsync, #289 - adds TaskSeq.chunkBySize (closes #258) and TaskSeq.windowed, #289 - fixes: CancellationToken passed to GetAsyncEnumerator is now honored in MoveNextAsync, #179 + - adds TaskSeq.exists2, forall2, forall2Async, fold2, fold2Async 0.5.0 - update engineering to .NET 9/10 diff --git a/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj b/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj index 89f8d94..f14dffd 100644 --- a/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj +++ b/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj @@ -23,6 +23,7 @@ + diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Exists2Forall2Fold2.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Exists2Forall2Fold2.Tests.fs new file mode 100644 index 0000000..9cea3fd --- /dev/null +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Exists2Forall2Fold2.Tests.fs @@ -0,0 +1,477 @@ +module TaskSeq.Tests.Exists2Forall2Fold2 + +open System.Text + +open Xunit +open FsUnit.Xunit + +open FSharp.Control + +// +// TaskSeq.exists2 +// TaskSeq.forall2 +// TaskSeq.forall2Async +// TaskSeq.fold2 +// TaskSeq.fold2Async +// + + +/////////// +// exists2 +/////////// + +module Exists2EmptySeq = + [] + let ``Null source is invalid`` () = + assertNullArg + <| fun () -> TaskSeq.exists2 (fun _ _ -> false) null TaskSeq.empty + + assertNullArg + <| fun () -> TaskSeq.exists2 (fun _ _ -> false) TaskSeq.empty null + + assertNullArg + <| fun () -> TaskSeq.exists2 (fun _ _ -> false) null null + + [)>] + let ``TaskSeq-exists2 returns false when both sources are empty`` variant = + TaskSeq.exists2 (fun _ _ -> true) (Gen.getEmptyVariant variant) (Gen.getEmptyVariant variant) + |> Task.map (should be False) + + [)>] + let ``TaskSeq-exists2 returns false when first source is empty`` variant = + TaskSeq.exists2 (fun _ _ -> true) (Gen.getEmptyVariant variant) (TaskSeq.ofList [ 1; 2; 3 ]) + |> Task.map (should be False) + + [)>] + let ``TaskSeq-exists2 returns false when second source is empty`` variant = + TaskSeq.exists2 (fun _ _ -> true) (TaskSeq.ofList [ 1; 2; 3 ]) (Gen.getEmptyVariant variant) + |> Task.map (should be False) + + +module Exists2Immutable = + [)>] + let ``TaskSeq-exists2 sad path returns false when no pair matches`` variant = + TaskSeq.exists2 (fun x y -> x = y && x > 100) (Gen.getSeqImmutable variant) (Gen.getSeqImmutable variant) + |> Task.map (should be False) + + [)>] + let ``TaskSeq-exists2 happy path returns true when first pair matches`` variant = + // source1 and source2 are both 1..10; predicate (=) matches every pair + TaskSeq.exists2 (=) (Gen.getSeqImmutable variant) (Gen.getSeqImmutable variant) + |> Task.map (should be True) + + [)>] + let ``TaskSeq-exists2 happy path finds pair in middle of seq`` variant = + // source1 = 1..10, source2 = 1..10; pair (5,5) satisfies the predicate + TaskSeq.exists2 (fun x y -> x = 5 && y = 5) (Gen.getSeqImmutable variant) (Gen.getSeqImmutable variant) + |> Task.map (should be True) + + [)>] + let ``TaskSeq-exists2 happy path finds pair at end of seq`` variant = + TaskSeq.exists2 (fun x y -> x = 10 && y = 10) (Gen.getSeqImmutable variant) (Gen.getSeqImmutable variant) + |> Task.map (should be True) + + [] + let ``TaskSeq-exists2 stops at shorter sequence - first shorter`` () = + // source1 = [1;2;3], source2 = [1;2;3;100;200] + // predicate checks if sum > 50; without truncation, pairs (4,100)+(5,200) would match + TaskSeq.exists2 (fun x y -> x + y > 50) (TaskSeq.ofList [ 1; 2; 3 ]) (TaskSeq.ofList [ 1; 2; 3; 100; 200 ]) + |> Task.map (should be False) + + [] + let ``TaskSeq-exists2 stops at shorter sequence - second shorter`` () = + TaskSeq.exists2 (fun x y -> x + y > 50) (TaskSeq.ofList [ 1; 2; 3; 100; 200 ]) (TaskSeq.ofList [ 1; 2; 3 ]) + |> Task.map (should be False) + + [] + let ``TaskSeq-exists2 works with different element types`` () = + TaskSeq.exists2 (fun (x: int) (y: string) -> string x = y) (TaskSeq.ofList [ 1; 2; 3 ]) (TaskSeq.ofList [ "1"; "2"; "3" ]) + |> Task.map (should be True) + + +module Exists2SideEffects = + [] + let ``TaskSeq-exists2 _specialcase_ stops evaluating after first match`` () = task { + let mutable i = 0 + let mutable j = 0 + + let ts1 = taskSeq { + for _ in 0..9 do + i <- i + 1 + yield i + } + + let ts2 = taskSeq { + for _ in 0..9 do + j <- j + 1 + yield j + } + + // predicate matches on second pair (2, 2) + let! found = TaskSeq.exists2 (fun x y -> x = 2 && y = 2) ts1 ts2 + found |> should be True + i |> should equal 2 // only partial evaluation + j |> should equal 2 + } + + [] + let ``TaskSeq-exists2 _specialcase_ evaluates all pairs when not found`` () = task { + let mutable i = 0 + let mutable j = 0 + + let ts1 = taskSeq { + for _ in 0..9 do + i <- i + 1 + yield i + } + + let ts2 = taskSeq { + for _ in 0..9 do + j <- j + 1 + yield j + } + + let! found = TaskSeq.exists2 (fun x y -> x = 999 && y = 999) ts1 ts2 + found |> should be False + i |> should equal 10 + j |> should equal 10 + } + + +/////////// +// forall2 +/////////// + +module Forall2EmptySeq = + [] + let ``Null source is invalid`` () = + assertNullArg + <| fun () -> TaskSeq.forall2 (fun _ _ -> true) null TaskSeq.empty + + assertNullArg + <| fun () -> TaskSeq.forall2 (fun _ _ -> true) TaskSeq.empty null + + assertNullArg + <| fun () -> TaskSeq.forall2Async (fun _ _ -> Task.fromResult true) null TaskSeq.empty + + assertNullArg + <| fun () -> TaskSeq.forall2Async (fun _ _ -> Task.fromResult true) TaskSeq.empty null + + [)>] + let ``TaskSeq-forall2 always returns true when both empty`` variant = + TaskSeq.forall2 (fun _ _ -> false) (Gen.getEmptyVariant variant) (Gen.getEmptyVariant variant) + |> Task.map (should be True) + + [)>] + let ``TaskSeq-forall2Async always returns true when both empty`` variant = + TaskSeq.forall2Async (fun _ _ -> Task.fromResult false) (Gen.getEmptyVariant variant) (Gen.getEmptyVariant variant) + |> Task.map (should be True) + + [)>] + let ``TaskSeq-forall2 always returns true when first is empty`` variant = + TaskSeq.forall2 (fun _ _ -> false) (Gen.getEmptyVariant variant) (TaskSeq.ofList [ 1; 2; 3 ]) + |> Task.map (should be True) + + [)>] + let ``TaskSeq-forall2 always returns true when second is empty`` variant = + TaskSeq.forall2 (fun _ _ -> false) (TaskSeq.ofList [ 1; 2; 3 ]) (Gen.getEmptyVariant variant) + |> Task.map (should be True) + + +module Forall2Immutable = + [)>] + let ``TaskSeq-forall2 sad path returns false when some pair fails`` variant = + // source1 = source2 = 1..10; predicate: all x = y but x must also be < 5 + TaskSeq.forall2 (fun x y -> x = y && x < 5) (Gen.getSeqImmutable variant) (Gen.getSeqImmutable variant) + |> Task.map (should be False) + + [)>] + let ``TaskSeq-forall2Async sad path returns false when some pair fails`` variant = + TaskSeq.forall2Async (fun x y -> task { return x = y && x < 5 }) (Gen.getSeqImmutable variant) (Gen.getSeqImmutable variant) + |> Task.map (should be False) + + [)>] + let ``TaskSeq-forall2 happy path returns true when all pairs satisfy predicate`` variant = + // source1 = source2 = 1..10; predicate: x = y always true + TaskSeq.forall2 (=) (Gen.getSeqImmutable variant) (Gen.getSeqImmutable variant) + |> Task.map (should be True) + + [)>] + let ``TaskSeq-forall2Async happy path returns true when all pairs satisfy predicate`` variant = + TaskSeq.forall2Async (fun x y -> task { return x = y }) (Gen.getSeqImmutable variant) (Gen.getSeqImmutable variant) + |> Task.map (should be True) + + [] + let ``TaskSeq-forall2 stops at shorter sequence - longer does not affect result`` () = + // source1 = [1;2;3], source2 = [1;2;3;0;0] + // Without truncation, pairs (4,0) would fail the (=) predicate + // With truncation at length 3, only (1,1) (2,2) (3,3) are checked → all pass + TaskSeq.forall2 (=) (TaskSeq.ofList [ 1; 2; 3 ]) (TaskSeq.ofList [ 1; 2; 3; 0; 0 ]) + |> Task.map (should be True) + + [] + let ``TaskSeq-forall2 stops at shorter sequence - second shorter`` () = + TaskSeq.forall2 (=) (TaskSeq.ofList [ 1; 2; 3; 0; 0 ]) (TaskSeq.ofList [ 1; 2; 3 ]) + |> Task.map (should be True) + + [] + let ``TaskSeq-forall2 works with different element types`` () = + TaskSeq.forall2 (fun (x: int) (y: string) -> string x = y) (TaskSeq.ofList [ 1; 2; 3 ]) (TaskSeq.ofList [ "1"; "2"; "3" ]) + |> Task.map (should be True) + + [] + let ``TaskSeq-forall2Async works with different element types`` () = + TaskSeq.forall2Async + (fun (x: int) (y: string) -> task { return string x = y }) + (TaskSeq.ofList [ 1; 2; 3 ]) + (TaskSeq.ofList [ "1"; "2"; "3" ]) + |> Task.map (should be True) + + +module Forall2SideEffects = + [] + let ``TaskSeq-forall2 _specialcase_ stops evaluating after first false pair`` () = task { + let mutable i = 0 + let mutable j = 0 + + let ts1 = taskSeq { + for _ in 0..9 do + i <- i + 1 + yield i + } + + let ts2 = taskSeq { + for _ in 0..9 do + j <- j + 1 + yield j * 2 // offsets from ts1 after first item + } + + // pair (1,2) fails: 1 = 2 is false + let! result = TaskSeq.forall2 (=) ts1 ts2 + result |> should be False + i |> should equal 1 // stopped at first pair + j |> should equal 1 + } + + [] + let ``TaskSeq-forall2Async _specialcase_ stops evaluating after first false pair`` () = task { + let mutable i = 0 + let mutable j = 0 + + let ts1 = taskSeq { + for _ in 0..9 do + i <- i + 1 + yield i + } + + let ts2 = taskSeq { + for _ in 0..9 do + j <- j + 1 + yield j * 2 + } + + let! result = TaskSeq.forall2Async (fun x y -> task { return x = y }) ts1 ts2 + result |> should be False + i |> should equal 1 + j |> should equal 1 + } + + [] + let ``TaskSeq-forall2 mutated state can change result across iterations`` () = task { + let mutable i = 0 + + let ts1 = taskSeq { + for _ in 0..9 do + i <- i + 1 + yield i + } + + // Compare ts1 with a fixed sequence that always yields 1..10 + let staticSeq = TaskSeq.ofList [ 1..10 ] + + // first iteration: ts1 = 1..10, fixed = 1..10 → equal pairs → true + let! result = TaskSeq.forall2 (fun x y -> x = y) ts1 staticSeq + result |> should be True + i |> should equal 10 + + // second iteration: ts1 = 11..20 (side effects advance), fixed = 1..10 (fresh) → not equal → false + let! result = TaskSeq.forall2 (fun x y -> x = y) ts1 staticSeq + result |> should be False + i |> should equal 11 // stopped at first mismatch + } + + [] + let ``TaskSeq-forall2Async mutated state can change result across iterations`` () = task { + let mutable i = 0 + + let ts1 = taskSeq { + for _ in 0..9 do + i <- i + 1 + yield i + } + + let staticSeq = TaskSeq.ofList [ 1..10 ] + + let! result = TaskSeq.forall2Async (fun x y -> task { return x = y }) ts1 staticSeq + + result |> should be True + + let! result = TaskSeq.forall2Async (fun x y -> task { return x = y }) ts1 staticSeq + + result |> should be False + i |> should equal 11 + } + + +/////////// +// fold2 +/////////// + +module Fold2EmptySeq = + [] + let ``Null source is invalid`` () = + assertNullArg + <| fun () -> TaskSeq.fold2 (fun _ _ _ -> 0) 0 null TaskSeq.empty + + assertNullArg + <| fun () -> TaskSeq.fold2 (fun _ _ _ -> 0) 0 TaskSeq.empty null + + assertNullArg + <| fun () -> TaskSeq.fold2Async (fun _ _ _ -> Task.fromResult 0) 0 null TaskSeq.empty + + assertNullArg + <| fun () -> TaskSeq.fold2Async (fun _ _ _ -> Task.fromResult 0) 0 TaskSeq.empty null + + [)>] + let ``TaskSeq-fold2 returns initial state when both empty`` variant = task { + let! result = TaskSeq.fold2 (fun acc _ _ -> acc + 1) 42 (Gen.getEmptyVariant variant) (Gen.getEmptyVariant variant) + + result |> should equal 42 + } + + [)>] + let ``TaskSeq-fold2Async returns initial state when both empty`` variant = task { + let! result = + TaskSeq.fold2Async (fun acc _ _ -> task { return acc + 1 }) 42 (Gen.getEmptyVariant variant) (Gen.getEmptyVariant variant) + + result |> should equal 42 + } + + [)>] + let ``TaskSeq-fold2 returns initial state when first is empty`` variant = task { + let! result = TaskSeq.fold2 (fun acc _ _ -> acc + 1) 99 (Gen.getEmptyVariant variant) (TaskSeq.ofList [ 1; 2; 3 ]) + + result |> should equal 99 + } + + [)>] + let ``TaskSeq-fold2 returns initial state when second is empty`` variant = task { + let! result = TaskSeq.fold2 (fun acc _ _ -> acc + 1) 99 (TaskSeq.ofList [ 1; 2; 3 ]) (Gen.getEmptyVariant variant) + + result |> should equal 99 + } + + +module Fold2Immutable = + [)>] + let ``TaskSeq-fold2 folds over all pairs`` variant = task { + // source1 = source2 = 1..10; sum of products: 1*1 + 2*2 + ... + 10*10 = 385 + let! result = TaskSeq.fold2 (fun acc x y -> acc + x * y) 0 (Gen.getSeqImmutable variant) (Gen.getSeqImmutable variant) + + result |> should equal 385 + } + + [)>] + let ``TaskSeq-fold2Async folds over all pairs`` variant = task { + let! result = + TaskSeq.fold2Async (fun acc x y -> task { return acc + x * y }) 0 (Gen.getSeqImmutable variant) (Gen.getSeqImmutable variant) + + result |> should equal 385 + } + + [] + let ``TaskSeq-fold2 builds string from paired elements`` () = task { + let! result = + TaskSeq.fold2 + (fun (acc: StringBuilder) (x: int) (y: string) -> acc.Append(string x).Append(y)) + (StringBuilder()) + (TaskSeq.ofList [ 1; 2; 3 ]) + (TaskSeq.ofList [ "a"; "b"; "c" ]) + + result.ToString() |> should equal "1a2b3c" + } + + [] + let ``TaskSeq-fold2Async builds string from paired elements`` () = task { + let! result = + TaskSeq.fold2Async + (fun (acc: StringBuilder) (x: int) (y: string) -> task { return acc.Append(string x).Append(y) }) + (StringBuilder()) + (TaskSeq.ofList [ 1; 2; 3 ]) + (TaskSeq.ofList [ "a"; "b"; "c" ]) + + result.ToString() |> should equal "1a2b3c" + } + + [] + let ``TaskSeq-fold2 stops at shorter - first shorter`` () = task { + // source1 = [1;2;3], source2 = [10;20;30;40;50] + // sum of products: 1*10 + 2*20 + 3*30 = 10+40+90 = 140 (not 4*40 + 5*50) + let! result = TaskSeq.fold2 (fun acc x y -> acc + x * y) 0 (TaskSeq.ofList [ 1; 2; 3 ]) (TaskSeq.ofList [ 10; 20; 30; 40; 50 ]) + + result |> should equal 140 + } + + [] + let ``TaskSeq-fold2 stops at shorter - second shorter`` () = task { + let! result = TaskSeq.fold2 (fun acc x y -> acc + x * y) 0 (TaskSeq.ofList [ 10; 20; 30; 40; 50 ]) (TaskSeq.ofList [ 1; 2; 3 ]) + + result |> should equal 140 + } + + [] + let ``TaskSeq-fold2 with equal-length sequences folds all pairs`` () = task { + // Zips and counts pairs + let! result = + TaskSeq.fold2 (fun acc _ _ -> acc + 1) 0 (TaskSeq.ofList [ 1; 2; 3; 4; 5 ]) (TaskSeq.ofList [ 'a'; 'b'; 'c'; 'd'; 'e' ]) + + result |> should equal 5 + } + + [] + let ``TaskSeq-fold2 with singleton sequences`` () = task { + let! result = TaskSeq.fold2 (fun acc x y -> acc + x + y) 0 (TaskSeq.singleton 10) (TaskSeq.singleton 32) + + result |> should equal 42 + } + + +module Fold2SideEffects = + [)>] + let ``TaskSeq-fold2 second fold has fresh state from side-effect sequences`` variant = task { + let ts1 = Gen.getSeqWithSideEffect variant + let ts2 = Gen.getSeqWithSideEffect variant + + // first iteration: ts1 = 1..10, ts2 = 1..10 + let! first = TaskSeq.fold2 (fun acc x y -> acc + x + y) 0 ts1 ts2 + first |> should equal 110 // sum of 2*(1+2+...+10) = 110 + + // second iteration: ts1 = 11..20, ts2 = 11..20 (independent counters both advance) + // sum of pairs: (11+11) + (12+12) + ... + (20+20) = 2*(11+...+20) = 2*155 = 310 + let! second = TaskSeq.fold2 (fun acc x y -> acc + x + y) 0 ts1 ts2 + second |> should equal 310 + } + + [)>] + let ``TaskSeq-fold2Async second fold has fresh state from side-effect sequences`` variant = task { + let ts1 = Gen.getSeqWithSideEffect variant + let ts2 = Gen.getSeqWithSideEffect variant + + let! first = TaskSeq.fold2Async (fun acc x y -> task { return acc + x + y }) 0 ts1 ts2 + + first |> should equal 110 + + let! second = TaskSeq.fold2Async (fun acc x y -> task { return acc + x + y }) 0 ts1 ts2 + + second |> should equal 310 + } diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fs b/src/FSharp.Control.TaskSeq/TaskSeq.fs index aced3a5..15320ec 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeq.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeq.fs @@ -475,10 +475,12 @@ type TaskSeq private () = static member forall predicate source = Internal.forall (Predicate predicate) source static member forallAsync predicate source = Internal.forall (PredicateAsync predicate) source + static member forall2 predicate source1 source2 = Internal.forall2 predicate source1 source2 + static member forall2Async predicate source1 source2 = Internal.forall2Async predicate source1 source2 static member exists predicate source = Internal.exists (Predicate predicate) source - static member existsAsync predicate source = Internal.exists (PredicateAsync predicate) source + static member exists2 predicate source1 source2 = Internal.exists2 predicate source1 source2 static member contains value source = Internal.contains value source @@ -514,6 +516,8 @@ type TaskSeq private () = static member zip3 source1 source2 source3 = Internal.zip3 source1 source2 source3 static member fold folder state source = Internal.fold (FolderAction folder) state source static member foldAsync folder state source = Internal.fold (AsyncFolderAction folder) state source + static member fold2 folder state source1 source2 = Internal.fold2 folder state source1 source2 + static member fold2Async folder state source1 source2 = Internal.fold2Async folder state source1 source2 static member scan folder state source = Internal.scan (FolderAction folder) state source static member scanAsync folder state source = Internal.scan (AsyncFolderAction folder) state source static member reduce folder source = Internal.reduce (FolderAction folder) source diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fsi b/src/FSharp.Control.TaskSeq/TaskSeq.fsi index fcf45cf..af9bd0b 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeq.fsi +++ b/src/FSharp.Control.TaskSeq/TaskSeq.fsi @@ -1021,6 +1021,35 @@ type TaskSeq = /// Thrown when the input task sequence is null. static member forallAsync: predicate: ('T -> #Task) -> source: TaskSeq<'T> -> Task + /// + /// Tests if all corresponding element pairs from the two sequences satisfy the given predicate. Stops evaluating + /// as soon as returns or either sequence is exhausted. When the sequences + /// differ in length, the extra elements of the longer sequence are ignored. + /// If is asynchronous, consider using . + /// + /// + /// A function to test each pair of elements from the two input sequences. + /// The first input task sequence. + /// The second input task sequence. + /// A task that, after awaiting, holds true if every corresponding pair of elements satisfies the predicate; false otherwise. + /// Thrown when either input task sequence is null. + static member forall2: predicate: ('T -> 'U -> bool) -> source1: TaskSeq<'T> -> source2: TaskSeq<'U> -> Task + + /// + /// Tests if all corresponding element pairs from the two sequences satisfy the given asynchronous predicate. Stops evaluating + /// as soon as returns or either sequence is exhausted. When the sequences + /// differ in length, the extra elements of the longer sequence are ignored. + /// If is synchronous, consider using . + /// + /// + /// An asynchronous function to test each pair of elements from the two input sequences. + /// The first input task sequence. + /// The second input task sequence. + /// A task that, after awaiting, holds true if every corresponding pair of elements satisfies the predicate; false otherwise. + /// Thrown when either input task sequence is null. + static member forall2Async: + predicate: ('T -> 'U -> #Task) -> source1: TaskSeq<'T> -> source2: TaskSeq<'U> -> Task + /// /// Returns a task sequence that, when iterated, skips elements of the underlying /// sequence, and then yields the remainder. Raises an exception if there are not @@ -1381,6 +1410,19 @@ type TaskSeq = /// Thrown when the input task sequence is null. static member existsAsync: predicate: ('T -> #Task) -> source: TaskSeq<'T> -> Task + /// + /// Tests if any corresponding element pair from the two sequences satisfies the given predicate. Stops evaluating + /// as soon as returns or either sequence is exhausted. When the sequences + /// differ in length, the extra elements of the longer sequence are ignored. + /// + /// + /// A function to test each pair of elements from the two input sequences. + /// The first input task sequence. + /// The second input task sequence. + /// if any corresponding pair of elements satisfies the predicate; otherwise. + /// Thrown when either input task sequence is null. + static member exists2: predicate: ('T -> 'U -> bool) -> source1: TaskSeq<'T> -> source2: TaskSeq<'U> -> Task + /// /// Returns a new task sequence with the distinct elements of the second task sequence which do not appear in the /// sequence, using generic hash and equality comparisons to compare values. @@ -1585,6 +1627,46 @@ type TaskSeq = static member foldAsync: folder: ('State -> 'T -> #Task<'State>) -> state: 'State -> source: TaskSeq<'T> -> Task<'State> + /// + /// Applies the function to corresponding element pairs from the two sequences, threading an accumulator + /// argument of type through the computation. Stops at the shorter sequence; + /// extra elements of the longer sequence are ignored. + /// If the accumulator function is asynchronous, consider using . + /// + /// + /// A function that updates the state with each pair of elements from the sequences. + /// The initial state. + /// The first input task sequence. + /// The second input task sequence. + /// The state object after the folding function is applied to each corresponding pair of elements. + /// Thrown when either input task sequence is null. + static member fold2: + folder: ('State -> 'T -> 'U -> 'State) -> + state: 'State -> + source1: TaskSeq<'T> -> + source2: TaskSeq<'U> -> + Task<'State> + + /// + /// Applies the asynchronous function to corresponding element pairs from the two sequences, threading an accumulator + /// argument of type through the computation. Stops at the shorter sequence; + /// extra elements of the longer sequence are ignored. + /// If the accumulator function is synchronous, consider using . + /// + /// + /// An asynchronous function that updates the state with each pair of elements from the sequences. + /// The initial state. + /// The first input task sequence. + /// The second input task sequence. + /// The state object after the folding function is applied to each corresponding pair of elements. + /// Thrown when either input task sequence is null. + static member fold2Async: + folder: ('State -> 'T -> 'U -> #Task<'State>) -> + state: 'State -> + source1: TaskSeq<'T> -> + source2: TaskSeq<'U> -> + Task<'State> + /// /// Like , but returns the sequence of intermediate results and the final result. /// The first element of the output sequence is always the initial state. If the input task sequence diff --git a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs index 26b02a6..704261e 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs @@ -415,6 +415,49 @@ module internal TaskSeqInternal = return result } + let fold2 (folder: 'State -> 'T1 -> 'T2 -> 'State) state (source1: TaskSeq<'T1>) (source2: TaskSeq<'T2>) = + checkNonNull (nameof source1) source1 + checkNonNull (nameof source2) source2 + + task { + use e1 = source1.GetAsyncEnumerator CancellationToken.None + use e2 = source2.GetAsyncEnumerator CancellationToken.None + let mutable result = state + let! step1 = e1.MoveNextAsync() + let! step2 = e2.MoveNextAsync() + let mutable hasMore = step1 && step2 + + while hasMore do + result <- folder result e1.Current e2.Current + let! step1 = e1.MoveNextAsync() + let! step2 = e2.MoveNextAsync() + hasMore <- step1 && step2 + + return result + } + + let fold2Async (folder: 'State -> 'T1 -> 'T2 -> #Task<'State>) state (source1: TaskSeq<'T1>) (source2: TaskSeq<'T2>) = + checkNonNull (nameof source1) source1 + checkNonNull (nameof source2) source2 + + task { + use e1 = source1.GetAsyncEnumerator CancellationToken.None + use e2 = source2.GetAsyncEnumerator CancellationToken.None + let mutable result = state + let! step1 = e1.MoveNextAsync() + let! step2 = e2.MoveNextAsync() + let mutable hasMore = step1 && step2 + + while hasMore do + let! tempResult = folder result e1.Current e2.Current + result <- tempResult + let! step1 = e1.MoveNextAsync() + let! step2 = e2.MoveNextAsync() + hasMore <- step1 && step2 + + return result + } + let scan folder initial (source: TaskSeq<_>) = checkNonNull (nameof source) source @@ -883,6 +926,77 @@ module internal TaskSeqInternal = return state } + let forall2 (predicate: 'T1 -> 'T2 -> bool) (source1: TaskSeq<'T1>) (source2: TaskSeq<'T2>) = + checkNonNull (nameof source1) source1 + checkNonNull (nameof source2) source2 + + task { + use e1 = source1.GetAsyncEnumerator CancellationToken.None + use e2 = source2.GetAsyncEnumerator CancellationToken.None + let mutable result = true + let! step1 = e1.MoveNextAsync() + let! step2 = e2.MoveNextAsync() + let mutable hasMore = step1 && step2 + + while result && hasMore do + result <- predicate e1.Current e2.Current + + if result then + let! step1 = e1.MoveNextAsync() + let! step2 = e2.MoveNextAsync() + hasMore <- step1 && step2 + + return result + } + + let forall2Async (predicate: 'T1 -> 'T2 -> #Task) (source1: TaskSeq<'T1>) (source2: TaskSeq<'T2>) = + checkNonNull (nameof source1) source1 + checkNonNull (nameof source2) source2 + + task { + use e1 = source1.GetAsyncEnumerator CancellationToken.None + use e2 = source2.GetAsyncEnumerator CancellationToken.None + let mutable result = true + let! step1 = e1.MoveNextAsync() + let! step2 = e2.MoveNextAsync() + let mutable hasMore = step1 && step2 + + while result && hasMore do + let! pred = predicate e1.Current e2.Current + result <- pred + + if result then + let! step1 = e1.MoveNextAsync() + let! step2 = e2.MoveNextAsync() + hasMore <- step1 && step2 + + return result + } + + /// Direct bool-returning exists2, avoiding the Option<'T> allocation that tryFind+isSome would incur. + let exists2 (predicate: 'T1 -> 'T2 -> bool) (source1: TaskSeq<'T1>) (source2: TaskSeq<'T2>) = + checkNonNull (nameof source1) source1 + checkNonNull (nameof source2) source2 + + task { + use e1 = source1.GetAsyncEnumerator CancellationToken.None + use e2 = source2.GetAsyncEnumerator CancellationToken.None + let mutable found = false + let! step1 = e1.MoveNextAsync() + let! step2 = e2.MoveNextAsync() + let mutable hasMore = step1 && step2 + + while not found && hasMore do + found <- predicate e1.Current e2.Current + + if not found then + let! step1 = e1.MoveNextAsync() + let! step2 = e2.MoveNextAsync() + hasMore <- step1 && step2 + + return found + } + /// Direct bool-returning exists, avoiding the Option<'T> allocation that tryFind+isSome would incur. let exists predicate (source: TaskSeq<_>) = checkNonNull (nameof source) source