Skip to content

Commit b0decce

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 358/358 tests pass. Co-authored-by: Copilot <[email protected]>
1 parent 7319e42 commit b0decce

4 files changed

Lines changed: 134 additions & 0 deletions

File tree

RELEASE_NOTES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
* Added `AsyncSeq.delay` — defers sequence creation to enumeration time by calling a factory function each time `GetAsyncEnumerator` is called. Mirrors `TaskSeq.delay`.
1313
* Added `AsyncSeq.collectAsync` — like `collect` but the mapping function is asynchronous (`'T -> Async<AsyncSeq<'U>>`). Mirrors `TaskSeq.collectAsync`.
1414
* 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`.
15+
* Added `AsyncSeq.tryFindBack` / `findBack` — returns the **last** element in a sequence for which the predicate returns `true`. Mirrors `Seq.tryFindBack` / `Seq.findBack`.
16+
* Added `AsyncSeq.tryFindBackAsync` / `findBackAsync` — async-predicate variants of the above.
1517

1618
### 4.10.0
1719

src/FSharp.Control.AsyncSeq/AsyncSeq.fs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1347,6 +1347,38 @@ module AsyncSeq =
13471347
let compareWith (comparer: 'T -> 'T -> int) (source1: AsyncSeq<'T>) (source2: AsyncSeq<'T>) : Async<int> =
13481348
compareWithAsync (fun a b -> comparer a b |> async.Return) source1 source2
13491349

1350+
/// Returns the last element for which the given async predicate returns true, or None if no
1351+
/// such element exists. The entire sequence is consumed.
1352+
let tryFindBackAsync (predicate: 'T -> Async<bool>) (source: AsyncSeq<'T>) : Async<'T option> = async {
1353+
use ie = source.GetEnumerator()
1354+
let! move = ie.MoveNext()
1355+
let mutable b = move
1356+
let mutable result = None
1357+
while b.IsSome do
1358+
let! ok = predicate b.Value
1359+
if ok then result <- b
1360+
let! next = ie.MoveNext()
1361+
b <- next
1362+
return result }
1363+
1364+
/// Returns the last element for which the given predicate returns true, or None if no
1365+
/// such element exists. The entire sequence is consumed.
1366+
let tryFindBack (predicate: 'T -> bool) (source: AsyncSeq<'T>) : Async<'T option> =
1367+
tryFindBackAsync (predicate >> async.Return) source
1368+
1369+
/// Returns the last element for which the given async predicate returns true.
1370+
/// Raises <c>KeyNotFoundException</c> if no such element exists.
1371+
let findBackAsync (predicate: 'T -> Async<bool>) (source: AsyncSeq<'T>) : Async<'T> = async {
1372+
let! result = tryFindBackAsync predicate source
1373+
match result with
1374+
| None -> return raise (System.Collections.Generic.KeyNotFoundException("An element satisfying the predicate was not found in the collection."))
1375+
| Some v -> return v }
1376+
1377+
/// Returns the last element for which the given predicate returns true.
1378+
/// Raises <c>KeyNotFoundException</c> if no such element exists.
1379+
let findBack (predicate: 'T -> bool) (source: AsyncSeq<'T>) : Async<'T> =
1380+
findBackAsync (predicate >> async.Return) source
1381+
13501382
let foldAsync f (state:'State) (source : AsyncSeq<'T>) =
13511383
match source with
13521384
| :? 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
@@ -413,6 +413,22 @@ module AsyncSeq =
413413
/// Returns a negative integer if source1 < source2, 0 if equal, and a positive integer if source1 > source2.
414414
val compareWithAsync : comparer:('T -> 'T -> Async<int>) -> source1:AsyncSeq<'T> -> source2:AsyncSeq<'T> -> Async<int>
415415

416+
/// Returns the last element in the sequence for which the given async predicate returns true,
417+
/// or <c>None</c> if no such element exists. The entire sequence is consumed.
418+
val tryFindBackAsync : predicate:('T -> Async<bool>) -> source:AsyncSeq<'T> -> Async<'T option>
419+
420+
/// Returns the last element in the sequence for which the given predicate returns true,
421+
/// or <c>None</c> if no such element exists. The entire sequence is consumed.
422+
val tryFindBack : predicate:('T -> bool) -> source:AsyncSeq<'T> -> Async<'T option>
423+
424+
/// Returns the last element in the sequence for which the given async predicate returns true.
425+
/// Raises <c>KeyNotFoundException</c> if no such element exists.
426+
val findBackAsync : predicate:('T -> Async<bool>) -> source:AsyncSeq<'T> -> Async<'T>
427+
428+
/// Returns the last element in the sequence for which the given predicate returns true.
429+
/// Raises <c>KeyNotFoundException</c> if no such element exists.
430+
val findBack : predicate:('T -> bool) -> source:AsyncSeq<'T> -> Async<'T>
431+
416432
/// Return an asynchronous sequence which, when iterated, includes an integer indicating the index of each element in the sequence.
417433
val indexed : source:AsyncSeq<'T> -> AsyncSeq<int64 * 'T>
418434

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

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3934,3 +3934,87 @@ let ``AsyncSeq.partitionAsync splits by async predicate`` () =
39343934
|> Async.RunSynchronously
39353935
Assert.AreEqual([|2;4;6|], trues)
39363936
Assert.AreEqual([|1;3;5|], falses)
3937+
3938+
// ===== tryFindBack / findBack / tryFindBackAsync / findBackAsync =====
3939+
3940+
[<Test>]
3941+
let ``AsyncSeq.tryFindBack returns last matching element`` () =
3942+
let result =
3943+
AsyncSeq.ofSeq [1; 3; 5; 2; 4; 6; 7]
3944+
|> AsyncSeq.tryFindBack (fun x -> x % 2 = 0)
3945+
|> Async.RunSynchronously
3946+
Assert.AreEqual(Some 6, result)
3947+
3948+
[<Test>]
3949+
let ``AsyncSeq.tryFindBack returns None when no element matches`` () =
3950+
let result =
3951+
AsyncSeq.ofSeq [1; 3; 5]
3952+
|> AsyncSeq.tryFindBack (fun x -> x % 2 = 0)
3953+
|> Async.RunSynchronously
3954+
Assert.AreEqual(None, result)
3955+
3956+
[<Test>]
3957+
let ``AsyncSeq.tryFindBack returns None for empty sequence`` () =
3958+
let result =
3959+
AsyncSeq.empty<int>
3960+
|> AsyncSeq.tryFindBack (fun _ -> true)
3961+
|> Async.RunSynchronously
3962+
Assert.AreEqual(None, result)
3963+
3964+
[<Test>]
3965+
let ``AsyncSeq.tryFindBack returns last element when all match`` () =
3966+
let result =
3967+
AsyncSeq.ofSeq [10; 20; 30]
3968+
|> AsyncSeq.tryFindBack (fun _ -> true)
3969+
|> Async.RunSynchronously
3970+
Assert.AreEqual(Some 30, result)
3971+
3972+
[<Test>]
3973+
let ``AsyncSeq.findBack returns last matching element`` () =
3974+
let result =
3975+
AsyncSeq.ofSeq [2; 4; 1; 3; 6; 5]
3976+
|> AsyncSeq.findBack (fun x -> x % 2 = 0)
3977+
|> Async.RunSynchronously
3978+
Assert.AreEqual(6, result)
3979+
3980+
[<Test>]
3981+
let ``AsyncSeq.findBack raises KeyNotFoundException when no match`` () =
3982+
Assert.Throws<System.Collections.Generic.KeyNotFoundException>(fun () ->
3983+
AsyncSeq.ofSeq [1; 3; 5]
3984+
|> AsyncSeq.findBack (fun x -> x % 2 = 0)
3985+
|> Async.RunSynchronously
3986+
|> ignore)
3987+
|> ignore
3988+
3989+
[<Test>]
3990+
let ``AsyncSeq.tryFindBackAsync returns last element satisfying async predicate`` () =
3991+
let result =
3992+
AsyncSeq.ofSeq [1; 2; 3; 4; 5]
3993+
|> AsyncSeq.tryFindBackAsync (fun x -> async { return x < 4 })
3994+
|> Async.RunSynchronously
3995+
Assert.AreEqual(Some 3, result)
3996+
3997+
[<Test>]
3998+
let ``AsyncSeq.tryFindBackAsync returns None when nothing matches`` () =
3999+
let result =
4000+
AsyncSeq.ofSeq [1; 2; 3]
4001+
|> AsyncSeq.tryFindBackAsync (fun x -> async { return x > 99 })
4002+
|> Async.RunSynchronously
4003+
Assert.AreEqual(None, result)
4004+
4005+
[<Test>]
4006+
let ``AsyncSeq.findBackAsync returns last matching element`` () =
4007+
let result =
4008+
AsyncSeq.ofSeq [10; 20; 5; 15; 30]
4009+
|> AsyncSeq.findBackAsync (fun x -> async { return x > 10 })
4010+
|> Async.RunSynchronously
4011+
Assert.AreEqual(30, result)
4012+
4013+
[<Test>]
4014+
let ``AsyncSeq.findBackAsync raises KeyNotFoundException when no match`` () =
4015+
Assert.Throws<System.Collections.Generic.KeyNotFoundException>(fun () ->
4016+
AsyncSeq.ofSeq [1; 2; 3]
4017+
|> AsyncSeq.findBackAsync (fun x -> async { return x > 99 })
4018+
|> Async.RunSynchronously
4019+
|> ignore)
4020+
|> ignore

0 commit comments

Comments
 (0)