Skip to content

Commit 6b7e2dc

Browse files
github-actions[bot]Repo AssistCopilot
authored
[Repo Assist] Add AsyncSeq.head, iteri, find, tryFindAsync, tail (#255)
* Add AsyncSeq.head, iteri, find, tryFindAsync, tail Five new combinators mirroring standard F# module functions: - head: returns first element; raises InvalidOperationException on empty (mirrors Seq.head) - iteri: sync iteration with element index exposed in .fsi (was already implemented but not exported) - find: returns first matching element; raises KeyNotFoundException if no match (mirrors Seq.find) - tryFindAsync: async-predicate variant of tryFind, returning option - tail: returns sequence without first element (mirrors Seq.tail / List.tail) 11 new tests added; all 248 tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * ci: trigger CI checks --------- Co-authored-by: Repo Assist <repo-assist@github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 5323ab0 commit 6b7e2dc

File tree

4 files changed

+136
-0
lines changed

4 files changed

+136
-0
lines changed

RELEASE_NOTES.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
### 4.3.0
2+
3+
* Added `AsyncSeq.head` — returns the first element of the sequence; raises `InvalidOperationException` if empty, mirroring `Seq.head`.
4+
* Added `AsyncSeq.iteri` — iterates the sequence calling a synchronous action with each element's index, mirroring `Seq.iteri`.
5+
* Added `AsyncSeq.find` — returns the first element satisfying a predicate; raises `KeyNotFoundException` if no match, mirroring `Seq.find`.
6+
* Added `AsyncSeq.tryFindAsync` — async-predicate variant of `AsyncSeq.tryFind`, returning the first matching element as `option`.
7+
* Added `AsyncSeq.tail` — returns the sequence without its first element, mirroring `Seq.tail`.
8+
19
### 4.2.0
210

311
* Added `AsyncSeq.zip3`, `AsyncSeq.zipWith3`, and `AsyncSeq.zipWithAsync3` — combinators for zipping three async sequences, mirroring `Seq.zip3` ([PR #254](https://github.com/fsprojects/FSharp.Control.AsyncSeq/pull/254)).

src/FSharp.Control.AsyncSeq/AsyncSeq.fs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1060,6 +1060,12 @@ module AsyncSeq =
10601060
| None -> return def
10611061
| Some v -> return v }
10621062

1063+
let head (source : AsyncSeq<'T>) = async {
1064+
let! result = tryFirst source
1065+
match result with
1066+
| None -> return raise (System.InvalidOperationException("The input sequence was empty."))
1067+
| Some v -> return v }
1068+
10631069
let exactlyOne (source : AsyncSeq<'T>) = async {
10641070
use ie = source.GetEnumerator()
10651071
let! first = ie.MoveNext()
@@ -1168,6 +1174,12 @@ module AsyncSeq =
11681174
let tryFind f (source : AsyncSeq<'T>) =
11691175
source |> tryPick (fun v -> if f v then Some v else None)
11701176

1177+
let tryFindAsync f (source : AsyncSeq<'T>) =
1178+
source |> tryPickAsync (fun v -> async { let! b = f v in return if b then Some v else None })
1179+
1180+
let find f (source : AsyncSeq<'T>) =
1181+
source |> pick (fun v -> if f v then Some v else None)
1182+
11711183
let exists f (source : AsyncSeq<'T>) =
11721184
source |> tryFind f |> Async.map Option.isSome
11731185

@@ -1715,6 +1727,8 @@ module AsyncSeq =
17151727
let! moven = ie.MoveNext()
17161728
b := moven }
17171729

1730+
let tail (source : AsyncSeq<'T>) : AsyncSeq<'T> = skip 1 source
1731+
17181732
let toArrayAsync (source : AsyncSeq<'T>) : Async<'T[]> = async {
17191733
let ra = (new ResizeArray<_>())
17201734
use ie = source.GetEnumerator()

src/FSharp.Control.AsyncSeq/AsyncSeq.fsi

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,10 @@ module AsyncSeq =
163163
/// given asynchronous sequence (or None if the sequence is empty).
164164
val tryFirst : source:AsyncSeq<'T> -> Async<'T option>
165165

166+
/// Asynchronously returns the first element of the asynchronous sequence.
167+
/// Raises InvalidOperationException if the sequence is empty.
168+
val head : source:AsyncSeq<'T> -> Async<'T>
169+
166170
/// Asynchronously returns the only element of the asynchronous sequence.
167171
/// Raises InvalidOperationException if the sequence is empty or contains more than one element.
168172
val exactlyOne : source:AsyncSeq<'T> -> Async<'T>
@@ -189,6 +193,10 @@ module AsyncSeq =
189193
/// The input sequence will be asked for the next element after the processing of an element completes.
190194
val iteriAsync : action:(int -> 'T -> Async<unit>) -> source:AsyncSeq<'T> -> Async<unit>
191195

196+
/// Iterates over the input sequence and calls the specified function for
197+
/// every value, passing along the index of that element.
198+
val iteri : action:(int -> 'T -> unit) -> source:AsyncSeq<'T> -> Async<unit>
199+
192200
#if !FABLE_COMPILER
193201
/// Iterates over the input sequence and calls the specified asynchronous function for
194202
/// every value. Each action computation is started but not awaited before consuming
@@ -312,6 +320,13 @@ module AsyncSeq =
312320
/// Asynchronously find the first value in a sequence for which the predicate returns true
313321
val tryFind : predicate:('T -> bool) -> source:AsyncSeq<'T> -> Async<'T option>
314322

323+
/// Asynchronously find the first value in a sequence for which the async predicate returns true
324+
val tryFindAsync : predicate:('T -> Async<bool>) -> source:AsyncSeq<'T> -> Async<'T option>
325+
326+
/// Asynchronously find the first value in a sequence for which the predicate returns true.
327+
/// Raises KeyNotFoundException if no matching element is found.
328+
val find : predicate:('T -> bool) -> source:AsyncSeq<'T> -> Async<'T>
329+
315330
/// Asynchronously determine if there is a value in the sequence for which the predicate returns true
316331
val exists : predicate:('T -> bool) -> source:AsyncSeq<'T> -> Async<bool>
317332

@@ -517,6 +532,10 @@ module AsyncSeq =
517532
/// then returns the rest of the sequence unmodified.
518533
val skip : count:int -> source:AsyncSeq<'T> -> AsyncSeq<'T>
519534

535+
/// Returns an asynchronous sequence that skips the first element of the input sequence.
536+
/// Returns an empty sequence if the source is empty.
537+
val tail : source:AsyncSeq<'T> -> AsyncSeq<'T>
538+
520539
/// Creates an async computation which iterates the AsyncSeq and collects the output into an array.
521540
val toArrayAsync : source:AsyncSeq<'T> -> Async<'T []>
522541

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

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3046,3 +3046,98 @@ let ``AsyncSeq.tryExactlyOne returns None for sequence with more than one elemen
30463046
let result = AsyncSeq.tryExactlyOne source |> Async.RunSynchronously
30473047
Assert.AreEqual(None, result)
30483048
} |> Async.RunSynchronously
3049+
3050+
// ===== head =====
3051+
3052+
[<Test>]
3053+
let ``AsyncSeq.head returns first element`` () =
3054+
let source = asyncSeq { yield 42; yield 99 }
3055+
let result = AsyncSeq.head source |> Async.RunSynchronously
3056+
Assert.AreEqual(42, result)
3057+
3058+
[<Test>]
3059+
let ``AsyncSeq.head raises on empty sequence`` () =
3060+
Assert.Throws<InvalidOperationException>(fun () ->
3061+
AsyncSeq.head AsyncSeq.empty<int> |> Async.RunSynchronously |> ignore) |> ignore
3062+
3063+
// ===== iteri =====
3064+
3065+
[<Test>]
3066+
let ``AsyncSeq.iteri calls action with correct indices`` () =
3067+
let indices = System.Collections.Generic.List<int>()
3068+
let values = System.Collections.Generic.List<int>()
3069+
asyncSeq { yield 10; yield 20; yield 30 }
3070+
|> AsyncSeq.iteri (fun i v -> indices.Add(i); values.Add(v))
3071+
|> Async.RunSynchronously
3072+
Assert.AreEqual([| 0; 1; 2 |], indices |> Seq.toArray)
3073+
Assert.AreEqual([| 10; 20; 30 |], values |> Seq.toArray)
3074+
3075+
[<Test>]
3076+
let ``AsyncSeq.iteri on empty sequence does nothing`` () =
3077+
let mutable count = 0
3078+
AsyncSeq.empty<int>
3079+
|> AsyncSeq.iteri (fun _ _ -> count <- count + 1)
3080+
|> Async.RunSynchronously
3081+
Assert.AreEqual(0, count)
3082+
3083+
// ===== find / tryFindAsync =====
3084+
3085+
[<Test>]
3086+
let ``AsyncSeq.find returns matching element`` () =
3087+
for i in 0 .. 10 do
3088+
let ls = [ 1 .. i + 1 ]
3089+
let result = AsyncSeq.ofSeq ls |> AsyncSeq.find (fun x -> x = i + 1) |> Async.RunSynchronously
3090+
Assert.AreEqual(i + 1, result)
3091+
3092+
[<Test>]
3093+
let ``AsyncSeq.find raises KeyNotFoundException when no match`` () =
3094+
Assert.Throws<System.Collections.Generic.KeyNotFoundException>(fun () ->
3095+
AsyncSeq.ofSeq [ 1; 2; 3 ] |> AsyncSeq.find (fun x -> x = 99) |> Async.RunSynchronously |> ignore)
3096+
|> ignore
3097+
3098+
[<Test>]
3099+
let ``AsyncSeq.tryFindAsync returns Some when found`` () =
3100+
for i in 0 .. 10 do
3101+
let ls = [ 1 .. i + 1 ]
3102+
let result =
3103+
AsyncSeq.ofSeq ls
3104+
|> AsyncSeq.tryFindAsync (fun x -> async { return x = i + 1 })
3105+
|> Async.RunSynchronously
3106+
Assert.AreEqual(Some (i + 1), result)
3107+
3108+
[<Test>]
3109+
let ``AsyncSeq.tryFindAsync returns None when not found`` () =
3110+
let result =
3111+
AsyncSeq.ofSeq [ 1; 2; 3 ]
3112+
|> AsyncSeq.tryFindAsync (fun x -> async { return x = 99 })
3113+
|> Async.RunSynchronously
3114+
Assert.AreEqual(None, result)
3115+
3116+
// ===== tail =====
3117+
3118+
[<Test>]
3119+
let ``AsyncSeq.tail skips first element`` () =
3120+
let result =
3121+
asyncSeq { yield 1; yield 2; yield 3 }
3122+
|> AsyncSeq.tail
3123+
|> AsyncSeq.toListAsync
3124+
|> Async.RunSynchronously
3125+
Assert.AreEqual([ 2; 3 ], result)
3126+
3127+
[<Test>]
3128+
let ``AsyncSeq.tail on singleton returns empty`` () =
3129+
let result =
3130+
asyncSeq { yield 42 }
3131+
|> AsyncSeq.tail
3132+
|> AsyncSeq.toListAsync
3133+
|> Async.RunSynchronously
3134+
Assert.AreEqual([], result)
3135+
3136+
[<Test>]
3137+
let ``AsyncSeq.tail on empty returns empty`` () =
3138+
let result =
3139+
AsyncSeq.empty<int>
3140+
|> AsyncSeq.tail
3141+
|> AsyncSeq.toListAsync
3142+
|> Async.RunSynchronously
3143+
Assert.AreEqual([], result)

0 commit comments

Comments
 (0)