From b0decce1fb6f9972943cc1b17258efa5b1447c22 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 358/358 tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- RELEASE_NOTES.md | 2 + src/FSharp.Control.AsyncSeq/AsyncSeq.fs | 32 +++++++ src/FSharp.Control.AsyncSeq/AsyncSeq.fsi | 16 ++++ .../AsyncSeqTests.fs | 84 +++++++++++++++++++ 4 files changed, 134 insertions(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index bc6ca07..89efef1 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -12,6 +12,8 @@ * Added `AsyncSeq.delay` — defers sequence creation to enumeration time by calling a factory function each time `GetAsyncEnumerator` is called. Mirrors `TaskSeq.delay`. * Added `AsyncSeq.collectAsync` — like `collect` but the mapping function is asynchronous (`'T -> Async>`). Mirrors `TaskSeq.collectAsync`. * Added `AsyncSeq.partition` / `AsyncSeq.partitionAsync` — splits a sequence into two arrays using a (optionally async) predicate; the first array contains matching elements, the second non-matching. Mirrors `TaskSeq.partition` / `TaskSeq.partitionAsync`. +* 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 diff --git a/src/FSharp.Control.AsyncSeq/AsyncSeq.fs b/src/FSharp.Control.AsyncSeq/AsyncSeq.fs index ca58137..43322fa 100644 --- a/src/FSharp.Control.AsyncSeq/AsyncSeq.fs +++ b/src/FSharp.Control.AsyncSeq/AsyncSeq.fs @@ -1347,6 +1347,38 @@ module AsyncSeq = let compareWith (comparer: 'T -> 'T -> int) (source1: AsyncSeq<'T>) (source2: AsyncSeq<'T>) : Async = compareWithAsync (fun a b -> comparer a b |> async.Return) source1 source2 + /// 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 c3c266f..f1a737c 100644 --- a/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi +++ b/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi @@ -413,6 +413,22 @@ module AsyncSeq = /// Returns a negative integer if source1 < source2, 0 if equal, and a positive integer if source1 > source2. val compareWithAsync : comparer:('T -> 'T -> Async) -> source1:AsyncSeq<'T> -> source2: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 0b39aad..3da5fc1 100644 --- a/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs +++ b/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs @@ -3934,3 +3934,87 @@ let ``AsyncSeq.partitionAsync splits by async predicate`` () = |> Async.RunSynchronously Assert.AreEqual([|2;4;6|], trues) Assert.AreEqual([|1;3;5|], falses) + +// ===== 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 25b5a29dd492d2130172cd79b5af0e02577cfae1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 25 Mar 2026 00:26:34 +0000 Subject: [PATCH 2/2] ci: trigger checks