Skip to content

Commit 8ee2296

Browse files
github-actions[bot]Copilotdsyme
authored
[Repo Assist] Add AsyncSeq.isEmpty, tryHead, except (#264)
* Add AsyncSeq.isEmpty, tryHead, except - isEmpty: checks if sequence has no elements; short-circuits after the first - tryHead: returns first element as option (mirrors Seq.tryHead / alias for tryFirst) - except: filters out elements present in a given collection (mirrors Seq.except) All 276 tests pass (267 pre-existing + 9 new). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * ci: trigger CI checks * Update AsyncSeqTests.fs --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Don Syme <dsyme@users.noreply.github.com>
1 parent 3b830d2 commit 8ee2296

File tree

4 files changed

+80
-0
lines changed

4 files changed

+80
-0
lines changed

RELEASE_NOTES.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
### 4.6.0
22

3+
<<<<<<< repo-assist/improve-isempty-tryhead-except-2471744-646c6897389cdfcb
4+
* Added `AsyncSeq.isEmpty` — returns `true` if the sequence contains no elements; short-circuits after the first element, mirroring `Seq.isEmpty`.
5+
* Added `AsyncSeq.tryHead` — returns the first element as `option`, or `None` if the sequence is empty, mirroring `Seq.tryHead` (equivalent to the existing `AsyncSeq.tryFirst`).
6+
* Added `AsyncSeq.except` — returns a new sequence excluding all elements present in a given collection, mirroring `Seq.except`.
7+
=======
38
* Added `AsyncSeq.findIndex` — returns the index of the first element satisfying a predicate; raises `KeyNotFoundException` if no match, mirroring `Seq.findIndex`.
49
* Added `AsyncSeq.tryFindIndex` — returns the index of the first element satisfying a predicate as `option`, or `None` if not found, mirroring `Seq.tryFindIndex`.
510
* Added `AsyncSeq.findIndexAsync` — async-predicate variant of `AsyncSeq.findIndex`; raises `KeyNotFoundException` if no match.
611
* Added `AsyncSeq.tryFindIndexAsync` — async-predicate variant of `AsyncSeq.tryFindIndex`; returns `option`.
712
* Added `AsyncSeq.sortWith` — sorts the sequence using a custom comparison function, returning an array, mirroring `Seq.sortWith`.
13+
>>>>>>> main
814
915
### 4.5.0
1016

src/FSharp.Control.AsyncSeq/AsyncSeq.fs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1066,6 +1066,13 @@ module AsyncSeq =
10661066
| None -> return raise (System.InvalidOperationException("The input sequence was empty."))
10671067
| Some v -> return v }
10681068

1069+
let tryHead (source : AsyncSeq<'T>) = tryFirst source
1070+
1071+
let isEmpty (source : AsyncSeq<'T>) = async {
1072+
use ie = source.GetEnumerator()
1073+
let! v = ie.MoveNext()
1074+
return v.IsNone }
1075+
10691076
let last (source : AsyncSeq<'T>) = async {
10701077
let! result = tryLast source
10711078
match result with
@@ -1421,6 +1428,10 @@ module AsyncSeq =
14211428
let filter f (source : AsyncSeq<'T>) =
14221429
filterAsync (f >> async.Return) source
14231430

1431+
let except (excluded : seq<'T>) (source : AsyncSeq<'T>) : AsyncSeq<'T> =
1432+
let s = System.Collections.Generic.HashSet(excluded)
1433+
source |> filter (fun x -> not (s.Contains(x)))
1434+
14241435
#if !FABLE_COMPILER
14251436
let iterAsyncParallel (f:'a -> Async<unit>) (s:AsyncSeq<'a>) : Async<unit> = async {
14261437
use mb = MailboxProcessor.Start (ignore >> async.Return)

src/FSharp.Control.AsyncSeq/AsyncSeq.fsi

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,14 @@ module AsyncSeq =
179179
/// Raises InvalidOperationException if the sequence is empty.
180180
val head : source:AsyncSeq<'T> -> Async<'T>
181181

182+
/// Asynchronously returns the first element of the asynchronous sequence as an option,
183+
/// or None if the sequence is empty. Mirrors Seq.tryHead.
184+
val tryHead : source:AsyncSeq<'T> -> Async<'T option>
185+
186+
/// Asynchronously returns true if the asynchronous sequence contains no elements, false otherwise.
187+
/// Short-circuits after the first element. Mirrors Seq.isEmpty.
188+
val isEmpty : source:AsyncSeq<'T> -> Async<bool>
189+
182190
/// Asynchronously returns the only element of the asynchronous sequence.
183191
/// Raises InvalidOperationException if the sequence is empty or contains more than one element.
184192
val exactlyOne : source:AsyncSeq<'T> -> Async<'T>
@@ -396,6 +404,10 @@ module AsyncSeq =
396404
/// and processes the input element immediately.
397405
val filter : predicate:('T -> bool) -> source:AsyncSeq<'T> -> AsyncSeq<'T>
398406

407+
/// Returns a new asynchronous sequence containing only elements that are not present
408+
/// in the given excluded collection. Uses a HashSet for O(1) lookup. Mirrors Seq.except.
409+
val except : excluded:seq<'T> -> source:AsyncSeq<'T> -> AsyncSeq<'T> when 'T : equality
410+
399411
/// Creates an asynchronous sequence that lazily takes element from an
400412
/// input synchronous sequence and returns them one-by-one.
401413
val ofSeq : source:seq<'T> -> AsyncSeq<'T>

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

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3275,6 +3275,57 @@ let ``AsyncSeq.tryItem returns None on empty sequence`` () =
32753275
let result = AsyncSeq.tryItem 0 AsyncSeq.empty<int> |> Async.RunSynchronously
32763276
Assert.AreEqual(None, result)
32773277

3278+
// ===== isEmpty =====
3279+
3280+
[<Test>]
3281+
let ``AsyncSeq.isEmpty returns true for empty sequence`` () =
3282+
let result = AsyncSeq.isEmpty AsyncSeq.empty<int> |> Async.RunSynchronously
3283+
Assert.True(result)
3284+
3285+
[<Test>]
3286+
let ``AsyncSeq.isEmpty returns false for non-empty sequence`` () =
3287+
let source = asyncSeq { yield 1; yield 2 }
3288+
let result = AsyncSeq.isEmpty source |> Async.RunSynchronously
3289+
Assert.False(result)
3290+
3291+
[<Test>]
3292+
let ``AsyncSeq.isEmpty returns false for singleton`` () =
3293+
let result = AsyncSeq.isEmpty (AsyncSeq.singleton 42) |> Async.RunSynchronously
3294+
Assert.False(result)
3295+
3296+
// ===== tryHead =====
3297+
3298+
[<Test>]
3299+
let ``AsyncSeq.tryHead returns Some for non-empty sequence`` () =
3300+
let source = asyncSeq { yield 42; yield 99 }
3301+
let result = AsyncSeq.tryHead source |> Async.RunSynchronously
3302+
Assert.AreEqual(Some 42, result)
3303+
3304+
[<Test>]
3305+
let ``AsyncSeq.tryHead returns None for empty sequence`` () =
3306+
let result = AsyncSeq.tryHead AsyncSeq.empty<int> |> Async.RunSynchronously
3307+
Assert.AreEqual(None, result)
3308+
3309+
// ===== except =====
3310+
3311+
[<Test>]
3312+
let ``AsyncSeq.except removes excluded elements`` () =
3313+
let source = asyncSeq { yield 1; yield 2; yield 3; yield 4; yield 5 }
3314+
let result = AsyncSeq.except [2; 4] source |> AsyncSeq.toArrayAsync |> Async.RunSynchronously
3315+
Assert.AreEqual([| 1; 3; 5 |], result)
3316+
3317+
[<Test>]
3318+
let ``AsyncSeq.except with empty excluded returns all elements`` () =
3319+
let source = asyncSeq { yield 1; yield 2; yield 3 }
3320+
let result = AsyncSeq.except [] source |> AsyncSeq.toArrayAsync |> Async.RunSynchronously
3321+
Assert.AreEqual([| 1; 2; 3 |], result)
3322+
3323+
[<Test>]
3324+
let ``AsyncSeq.except with all excluded returns empty sequence`` () =
3325+
let source = asyncSeq { yield 1; yield 2; yield 3 }
3326+
let result = AsyncSeq.except [1; 2; 3] source |> AsyncSeq.toArrayAsync |> Async.RunSynchronously
3327+
Assert.AreEqual([||], result)
3328+
32783329
// ===== findIndex / tryFindIndex / findIndexAsync / tryFindIndexAsync =====
32793330

32803331
[<Test>]

0 commit comments

Comments
 (0)