Skip to content

Commit 0c285c4

Browse files
Add AsyncSeq.tryFindBack, findBack, tryFindBackAsync, findBackAsync
Adds four new functions mirroring Array.findBack / List.findBack from the F# standard library: - tryFindBackAsync: returns the last element satisfying an async predicate, or None - tryFindBack: synchronous predicate variant of the above - findBackAsync: like tryFindBackAsync but raises KeyNotFoundException - findBack: like tryFindBack but raises KeyNotFoundException Implementation scans the entire sequence, keeping track of the last matching element. 10 new tests added; all 382 tests pass. Co-authored-by: Copilot <[email protected]>
1 parent 484d414 commit 0c285c4

4 files changed

Lines changed: 126 additions & 0 deletions

File tree

RELEASE_NOTES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
### 4.11.0
22

3+
* Added `AsyncSeq.tryFindBack` / `AsyncSeq.tryFindBackAsync` — return the last element in the sequence satisfying a (sync or async) predicate, or `None` if no such element exists. The entire sequence is consumed.
4+
* Added `AsyncSeq.findBack` / `AsyncSeq.findBackAsync` — like `tryFindBack`/`tryFindBackAsync` but raise `KeyNotFoundException` when no matching element is found. Mirrors `Array.findBack` and `List.findBack`.
35
* Performance: `mapiAsync` — replaced `asyncSeq`-builder + `collect` implementation with a direct optimised enumerator (`OptimizedMapiAsyncEnumerator`), eliminating `collect` overhead and bringing per-element cost in line with `mapAsync`. Benchmarks added in `AsyncSeqMapiBenchmarks`.
46
* Design parity with FSharp.Control.TaskSeq (#277, batch 2):
57
* Added `AsyncSeq.tryTail` — returns `None` if the sequence is empty; otherwise returns `Some` of the tail. Safe counterpart to `tail`. Mirrors `TaskSeq.tryTail`.

src/FSharp.Control.AsyncSeq/AsyncSeq.fs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1337,6 +1337,30 @@ module AsyncSeq =
13371337
let findAsync f (source : AsyncSeq<'T>) =
13381338
source |> pickAsync (fun v -> async { let! b = f v in return if b then Some v else None })
13391339

1340+
let tryFindBackAsync (predicate: 'T -> Async<bool>) (source: AsyncSeq<'T>) : Async<'T option> = async {
1341+
use ie = source.GetEnumerator()
1342+
let! move = ie.MoveNext()
1343+
let mutable b = move
1344+
let mutable result = None
1345+
while b.IsSome do
1346+
let! ok = predicate b.Value
1347+
if ok then result <- b
1348+
let! next = ie.MoveNext()
1349+
b <- next
1350+
return result }
1351+
1352+
let tryFindBack (predicate: 'T -> bool) (source: AsyncSeq<'T>) : Async<'T option> =
1353+
tryFindBackAsync (predicate >> async.Return) source
1354+
1355+
let findBackAsync (predicate: 'T -> Async<bool>) (source: AsyncSeq<'T>) : Async<'T> = async {
1356+
let! result = tryFindBackAsync predicate source
1357+
match result with
1358+
| None -> return raise (System.Collections.Generic.KeyNotFoundException("An element satisfying the predicate was not found in the collection."))
1359+
| Some v -> return v }
1360+
1361+
let findBack (predicate: 'T -> bool) (source: AsyncSeq<'T>) : Async<'T> =
1362+
findBackAsync (predicate >> async.Return) source
1363+
13401364
let tryFindIndex f (source : AsyncSeq<'T>) = async {
13411365
use ie = source.GetEnumerator()
13421366
let! first = ie.MoveNext()

src/FSharp.Control.AsyncSeq/AsyncSeq.fsi

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,22 @@ module AsyncSeq =
377377
/// Raises KeyNotFoundException if no matching element is found.
378378
val findAsync : predicate:('T -> Async<bool>) -> source:AsyncSeq<'T> -> Async<'T>
379379

380+
/// Returns the last element in the sequence for which the given async predicate returns true,
381+
/// or <c>None</c> if no such element exists. The entire sequence is consumed.
382+
val tryFindBackAsync : predicate:('T -> Async<bool>) -> source:AsyncSeq<'T> -> Async<'T option>
383+
384+
/// Returns the last element in the sequence for which the given predicate returns true,
385+
/// or <c>None</c> if no such element exists. The entire sequence is consumed.
386+
val tryFindBack : predicate:('T -> bool) -> source:AsyncSeq<'T> -> Async<'T option>
387+
388+
/// Returns the last element in the sequence for which the given async predicate returns true.
389+
/// Raises <c>KeyNotFoundException</c> if no such element exists.
390+
val findBackAsync : predicate:('T -> Async<bool>) -> source:AsyncSeq<'T> -> Async<'T>
391+
392+
/// Returns the last element in the sequence for which the given predicate returns true.
393+
/// Raises <c>KeyNotFoundException</c> if no such element exists.
394+
val findBack : predicate:('T -> bool) -> source:AsyncSeq<'T> -> Async<'T>
395+
380396
/// Asynchronously find the index of the first value in a sequence for which the predicate returns true.
381397
/// Returns None if no matching element is found.
382398
val tryFindIndex : predicate:('T -> bool) -> source:AsyncSeq<'T> -> Async<int option>

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

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4179,3 +4179,87 @@ let ``AsyncSeq.partitionAsync splits by async predicate`` () =
41794179
|> Async.RunSynchronously
41804180
Assert.AreEqual([|2;4;6|], trues)
41814181
Assert.AreEqual([|1;3;5|], falses)
4182+
4183+
// ===== tryFindBack / findBack / tryFindBackAsync / findBackAsync =====
4184+
4185+
[<Test>]
4186+
let ``AsyncSeq.tryFindBack returns last matching element`` () =
4187+
let result =
4188+
AsyncSeq.ofSeq [1; 3; 5; 2; 4; 6; 7]
4189+
|> AsyncSeq.tryFindBack (fun x -> x % 2 = 0)
4190+
|> Async.RunSynchronously
4191+
Assert.AreEqual(Some 6, result)
4192+
4193+
[<Test>]
4194+
let ``AsyncSeq.tryFindBack returns None when no element matches`` () =
4195+
let result =
4196+
AsyncSeq.ofSeq [1; 3; 5]
4197+
|> AsyncSeq.tryFindBack (fun x -> x % 2 = 0)
4198+
|> Async.RunSynchronously
4199+
Assert.AreEqual(None, result)
4200+
4201+
[<Test>]
4202+
let ``AsyncSeq.tryFindBack returns None for empty sequence`` () =
4203+
let result =
4204+
AsyncSeq.empty<int>
4205+
|> AsyncSeq.tryFindBack (fun _ -> true)
4206+
|> Async.RunSynchronously
4207+
Assert.AreEqual(None, result)
4208+
4209+
[<Test>]
4210+
let ``AsyncSeq.tryFindBack returns last element when all match`` () =
4211+
let result =
4212+
AsyncSeq.ofSeq [10; 20; 30]
4213+
|> AsyncSeq.tryFindBack (fun _ -> true)
4214+
|> Async.RunSynchronously
4215+
Assert.AreEqual(Some 30, result)
4216+
4217+
[<Test>]
4218+
let ``AsyncSeq.findBack returns last matching element`` () =
4219+
let result =
4220+
AsyncSeq.ofSeq [2; 4; 1; 3; 6; 5]
4221+
|> AsyncSeq.findBack (fun x -> x % 2 = 0)
4222+
|> Async.RunSynchronously
4223+
Assert.AreEqual(6, result)
4224+
4225+
[<Test>]
4226+
let ``AsyncSeq.findBack raises KeyNotFoundException when no match`` () =
4227+
Assert.Throws<System.Collections.Generic.KeyNotFoundException>(fun () ->
4228+
AsyncSeq.ofSeq [1; 3; 5]
4229+
|> AsyncSeq.findBack (fun x -> x % 2 = 0)
4230+
|> Async.RunSynchronously
4231+
|> ignore)
4232+
|> ignore
4233+
4234+
[<Test>]
4235+
let ``AsyncSeq.tryFindBackAsync returns last element satisfying async predicate`` () =
4236+
let result =
4237+
AsyncSeq.ofSeq [1; 2; 3; 4; 5]
4238+
|> AsyncSeq.tryFindBackAsync (fun x -> async { return x < 4 })
4239+
|> Async.RunSynchronously
4240+
Assert.AreEqual(Some 3, result)
4241+
4242+
[<Test>]
4243+
let ``AsyncSeq.tryFindBackAsync returns None when nothing matches`` () =
4244+
let result =
4245+
AsyncSeq.ofSeq [1; 2; 3]
4246+
|> AsyncSeq.tryFindBackAsync (fun x -> async { return x > 99 })
4247+
|> Async.RunSynchronously
4248+
Assert.AreEqual(None, result)
4249+
4250+
[<Test>]
4251+
let ``AsyncSeq.findBackAsync returns last matching element`` () =
4252+
let result =
4253+
AsyncSeq.ofSeq [10; 20; 5; 15; 30]
4254+
|> AsyncSeq.findBackAsync (fun x -> async { return x > 10 })
4255+
|> Async.RunSynchronously
4256+
Assert.AreEqual(30, result)
4257+
4258+
[<Test>]
4259+
let ``AsyncSeq.findBackAsync raises KeyNotFoundException when no match`` () =
4260+
Assert.Throws<System.Collections.Generic.KeyNotFoundException>(fun () ->
4261+
AsyncSeq.ofSeq [1; 2; 3]
4262+
|> AsyncSeq.findBackAsync (fun x -> async { return x > 99 })
4263+
|> Async.RunSynchronously
4264+
|> ignore)
4265+
|> ignore

0 commit comments

Comments
 (0)