Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
### 4.6.0

<<<<<<< repo-assist/improve-isempty-tryhead-except-2471744-646c6897389cdfcb
* 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`.
=======
* Added `AsyncSeq.findIndex` — returns the index of the first element satisfying a predicate; raises `KeyNotFoundException` if no match, mirroring `Seq.findIndex`.
* Added `AsyncSeq.tryFindIndex` — returns the index of the first element satisfying a predicate as `option`, or `None` if not found, mirroring `Seq.tryFindIndex`.
* Added `AsyncSeq.findIndexAsync` — async-predicate variant of `AsyncSeq.findIndex`; raises `KeyNotFoundException` if no match.
* Added `AsyncSeq.tryFindIndexAsync` — async-predicate variant of `AsyncSeq.tryFindIndex`; returns `option`.
* Added `AsyncSeq.sortWith` — sorts the sequence using a custom comparison function, returning an array, mirroring `Seq.sortWith`.
>>>>>>> main

### 4.5.0

Expand Down
11 changes: 11 additions & 0 deletions src/FSharp.Control.AsyncSeq/AsyncSeq.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1066,6 +1066,13 @@
| 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
Expand Down Expand Up @@ -1421,6 +1428,10 @@
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<unit>) (s:AsyncSeq<'a>) : Async<unit> = async {
use mb = MailboxProcessor.Start (ignore >> async.Return)
Expand Down Expand Up @@ -2290,7 +2301,7 @@

[<CompilerMessage("The result of groupBy must be consumed with a parallel combinator such as AsyncSeq.mapAsyncParallel. Sequential consumption will deadlock because sub-sequence completion depends on other sub-sequences being consumed concurrently.", 9999)>]
let groupBy (p:'a -> 'k) (s:AsyncSeq<'a>) : AsyncSeq<'k * AsyncSeq<'a>> =
groupByAsync (p >> async.Return) s

Check warning on line 2304 in src/FSharp.Control.AsyncSeq/AsyncSeq.fs

View workflow job for this annotation

GitHub Actions / build

The result of groupByAsync must be consumed with a parallel combinator such as AsyncSeq.mapAsyncParallel. Sequential consumption will deadlock because sub-sequence completion depends on other sub-sequences being consumed concurrently.

Check warning on line 2304 in src/FSharp.Control.AsyncSeq/AsyncSeq.fs

View workflow job for this annotation

GitHub Actions / build

The result of groupByAsync must be consumed with a parallel combinator such as AsyncSeq.mapAsyncParallel. Sequential consumption will deadlock because sub-sequence completion depends on other sub-sequences being consumed concurrently.
#endif
#endif

Expand Down
12 changes: 12 additions & 0 deletions src/FSharp.Control.AsyncSeq/AsyncSeq.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -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<bool>

/// 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>
Expand Down Expand Up @@ -396,6 +404,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>
Expand Down
51 changes: 51 additions & 0 deletions tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2000,7 +2000,7 @@
let actual =
ls
|> AsyncSeq.ofSeq
|> AsyncSeq.groupBy p

Check warning on line 2003 in tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs

View workflow job for this annotation

GitHub Actions / build

The result of groupBy must be consumed with a parallel combinator such as AsyncSeq.mapAsyncParallel. Sequential consumption will deadlock because sub-sequence completion depends on other sub-sequences being consumed concurrently.
|> AsyncSeq.mapAsyncParallel (snd >> AsyncSeq.toListAsync)
Assert.AreEqual(expected, actual)

Expand All @@ -2009,7 +2009,7 @@
let expected = asyncSeq { raise (exn("test")) }
let actual =
asyncSeq { raise (exn("test")) }
|> AsyncSeq.groupBy (fun i -> i % 3)

Check warning on line 2012 in tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs

View workflow job for this annotation

GitHub Actions / build

The result of groupBy must be consumed with a parallel combinator such as AsyncSeq.mapAsyncParallel. Sequential consumption will deadlock because sub-sequence completion depends on other sub-sequences being consumed concurrently.
|> AsyncSeq.mapAsyncParallel (snd >> AsyncSeq.toListAsync)
Assert.AreEqual(expected, actual)

Expand Down Expand Up @@ -3275,6 +3275,57 @@
let result = AsyncSeq.tryItem 0 AsyncSeq.empty<int> |> Async.RunSynchronously
Assert.AreEqual(None, result)

// ===== isEmpty =====

[<Test>]
let ``AsyncSeq.isEmpty returns true for empty sequence`` () =
let result = AsyncSeq.isEmpty AsyncSeq.empty<int> |> Async.RunSynchronously
Assert.True(result)

[<Test>]
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)

[<Test>]
let ``AsyncSeq.isEmpty returns false for singleton`` () =
let result = AsyncSeq.isEmpty (AsyncSeq.singleton 42) |> Async.RunSynchronously
Assert.False(result)

// ===== tryHead =====

[<Test>]
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)

[<Test>]
let ``AsyncSeq.tryHead returns None for empty sequence`` () =
let result = AsyncSeq.tryHead AsyncSeq.empty<int> |> Async.RunSynchronously
Assert.AreEqual(None, result)

// ===== except =====

[<Test>]
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)

[<Test>]
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)

[<Test>]
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)

// ===== findIndex / tryFindIndex / findIndexAsync / tryFindIndexAsync =====

[<Test>]
Expand Down