From bdc437a62df7daa9c28ce0deb0667489387d2d77 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 2 Mar 2026 17:18:22 +0000 Subject: [PATCH 1/3] 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> --- RELEASE_NOTES.md | 6 ++ src/FSharp.Control.AsyncSeq/AsyncSeq.fs | 11 ++++ src/FSharp.Control.AsyncSeq/AsyncSeq.fsi | 12 ++++ .../AsyncSeqTests.fs | 56 +++++++++++++++++++ 4 files changed, 85 insertions(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index be845e4..63ed303 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,3 +1,9 @@ +### 4.6.0 + +* Added `AsyncSeq.isEmpty` — returns `true` if the sequence contains no elements; short-circuits after the first element, mirroring `Seq.isEmpty`. +* 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`). +* Added `AsyncSeq.except` — returns a new sequence excluding all elements present in a given collection, mirroring `Seq.except`. + ### 4.5.0 * Added `AsyncSeq.last` — returns the last element of the sequence; raises `InvalidOperationException` if empty, mirroring `Seq.last`. diff --git a/src/FSharp.Control.AsyncSeq/AsyncSeq.fs b/src/FSharp.Control.AsyncSeq/AsyncSeq.fs index 554cf07..f87938b 100644 --- a/src/FSharp.Control.AsyncSeq/AsyncSeq.fs +++ b/src/FSharp.Control.AsyncSeq/AsyncSeq.fs @@ -1066,6 +1066,13 @@ module AsyncSeq = | None -> return raise (System.InvalidOperationException("The input sequence was empty.")) | Some v -> return v } + let tryHead (source : AsyncSeq<'T>) = tryFirst source + + let isEmpty (source : AsyncSeq<'T>) = async { + use ie = source.GetEnumerator() + let! v = ie.MoveNext() + return v.IsNone } + let last (source : AsyncSeq<'T>) = async { let! result = tryLast source match result with @@ -1381,6 +1388,10 @@ module AsyncSeq = let filter f (source : AsyncSeq<'T>) = filterAsync (f >> async.Return) source + let except (excluded : seq<'T>) (source : AsyncSeq<'T>) : AsyncSeq<'T> = + let s = System.Collections.Generic.HashSet(excluded) + source |> filter (fun x -> not (s.Contains(x))) + #if !FABLE_COMPILER let iterAsyncParallel (f:'a -> Async) (s:AsyncSeq<'a>) : Async = async { use mb = MailboxProcessor.Start (ignore >> async.Return) diff --git a/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi b/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi index 4c84a41..cf722fc 100644 --- a/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi +++ b/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi @@ -179,6 +179,14 @@ module AsyncSeq = /// Raises InvalidOperationException if the sequence is empty. val head : source:AsyncSeq<'T> -> Async<'T> + /// Asynchronously returns the first element of the asynchronous sequence as an option, + /// or None if the sequence is empty. Mirrors Seq.tryHead. + val tryHead : source:AsyncSeq<'T> -> Async<'T option> + + /// Asynchronously returns true if the asynchronous sequence contains no elements, false otherwise. + /// Short-circuits after the first element. Mirrors Seq.isEmpty. + val isEmpty : source:AsyncSeq<'T> -> Async + /// Asynchronously returns the only element of the asynchronous sequence. /// Raises InvalidOperationException if the sequence is empty or contains more than one element. val exactlyOne : source:AsyncSeq<'T> -> Async<'T> @@ -380,6 +388,10 @@ module AsyncSeq = /// and processes the input element immediately. val filter : predicate:('T -> bool) -> source:AsyncSeq<'T> -> AsyncSeq<'T> + /// Returns a new asynchronous sequence containing only elements that are not present + /// in the given excluded collection. Uses a HashSet for O(1) lookup. Mirrors Seq.except. + val except : excluded:seq<'T> -> source:AsyncSeq<'T> -> AsyncSeq<'T> when 'T : equality + /// Creates an asynchronous sequence that lazily takes element from an /// input synchronous sequence and returns them one-by-one. val ofSeq : source:seq<'T> -> AsyncSeq<'T> diff --git a/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs b/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs index 88ce7f1..3f13995 100644 --- a/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs +++ b/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs @@ -3274,3 +3274,59 @@ let ``AsyncSeq.tryItem returns None for negative index`` () = let ``AsyncSeq.tryItem returns None on empty sequence`` () = let result = AsyncSeq.tryItem 0 AsyncSeq.empty |> Async.RunSynchronously Assert.AreEqual(None, result) + +// ===== isEmpty ===== + +[] +let ``AsyncSeq.isEmpty returns true for empty sequence`` () = + let result = AsyncSeq.isEmpty AsyncSeq.empty |> Async.RunSynchronously + Assert.True(result) + +[] +let ``AsyncSeq.isEmpty returns false for non-empty sequence`` () = + let source = asyncSeq { yield 1; yield 2 } + let result = AsyncSeq.isEmpty source |> Async.RunSynchronously + Assert.False(result) + +[] +let ``AsyncSeq.isEmpty returns false for singleton`` () = + let result = AsyncSeq.isEmpty (AsyncSeq.singleton 42) |> Async.RunSynchronously + Assert.False(result) + +// ===== tryHead ===== + +[] +let ``AsyncSeq.tryHead returns Some for non-empty sequence`` () = + let source = asyncSeq { yield 42; yield 99 } + let result = AsyncSeq.tryHead source |> Async.RunSynchronously + Assert.AreEqual(Some 42, result) + +[] +let ``AsyncSeq.tryHead returns None for empty sequence`` () = + let result = AsyncSeq.tryHead AsyncSeq.empty |> Async.RunSynchronously + Assert.AreEqual(None, result) + +// ===== except ===== + +[] +let ``AsyncSeq.except removes excluded elements`` () = + let source = asyncSeq { yield 1; yield 2; yield 3; yield 4; yield 5 } + let result = AsyncSeq.except [2; 4] source |> AsyncSeq.toArrayAsync |> Async.RunSynchronously + Assert.AreEqual([| 1; 3; 5 |], result) + +[] +let ``AsyncSeq.except with empty excluded returns all elements`` () = + let source = asyncSeq { yield 1; yield 2; yield 3 } + let result = AsyncSeq.except [] source |> AsyncSeq.toArrayAsync |> Async.RunSynchronously + Assert.AreEqual([| 1; 2; 3 |], result) + +[] +let ``AsyncSeq.except with all excluded returns empty sequence`` () = + let source = asyncSeq { yield 1; yield 2; yield 3 } + let result = AsyncSeq.except [1; 2; 3] source |> AsyncSeq.toArrayAsync |> Async.RunSynchronously + Assert.AreEqual([||], result) + +[] +let ``AsyncSeq.except on empty source returns empty`` () = + let result = AsyncSeq.except [1; 2] AsyncSeq.empty |> AsyncSeq.toArrayAsync |> Async.RunSynchronously + Assert.AreEqual([||], result) From cf04298faebd67f15e5758dfaa67ecac9d588948 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 2 Mar 2026 17:20:52 +0000 Subject: [PATCH 2/3] ci: trigger CI checks From de61f20e802019b8886d711d28270bf1de829e6a Mon Sep 17 00:00:00 2001 From: Don Syme Date: Tue, 3 Mar 2026 01:00:21 +0000 Subject: [PATCH 3/3] Update AsyncSeqTests.fs --- tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs b/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs index 8f2229c..74c43c7 100644 --- a/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs +++ b/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs @@ -3326,10 +3326,6 @@ let ``AsyncSeq.except with all excluded returns empty sequence`` () = let result = AsyncSeq.except [1; 2; 3] source |> AsyncSeq.toArrayAsync |> Async.RunSynchronously Assert.AreEqual([||], result) -[] -let ``AsyncSeq.except on empty source returns empty`` () = - let result = AsyncSeq.except [1; 2] AsyncSeq.empty |> AsyncSeq.toArrayAsync |> Async.RunSynchronously - // ===== findIndex / tryFindIndex / findIndexAsync / tryFindIndexAsync ===== []