diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 7290415..efd67d9 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,9 @@ ### 4.17.0 +* Added `AsyncSeq.exists2` — asynchronously tests whether any corresponding pair of elements in two async sequences satisfies the predicate. Evaluates pairwise up to the shorter sequence; short-circuits on first match. Mirrors `Seq.exists2`. +* Added `AsyncSeq.exists2Async` — asynchronous-predicate variant of `exists2`. +* Added `AsyncSeq.forall2` — asynchronously tests whether all corresponding pairs of elements in two async sequences satisfy the predicate. Evaluates pairwise up to the shorter sequence; short-circuits on first failure. Mirrors `Seq.forall2`. +* Added `AsyncSeq.forall2Async` — asynchronous-predicate variant of `forall2`. * Performance: Optimised `AsyncSeq.pairwise` to use a `hasPrev` flag and a direct `mutable` field instead of wrapping the previous element in `Some`. Previously, each iteration allocated a new `'T option` object on the heap; the new implementation eliminates that allocation entirely, reducing GC pressure for long sequences. * Bug fix: `AsyncSeq.splitAt` and `AsyncSeq.tryTail` now correctly dispose the underlying enumerator when an exception or cancellation occurs during the initial `MoveNext` call. Previously the enumerator could leak if the source sequence threw during the first few steps. diff --git a/src/FSharp.Control.AsyncSeq/AsyncSeq.fs b/src/FSharp.Control.AsyncSeq/AsyncSeq.fs index ddc5e71..d24b451 100644 --- a/src/FSharp.Control.AsyncSeq/AsyncSeq.fs +++ b/src/FSharp.Control.AsyncSeq/AsyncSeq.fs @@ -1472,6 +1472,56 @@ module AsyncSeq = let forallAsync f (source : AsyncSeq<'T>) = source |> existsAsync (fun v -> async { let! b = f v in return not b }) |> Async.map not + let exists2Async (predicate: 'T1 -> 'T2 -> Async) (source1: AsyncSeq<'T1>) (source2: AsyncSeq<'T2>) : Async = async { + use ie1 = source1.GetEnumerator() + use ie2 = source2.GetEnumerator() + let! m1 = ie1.MoveNext() + let! m2 = ie2.MoveNext() + let mutable b1 = m1 + let mutable b2 = m2 + let mutable result = false + let mutable isDone = false + while not isDone do + match b1, b2 with + | None, _ | _, None -> isDone <- true + | Some v1, Some v2 -> + let! ok = predicate v1 v2 + if ok then result <- true; isDone <- true + else + let! n1 = ie1.MoveNext() + let! n2 = ie2.MoveNext() + b1 <- n1 + b2 <- n2 + return result } + + let exists2 (predicate: 'T1 -> 'T2 -> bool) (source1: AsyncSeq<'T1>) (source2: AsyncSeq<'T2>) : Async = + exists2Async (fun a b -> async.Return (predicate a b)) source1 source2 + + let forall2Async (predicate: 'T1 -> 'T2 -> Async) (source1: AsyncSeq<'T1>) (source2: AsyncSeq<'T2>) : Async = async { + use ie1 = source1.GetEnumerator() + use ie2 = source2.GetEnumerator() + let! m1 = ie1.MoveNext() + let! m2 = ie2.MoveNext() + let mutable b1 = m1 + let mutable b2 = m2 + let mutable result = true + let mutable isDone = false + while not isDone do + match b1, b2 with + | None, _ | _, None -> isDone <- true + | Some v1, Some v2 -> + let! ok = predicate v1 v2 + if not ok then result <- false; isDone <- true + else + let! n1 = ie1.MoveNext() + let! n2 = ie2.MoveNext() + b1 <- n1 + b2 <- n2 + return result } + + let forall2 (predicate: 'T1 -> 'T2 -> bool) (source1: AsyncSeq<'T1>) (source2: AsyncSeq<'T2>) : Async = + forall2Async (fun a b -> async.Return (predicate a b)) source1 source2 + let compareWithAsync (comparer: 'T -> 'T -> Async) (source1: AsyncSeq<'T>) (source2: AsyncSeq<'T>) : Async = async { use ie1 = source1.GetEnumerator() use ie2 = source2.GetEnumerator() diff --git a/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi b/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi index d8ffec4..32c9b93 100644 --- a/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi +++ b/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi @@ -426,6 +426,24 @@ module AsyncSeq = /// Asynchronously determine if the async predicate returns true for all values in the sequence val forallAsync : predicate:('T -> Async) -> source:AsyncSeq<'T> -> Async + /// Asynchronously determine if any corresponding pair of elements in two async sequences satisfies + /// the predicate. Evaluates pairwise up to the shorter of the two sequences; short-circuits on first match. + /// Mirrors Seq.exists2. + val exists2 : predicate:('T1 -> 'T2 -> bool) -> source1:AsyncSeq<'T1> -> source2:AsyncSeq<'T2> -> Async + + /// Asynchronously determine if any corresponding pair of elements in two async sequences satisfies + /// the async predicate. Evaluates pairwise up to the shorter of the two sequences; short-circuits on first match. + val exists2Async : predicate:('T1 -> 'T2 -> Async) -> source1:AsyncSeq<'T1> -> source2:AsyncSeq<'T2> -> Async + + /// Asynchronously determine if all corresponding pairs of elements in two async sequences satisfy + /// the predicate. Evaluates pairwise up to the shorter of the two sequences; short-circuits on first failure. + /// Mirrors Seq.forall2. + val forall2 : predicate:('T1 -> 'T2 -> bool) -> source1:AsyncSeq<'T1> -> source2:AsyncSeq<'T2> -> Async + + /// Asynchronously determine if all corresponding pairs of elements in two async sequences satisfy + /// the async predicate. Evaluates pairwise up to the shorter of the two sequences; short-circuits on first failure. + val forall2Async : predicate:('T1 -> 'T2 -> Async) -> source1:AsyncSeq<'T1> -> source2:AsyncSeq<'T2> -> Async + /// Compares two async sequences lexicographically using the given synchronous comparison function. /// Returns a negative integer if source1 < source2, 0 if equal, and a positive integer if source1 > source2. val compareWith : comparer:('T -> 'T -> int) -> source1:AsyncSeq<'T> -> source2:AsyncSeq<'T> -> Async diff --git a/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs b/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs index 24748af..0c93784 100644 --- a/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs +++ b/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs @@ -4728,3 +4728,107 @@ let ``asyncSeq try-with handler can yield elements`` () = with _ -> yield 42 } let result = s |> AsyncSeq.toArrayAsync |> Async.RunSynchronously Assert.AreEqual([| 42 |], result) + +// ===== exists2 / exists2Async ===== + +[] +let ``AsyncSeq.exists2 returns true when a matching pair exists`` () = + let result = + AsyncSeq.exists2 (=) (AsyncSeq.ofSeq [1;2;3]) (AsyncSeq.ofSeq [0;2;0]) + |> Async.RunSynchronously + Assert.IsTrue(result) + +[] +let ``AsyncSeq.exists2 returns false when no matching pair exists`` () = + let result = + AsyncSeq.exists2 (=) (AsyncSeq.ofSeq [1;2;3]) (AsyncSeq.ofSeq [4;5;6]) + |> Async.RunSynchronously + Assert.IsFalse(result) + +[] +let ``AsyncSeq.exists2 on empty sequences returns false`` () = + let result = + AsyncSeq.exists2 (=) AsyncSeq.empty AsyncSeq.empty + |> Async.RunSynchronously + Assert.IsFalse(result) + +[] +let ``AsyncSeq.exists2 short-circuits on first match`` () = + let count = ref 0 + let result = + AsyncSeq.exists2 + (fun a b -> incr count; a = b) + (AsyncSeq.ofSeq [1;2;3;4;5]) + (AsyncSeq.ofSeq [0;2;0;0;0]) + |> Async.RunSynchronously + Assert.IsTrue(result) + Assert.AreEqual(2, count.Value) // stopped after second pair + +[] +let ``AsyncSeq.exists2 stops at shorter sequence`` () = + let result = + AsyncSeq.exists2 (=) (AsyncSeq.ofSeq [1;2]) (AsyncSeq.ofSeq [3;4;1]) + |> Async.RunSynchronously + Assert.IsFalse(result) // shorter seq ends before (1,1) can match + +[] +let ``AsyncSeq.exists2Async returns true with async predicate`` () = + let result = + AsyncSeq.exists2Async + (fun a b -> async { return a = b }) + (AsyncSeq.ofSeq [1;2;3]) + (AsyncSeq.ofSeq [0;2;0]) + |> Async.RunSynchronously + Assert.IsTrue(result) + +// ===== forall2 / forall2Async ===== + +[] +let ``AsyncSeq.forall2 returns true when all pairs satisfy the predicate`` () = + let result = + AsyncSeq.forall2 (=) (AsyncSeq.ofSeq [1;2;3]) (AsyncSeq.ofSeq [1;2;3]) + |> Async.RunSynchronously + Assert.IsTrue(result) + +[] +let ``AsyncSeq.forall2 returns false when a pair fails`` () = + let result = + AsyncSeq.forall2 (=) (AsyncSeq.ofSeq [1;2;3]) (AsyncSeq.ofSeq [1;9;3]) + |> Async.RunSynchronously + Assert.IsFalse(result) + +[] +let ``AsyncSeq.forall2 on empty sequences returns true`` () = + let result = + AsyncSeq.forall2 (=) AsyncSeq.empty AsyncSeq.empty + |> Async.RunSynchronously + Assert.IsTrue(result) + +[] +let ``AsyncSeq.forall2 short-circuits on first failure`` () = + let count = ref 0 + let result = + AsyncSeq.forall2 + (fun a b -> incr count; a = b) + (AsyncSeq.ofSeq [1;9;3;4;5]) + (AsyncSeq.ofSeq [1;2;3;4;5]) + |> Async.RunSynchronously + Assert.IsFalse(result) + Assert.AreEqual(2, count.Value) // stopped after second pair + +[] +let ``AsyncSeq.forall2 stops at shorter sequence`` () = + let result = + AsyncSeq.forall2 (=) (AsyncSeq.ofSeq [1;2]) (AsyncSeq.ofSeq [1;2;99]) + |> Async.RunSynchronously + Assert.IsTrue(result) // stops when shorter seq ends; all checked pairs passed + +[] +let ``AsyncSeq.forall2Async returns true with async predicate`` () = + let result = + AsyncSeq.forall2Async + (fun a b -> async { return a = b }) + (AsyncSeq.ofSeq [1;2;3]) + (AsyncSeq.ofSeq [1;2;3]) + |> Async.RunSynchronously + Assert.IsTrue(result)