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