From 8f20e6694aa0c40159923f454bbf8a1546dd2a40 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 23 Mar 2026 00:20:52 +0000 Subject: [PATCH 1/2] Add AsyncSeq.tryFindBack, findBack, tryFindBackAsync, findBackAsync Add four backward-search functions that mirror F# standard library: - tryFindBack : ('T -> bool) -> AsyncSeq<'T> -> Async<'T option> - findBack : ('T -> bool) -> AsyncSeq<'T> -> Async<'T> - tryFindBackAsync : ('T -> Async) -> AsyncSeq<'T> -> Async<'T option> - findBackAsync : ('T -> Async) -> AsyncSeq<'T> -> Async<'T> These iterate the entire source sequence and return the last element satisfying the predicate, complementing the existing tryFind / find (which return the first match). findBack raises KeyNotFoundException when no element satisfies the predicate; tryFindBack returns None. All four are documented in AsyncSeq.fsi. 10 new tests added; all 331/331 tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- RELEASE_NOTES.md | 5 ++ src/FSharp.Control.AsyncSeq/AsyncSeq.fs | 32 +++++++ src/FSharp.Control.AsyncSeq/AsyncSeq.fsi | 16 ++++ .../AsyncSeqTests.fs | 84 +++++++++++++++++++ 4 files changed, 137 insertions(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 6f4908d..0fe1ceb 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,3 +1,8 @@ +### 4.11.0 + +* Added `AsyncSeq.tryFindBack` / `findBack` — returns the **last** element in a sequence for which the predicate returns `true`. Mirrors `Seq.tryFindBack` / `Seq.findBack`. +* Added `AsyncSeq.tryFindBackAsync` / `findBackAsync` — async-predicate variants of the above. + ### 4.10.0 * Added `AsyncSeq.withCancellation` — returns a new `AsyncSeq` that passes the given `CancellationToken` to `GetAsyncEnumerator`, overriding whatever token would otherwise be supplied. Mirrors `TaskSeq.withCancellation` and is useful when consuming sequences from libraries (e.g. Entity Framework) that accept a cancellation token through `GetAsyncEnumerator`. Part of ongoing design-parity work with FSharp.Control.TaskSeq (see #277). diff --git a/src/FSharp.Control.AsyncSeq/AsyncSeq.fs b/src/FSharp.Control.AsyncSeq/AsyncSeq.fs index 642bab5..7fabfe0 100644 --- a/src/FSharp.Control.AsyncSeq/AsyncSeq.fs +++ b/src/FSharp.Control.AsyncSeq/AsyncSeq.fs @@ -1311,6 +1311,38 @@ module AsyncSeq = let forallAsync f (source : AsyncSeq<'T>) = source |> existsAsync (fun v -> async { let! b = f v in return not b }) |> Async.map not + /// Returns the last element for which the given async predicate returns true, or None if no + /// such element exists. The entire sequence is consumed. + let tryFindBackAsync (predicate: 'T -> Async) (source: AsyncSeq<'T>) : Async<'T option> = async { + use ie = source.GetEnumerator() + let! move = ie.MoveNext() + let mutable b = move + let mutable result = None + while b.IsSome do + let! ok = predicate b.Value + if ok then result <- b + let! next = ie.MoveNext() + b <- next + return result } + + /// Returns the last element for which the given predicate returns true, or None if no + /// such element exists. The entire sequence is consumed. + let tryFindBack (predicate: 'T -> bool) (source: AsyncSeq<'T>) : Async<'T option> = + tryFindBackAsync (predicate >> async.Return) source + + /// Returns the last element for which the given async predicate returns true. + /// Raises KeyNotFoundException if no such element exists. + let findBackAsync (predicate: 'T -> Async) (source: AsyncSeq<'T>) : Async<'T> = async { + let! result = tryFindBackAsync predicate source + match result with + | None -> return raise (System.Collections.Generic.KeyNotFoundException("An element satisfying the predicate was not found in the collection.")) + | Some v -> return v } + + /// Returns the last element for which the given predicate returns true. + /// Raises KeyNotFoundException if no such element exists. + let findBack (predicate: 'T -> bool) (source: AsyncSeq<'T>) : Async<'T> = + findBackAsync (predicate >> async.Return) source + let foldAsync f (state:'State) (source : AsyncSeq<'T>) = match source with | :? AsyncSeqOp<'T> as source -> source.FoldAsync f state diff --git a/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi b/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi index b17f019..34a8080 100644 --- a/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi +++ b/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi @@ -389,6 +389,22 @@ 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 + /// Returns the last element in the sequence for which the given async predicate returns true, + /// or None if no such element exists. The entire sequence is consumed. + val tryFindBackAsync : predicate:('T -> Async) -> source:AsyncSeq<'T> -> Async<'T option> + + /// Returns the last element in the sequence for which the given predicate returns true, + /// or None if no such element exists. The entire sequence is consumed. + val tryFindBack : predicate:('T -> bool) -> source:AsyncSeq<'T> -> Async<'T option> + + /// Returns the last element in the sequence for which the given async predicate returns true. + /// Raises KeyNotFoundException if no such element exists. + val findBackAsync : predicate:('T -> Async) -> source:AsyncSeq<'T> -> Async<'T> + + /// Returns the last element in the sequence for which the given predicate returns true. + /// Raises KeyNotFoundException if no such element exists. + val findBack : predicate:('T -> bool) -> source:AsyncSeq<'T> -> Async<'T> + /// Return an asynchronous sequence which, when iterated, includes an integer indicating the index of each element in the sequence. val indexed : source:AsyncSeq<'T> -> AsyncSeq diff --git a/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs b/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs index 6e39e77..5c38017 100644 --- a/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs +++ b/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs @@ -3719,3 +3719,87 @@ let ``AsyncSeq.withCancellation with cancelled token raises OperationCanceledExc |> Async.RunSynchronously |> ignore) |> ignore + +// ===== tryFindBack / findBack / tryFindBackAsync / findBackAsync ===== + +[] +let ``AsyncSeq.tryFindBack returns last matching element`` () = + let result = + AsyncSeq.ofSeq [1; 3; 5; 2; 4; 6; 7] + |> AsyncSeq.tryFindBack (fun x -> x % 2 = 0) + |> Async.RunSynchronously + Assert.AreEqual(Some 6, result) + +[] +let ``AsyncSeq.tryFindBack returns None when no element matches`` () = + let result = + AsyncSeq.ofSeq [1; 3; 5] + |> AsyncSeq.tryFindBack (fun x -> x % 2 = 0) + |> Async.RunSynchronously + Assert.AreEqual(None, result) + +[] +let ``AsyncSeq.tryFindBack returns None for empty sequence`` () = + let result = + AsyncSeq.empty + |> AsyncSeq.tryFindBack (fun _ -> true) + |> Async.RunSynchronously + Assert.AreEqual(None, result) + +[] +let ``AsyncSeq.tryFindBack returns last element when all match`` () = + let result = + AsyncSeq.ofSeq [10; 20; 30] + |> AsyncSeq.tryFindBack (fun _ -> true) + |> Async.RunSynchronously + Assert.AreEqual(Some 30, result) + +[] +let ``AsyncSeq.findBack returns last matching element`` () = + let result = + AsyncSeq.ofSeq [2; 4; 1; 3; 6; 5] + |> AsyncSeq.findBack (fun x -> x % 2 = 0) + |> Async.RunSynchronously + Assert.AreEqual(6, result) + +[] +let ``AsyncSeq.findBack raises KeyNotFoundException when no match`` () = + Assert.Throws(fun () -> + AsyncSeq.ofSeq [1; 3; 5] + |> AsyncSeq.findBack (fun x -> x % 2 = 0) + |> Async.RunSynchronously + |> ignore) + |> ignore + +[] +let ``AsyncSeq.tryFindBackAsync returns last element satisfying async predicate`` () = + let result = + AsyncSeq.ofSeq [1; 2; 3; 4; 5] + |> AsyncSeq.tryFindBackAsync (fun x -> async { return x < 4 }) + |> Async.RunSynchronously + Assert.AreEqual(Some 3, result) + +[] +let ``AsyncSeq.tryFindBackAsync returns None when nothing matches`` () = + let result = + AsyncSeq.ofSeq [1; 2; 3] + |> AsyncSeq.tryFindBackAsync (fun x -> async { return x > 99 }) + |> Async.RunSynchronously + Assert.AreEqual(None, result) + +[] +let ``AsyncSeq.findBackAsync returns last matching element`` () = + let result = + AsyncSeq.ofSeq [10; 20; 5; 15; 30] + |> AsyncSeq.findBackAsync (fun x -> async { return x > 10 }) + |> Async.RunSynchronously + Assert.AreEqual(30, result) + +[] +let ``AsyncSeq.findBackAsync raises KeyNotFoundException when no match`` () = + Assert.Throws(fun () -> + AsyncSeq.ofSeq [1; 2; 3] + |> AsyncSeq.findBackAsync (fun x -> async { return x > 99 }) + |> Async.RunSynchronously + |> ignore) + |> ignore From 2958734c75b81f411bc450eda1519bcb1193ec9a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 23 Mar 2026 00:26:35 +0000 Subject: [PATCH 2/2] ci: trigger checks