Skip to content

Commit 8f20e66

Browse files
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<bool>) -> AsyncSeq<'T> -> Async<'T option> - findBackAsync : ('T -> Async<bool>) -> 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 <[email protected]>
1 parent bc5c1f0 commit 8f20e66

4 files changed

Lines changed: 137 additions & 0 deletions

File tree

RELEASE_NOTES.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
### 4.11.0
2+
3+
* Added `AsyncSeq.tryFindBack` / `findBack` — returns the **last** element in a sequence for which the predicate returns `true`. Mirrors `Seq.tryFindBack` / `Seq.findBack`.
4+
* Added `AsyncSeq.tryFindBackAsync` / `findBackAsync` — async-predicate variants of the above.
5+
16
### 4.10.0
27

38
* 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).

src/FSharp.Control.AsyncSeq/AsyncSeq.fs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1311,6 +1311,38 @@ module AsyncSeq =
13111311
let forallAsync f (source : AsyncSeq<'T>) =
13121312
source |> existsAsync (fun v -> async { let! b = f v in return not b }) |> Async.map not
13131313

1314+
/// Returns the last element for which the given async predicate returns true, or None if no
1315+
/// such element exists. The entire sequence is consumed.
1316+
let tryFindBackAsync (predicate: 'T -> Async<bool>) (source: AsyncSeq<'T>) : Async<'T option> = async {
1317+
use ie = source.GetEnumerator()
1318+
let! move = ie.MoveNext()
1319+
let mutable b = move
1320+
let mutable result = None
1321+
while b.IsSome do
1322+
let! ok = predicate b.Value
1323+
if ok then result <- b
1324+
let! next = ie.MoveNext()
1325+
b <- next
1326+
return result }
1327+
1328+
/// Returns the last element for which the given predicate returns true, or None if no
1329+
/// such element exists. The entire sequence is consumed.
1330+
let tryFindBack (predicate: 'T -> bool) (source: AsyncSeq<'T>) : Async<'T option> =
1331+
tryFindBackAsync (predicate >> async.Return) source
1332+
1333+
/// Returns the last element for which the given async predicate returns true.
1334+
/// Raises <c>KeyNotFoundException</c> if no such element exists.
1335+
let findBackAsync (predicate: 'T -> Async<bool>) (source: AsyncSeq<'T>) : Async<'T> = async {
1336+
let! result = tryFindBackAsync predicate source
1337+
match result with
1338+
| None -> return raise (System.Collections.Generic.KeyNotFoundException("An element satisfying the predicate was not found in the collection."))
1339+
| Some v -> return v }
1340+
1341+
/// Returns the last element for which the given predicate returns true.
1342+
/// Raises <c>KeyNotFoundException</c> if no such element exists.
1343+
let findBack (predicate: 'T -> bool) (source: AsyncSeq<'T>) : Async<'T> =
1344+
findBackAsync (predicate >> async.Return) source
1345+
13141346
let foldAsync f (state:'State) (source : AsyncSeq<'T>) =
13151347
match source with
13161348
| :? AsyncSeqOp<'T> as source -> source.FoldAsync f state

src/FSharp.Control.AsyncSeq/AsyncSeq.fsi

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,22 @@ module AsyncSeq =
389389
/// Asynchronously determine if the async predicate returns true for all values in the sequence
390390
val forallAsync : predicate:('T -> Async<bool>) -> source:AsyncSeq<'T> -> Async<bool>
391391

392+
/// Returns the last element in the sequence for which the given async predicate returns true,
393+
/// or <c>None</c> if no such element exists. The entire sequence is consumed.
394+
val tryFindBackAsync : predicate:('T -> Async<bool>) -> source:AsyncSeq<'T> -> Async<'T option>
395+
396+
/// Returns the last element in the sequence for which the given predicate returns true,
397+
/// or <c>None</c> if no such element exists. The entire sequence is consumed.
398+
val tryFindBack : predicate:('T -> bool) -> source:AsyncSeq<'T> -> Async<'T option>
399+
400+
/// Returns the last element in the sequence for which the given async predicate returns true.
401+
/// Raises <c>KeyNotFoundException</c> if no such element exists.
402+
val findBackAsync : predicate:('T -> Async<bool>) -> source:AsyncSeq<'T> -> Async<'T>
403+
404+
/// Returns the last element in the sequence for which the given predicate returns true.
405+
/// Raises <c>KeyNotFoundException</c> if no such element exists.
406+
val findBack : predicate:('T -> bool) -> source:AsyncSeq<'T> -> Async<'T>
407+
392408
/// Return an asynchronous sequence which, when iterated, includes an integer indicating the index of each element in the sequence.
393409
val indexed : source:AsyncSeq<'T> -> AsyncSeq<int64 * 'T>
394410

tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3719,3 +3719,87 @@ let ``AsyncSeq.withCancellation with cancelled token raises OperationCanceledExc
37193719
|> Async.RunSynchronously
37203720
|> ignore)
37213721
|> ignore
3722+
3723+
// ===== tryFindBack / findBack / tryFindBackAsync / findBackAsync =====
3724+
3725+
[<Test>]
3726+
let ``AsyncSeq.tryFindBack returns last matching element`` () =
3727+
let result =
3728+
AsyncSeq.ofSeq [1; 3; 5; 2; 4; 6; 7]
3729+
|> AsyncSeq.tryFindBack (fun x -> x % 2 = 0)
3730+
|> Async.RunSynchronously
3731+
Assert.AreEqual(Some 6, result)
3732+
3733+
[<Test>]
3734+
let ``AsyncSeq.tryFindBack returns None when no element matches`` () =
3735+
let result =
3736+
AsyncSeq.ofSeq [1; 3; 5]
3737+
|> AsyncSeq.tryFindBack (fun x -> x % 2 = 0)
3738+
|> Async.RunSynchronously
3739+
Assert.AreEqual(None, result)
3740+
3741+
[<Test>]
3742+
let ``AsyncSeq.tryFindBack returns None for empty sequence`` () =
3743+
let result =
3744+
AsyncSeq.empty<int>
3745+
|> AsyncSeq.tryFindBack (fun _ -> true)
3746+
|> Async.RunSynchronously
3747+
Assert.AreEqual(None, result)
3748+
3749+
[<Test>]
3750+
let ``AsyncSeq.tryFindBack returns last element when all match`` () =
3751+
let result =
3752+
AsyncSeq.ofSeq [10; 20; 30]
3753+
|> AsyncSeq.tryFindBack (fun _ -> true)
3754+
|> Async.RunSynchronously
3755+
Assert.AreEqual(Some 30, result)
3756+
3757+
[<Test>]
3758+
let ``AsyncSeq.findBack returns last matching element`` () =
3759+
let result =
3760+
AsyncSeq.ofSeq [2; 4; 1; 3; 6; 5]
3761+
|> AsyncSeq.findBack (fun x -> x % 2 = 0)
3762+
|> Async.RunSynchronously
3763+
Assert.AreEqual(6, result)
3764+
3765+
[<Test>]
3766+
let ``AsyncSeq.findBack raises KeyNotFoundException when no match`` () =
3767+
Assert.Throws<System.Collections.Generic.KeyNotFoundException>(fun () ->
3768+
AsyncSeq.ofSeq [1; 3; 5]
3769+
|> AsyncSeq.findBack (fun x -> x % 2 = 0)
3770+
|> Async.RunSynchronously
3771+
|> ignore)
3772+
|> ignore
3773+
3774+
[<Test>]
3775+
let ``AsyncSeq.tryFindBackAsync returns last element satisfying async predicate`` () =
3776+
let result =
3777+
AsyncSeq.ofSeq [1; 2; 3; 4; 5]
3778+
|> AsyncSeq.tryFindBackAsync (fun x -> async { return x < 4 })
3779+
|> Async.RunSynchronously
3780+
Assert.AreEqual(Some 3, result)
3781+
3782+
[<Test>]
3783+
let ``AsyncSeq.tryFindBackAsync returns None when nothing matches`` () =
3784+
let result =
3785+
AsyncSeq.ofSeq [1; 2; 3]
3786+
|> AsyncSeq.tryFindBackAsync (fun x -> async { return x > 99 })
3787+
|> Async.RunSynchronously
3788+
Assert.AreEqual(None, result)
3789+
3790+
[<Test>]
3791+
let ``AsyncSeq.findBackAsync returns last matching element`` () =
3792+
let result =
3793+
AsyncSeq.ofSeq [10; 20; 5; 15; 30]
3794+
|> AsyncSeq.findBackAsync (fun x -> async { return x > 10 })
3795+
|> Async.RunSynchronously
3796+
Assert.AreEqual(30, result)
3797+
3798+
[<Test>]
3799+
let ``AsyncSeq.findBackAsync raises KeyNotFoundException when no match`` () =
3800+
Assert.Throws<System.Collections.Generic.KeyNotFoundException>(fun () ->
3801+
AsyncSeq.ofSeq [1; 2; 3]
3802+
|> AsyncSeq.findBackAsync (fun x -> async { return x > 99 })
3803+
|> Async.RunSynchronously
3804+
|> ignore)
3805+
|> ignore

0 commit comments

Comments
 (0)