Skip to content

Commit bdc437a

Browse files
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 <[email protected]>
1 parent f2f8be5 commit bdc437a

4 files changed

Lines changed: 85 additions & 0 deletions

File tree

RELEASE_NOTES.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
### 4.6.0
2+
3+
* Added `AsyncSeq.isEmpty` — returns `true` if the sequence contains no elements; short-circuits after the first element, mirroring `Seq.isEmpty`.
4+
* 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`).
5+
* Added `AsyncSeq.except` — returns a new sequence excluding all elements present in a given collection, mirroring `Seq.except`.
6+
17
### 4.5.0
28

39
* Added `AsyncSeq.last` — returns the last element of the sequence; raises `InvalidOperationException` if empty, mirroring `Seq.last`.

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
@@ -1381,6 +1388,10 @@ module AsyncSeq =
13811388
let filter f (source : AsyncSeq<'T>) =
13821389
filterAsync (f >> async.Return) source
13831390

1391+
let except (excluded : seq<'T>) (source : AsyncSeq<'T>) : AsyncSeq<'T> =
1392+
let s = System.Collections.Generic.HashSet(excluded)
1393+
source |> filter (fun x -> not (s.Contains(x)))
1394+
13841395
#if !FABLE_COMPILER
13851396
let iterAsyncParallel (f:'a -> Async<unit>) (s:AsyncSeq<'a>) : Async<unit> = async {
13861397
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>
@@ -380,6 +388,10 @@ module AsyncSeq =
380388
/// and processes the input element immediately.
381389
val filter : predicate:('T -> bool) -> source:AsyncSeq<'T> -> AsyncSeq<'T>
382390

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

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

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3274,3 +3274,59 @@ let ``AsyncSeq.tryItem returns None for negative index`` () =
32743274
let ``AsyncSeq.tryItem returns None on empty sequence`` () =
32753275
let result = AsyncSeq.tryItem 0 AsyncSeq.empty<int> |> Async.RunSynchronously
32763276
Assert.AreEqual(None, result)
3277+
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+
3329+
[<Test>]
3330+
let ``AsyncSeq.except on empty source returns empty`` () =
3331+
let result = AsyncSeq.except [1; 2] AsyncSeq.empty<int> |> AsyncSeq.toArrayAsync |> Async.RunSynchronously
3332+
Assert.AreEqual([||], result)

0 commit comments

Comments
 (0)