Skip to content
Open
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
8 changes: 8 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
### 4.11.0

* Part of ongoing design-parity work with FSharp.Control.TaskSeq (see #277).
* Design parity with FSharp.Control.TaskSeq (#277, batch 2):
* Added `AsyncSeq.tryTail` β€” returns `None` if the sequence is empty; otherwise returns `Some` of the tail. Safe counterpart to `tail`. Mirrors `TaskSeq.tryTail`.
* Added `AsyncSeq.where` / `AsyncSeq.whereAsync` β€” aliases for `filter` / `filterAsync`, mirroring the naming convention in `TaskSeq` and F# 8 collection expressions.
Expand All @@ -12,6 +13,13 @@
* Added `AsyncSeq.delay` β€” defers sequence creation to enumeration time by calling a factory function each time `GetAsyncEnumerator` is called. Mirrors `TaskSeq.delay`.
* Added `AsyncSeq.collectAsync` β€” like `collect` but the mapping function is asynchronous (`'T -> Async<AsyncSeq<'U>>`). Mirrors `TaskSeq.collectAsync`.
* Added `AsyncSeq.partition` / `AsyncSeq.partitionAsync` β€” splits a sequence into two arrays using a (optionally async) predicate; the first array contains matching elements, the second non-matching. Mirrors `TaskSeq.partition` / `TaskSeq.partitionAsync`.
* Added `AsyncSeq.insertManyAt` β€” inserts multiple values before the element at the given index. Mirrors `Seq.insertManyAt` and `TaskSeq.insertManyAt`.
* Added `AsyncSeq.removeManyAt` β€” removes a run of elements starting at the given index. Mirrors `Seq.removeManyAt` and `TaskSeq.removeManyAt`.
* Added `AsyncSeq.box` β€” boxes each element to `obj`. Mirrors `TaskSeq.box`.
* Added `AsyncSeq.unbox<'T>` β€” unboxes each `obj` element to `'T`. Mirrors `TaskSeq.unbox`.
* Added `AsyncSeq.cast<'T>` β€” dynamically casts each `obj` element to `'T`. Mirrors `TaskSeq.cast`.
* Added `AsyncSeq.lengthOrMax` β€” counts elements up to a maximum, avoiding full enumeration of long or infinite sequences. Mirrors `TaskSeq.lengthOrMax`.
* Note: `AsyncSeq.except` already accepts `seq<'T>` for the excluded collection, so no separate `exceptOfSeq` is needed.
* Tests: added 14 new unit tests covering previously untested functions β€” `AsyncSeq.indexed`, `AsyncSeq.iteriAsync`, `AsyncSeq.tryLast`, `AsyncSeq.replicateUntilNoneAsync`, and `AsyncSeq.reduceAsync` (empty-sequence edge case).

### 4.10.0
Expand Down
34 changes: 34 additions & 0 deletions src/FSharp.Control.AsyncSeq/AsyncSeq.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1626,6 +1626,34 @@ module AsyncSeq =
elif i.Value < index then
invalidArg "index" "The index is outside the range of elements in the collection." }

let insertManyAt (index : int) (values : seq<'T>) (source : AsyncSeq<'T>) : AsyncSeq<'T> = asyncSeq {
if index < 0 then invalidArg "index" "must be non-negative"
let i = ref 0
for x in source do
if i.Value = index then yield! ofSeq values
yield x
i := i.Value + 1
if i.Value = index then yield! ofSeq values
elif i.Value < index then
invalidArg "index" "The index is outside the range of elements in the collection." }

let removeManyAt (index : int) (count : int) (source : AsyncSeq<'T>) : AsyncSeq<'T> = asyncSeq {
if index < 0 then invalidArg "index" "must be non-negative"
if count < 0 then invalidArg "count" "must be non-negative"
let i = ref 0
for x in source do
if i.Value < index || i.Value >= index + count then yield x
i := i.Value + 1 }

let box (source : AsyncSeq<'T>) : AsyncSeq<obj> =
map Microsoft.FSharp.Core.Operators.box source

let unbox<'T> (source : AsyncSeq<obj>) : AsyncSeq<'T> =
map Microsoft.FSharp.Core.Operators.unbox source

let cast<'T> (source : AsyncSeq<obj>) : AsyncSeq<'T> =
map Microsoft.FSharp.Core.Operators.unbox source

#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 @@ -2040,6 +2068,12 @@ module AsyncSeq =

let truncate count source = take count source

let lengthOrMax (max : int) (source : AsyncSeq<'T>) : Async<int> =
async {
let! n = source |> take max |> length
return int n
}

let skip count (source : AsyncSeq<'T>) : AsyncSeq<_> =
if count < 0 then invalidArg "count" "must be non-negative"
AsyncSeqImpl(fun () -> new OptimizedSkipEnumerator<'T>(source.GetEnumerator(), count) :> IAsyncSeqEnumerator<'T>) :> AsyncSeq<'T>
Expand Down
24 changes: 24 additions & 0 deletions src/FSharp.Control.AsyncSeq/AsyncSeq.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,11 @@ module AsyncSeq =
/// Asynchronously determine the number of elements in the sequence
val length : source:AsyncSeq<'T> -> Async<int64>

/// Asynchronously counts elements up to a maximum. Returns the actual count if the sequence has
/// fewer than 'max' elements, otherwise returns 'max'. Avoids full enumeration of long sequences.
/// Mirrors TaskSeq.lengthOrMax.
val lengthOrMax : max:int -> source:AsyncSeq<'T> -> Async<int>

/// Asynchronously returns the number of elements in the sequence for which the predicate returns true.
val lengthBy : predicate:('T -> bool) -> source:AsyncSeq<'T> -> Async<int64>

Expand Down Expand Up @@ -468,6 +473,25 @@ module AsyncSeq =
/// Raises ArgumentException if index is negative or greater than the sequence length. Mirrors Seq.insertAt.
val insertAt : index:int -> value:'T -> source:AsyncSeq<'T> -> AsyncSeq<'T>

/// Returns a new asynchronous sequence with the given values inserted before the element at the specified index.
/// An index equal to the length of the sequence appends the values at the end.
/// Raises ArgumentException if index is negative or greater than the sequence length. Mirrors Seq.insertManyAt.
val insertManyAt : index:int -> values:seq<'T> -> source:AsyncSeq<'T> -> AsyncSeq<'T>

/// Returns a new asynchronous sequence with 'count' elements removed starting at the specified index.
/// Raises ArgumentException if index or count is negative. Mirrors Seq.removeManyAt.
val removeManyAt : index:int -> count:int -> source:AsyncSeq<'T> -> AsyncSeq<'T>

/// Returns a new asynchronous sequence where each element is boxed to type obj.
val box : source:AsyncSeq<'T> -> AsyncSeq<obj>

/// Returns a new asynchronous sequence where each obj element is unboxed to type 'T.
val unbox<'T> : source:AsyncSeq<obj> -> AsyncSeq<'T>

/// Returns a new asynchronous sequence where each obj element is dynamically cast to type 'T.
/// Raises InvalidCastException if an element cannot be cast.
val cast<'T> : source:AsyncSeq<obj> -> AsyncSeq<'T>

/// 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
185 changes: 185 additions & 0 deletions tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3823,6 +3823,79 @@ let ``AsyncSeq.withCancellation with cancelled token raises OperationCanceledExc
|> ignore)
|> ignore

// ===== insertManyAt =====

[<Test>]
let ``AsyncSeq.insertManyAt inserts values at middle index`` () =
let result =
AsyncSeq.ofSeq [ 1; 4; 5 ]
|> AsyncSeq.insertManyAt 1 [ 2; 3 ]
|> AsyncSeq.toArrayAsync
|> Async.RunSynchronously
Assert.AreEqual([| 1; 2; 3; 4; 5 |], result)

[<Test>]
let ``AsyncSeq.insertManyAt inserts at index 0 (prepend)`` () =
let result =
AsyncSeq.ofSeq [ 3; 4 ]
|> AsyncSeq.insertManyAt 0 [ 1; 2 ]
|> AsyncSeq.toArrayAsync
|> Async.RunSynchronously
Assert.AreEqual([| 1; 2; 3; 4 |], result)

[<Test>]
let ``AsyncSeq.insertManyAt appends when index equals sequence length`` () =
let result =
AsyncSeq.ofSeq [ 1; 2 ]
|> AsyncSeq.insertManyAt 2 [ 3; 4 ]
|> AsyncSeq.toArrayAsync
|> Async.RunSynchronously
Assert.AreEqual([| 1; 2; 3; 4 |], result)

[<Test>]
let ``AsyncSeq.insertManyAt with empty values is identity`` () =
let result =
AsyncSeq.ofSeq [ 1; 2; 3 ]
|> AsyncSeq.insertManyAt 1 []
|> AsyncSeq.toArrayAsync
|> Async.RunSynchronously
Assert.AreEqual([| 1; 2; 3 |], result)

[<Test>]
let ``AsyncSeq.insertManyAt raises ArgumentException for negative index`` () =
Assert.Throws<System.ArgumentException>(fun () ->
AsyncSeq.ofSeq [ 1; 2 ]
|> AsyncSeq.insertManyAt -1 [ 0 ]
|> AsyncSeq.toArrayAsync
|> Async.RunSynchronously |> ignore)
|> ignore

// ===== removeManyAt =====

[<Test>]
let ``AsyncSeq.removeManyAt removes elements at middle index`` () =
let result =
AsyncSeq.ofSeq [ 1; 2; 3; 4; 5 ]
|> AsyncSeq.removeManyAt 1 3
|> AsyncSeq.toArrayAsync
|> Async.RunSynchronously
Assert.AreEqual([| 1; 5 |], result)

[<Test>]
let ``AsyncSeq.removeManyAt removes from start`` () =
let result =
AsyncSeq.ofSeq [ 1; 2; 3; 4 ]
|> AsyncSeq.removeManyAt 0 2
|> AsyncSeq.toArrayAsync
|> Async.RunSynchronously
Assert.AreEqual([| 3; 4 |], result)

[<Test>]
let ``AsyncSeq.removeManyAt count zero returns all elements`` () =
let result =
AsyncSeq.ofSeq [ 1; 2; 3 ]
|> AsyncSeq.removeManyAt 1 0

// ===== indexed =====

[<Test>]
Expand Down Expand Up @@ -3931,6 +4004,118 @@ let ``AsyncSeq.replicateUntilNoneAsync generates elements until None`` () =
|> Async.RunSynchronously
Assert.AreEqual([| 1; 2; 3 |], result)

[<Test>]
let ``AsyncSeq.removeManyAt count greater than remaining removes to end`` () =
let result =
AsyncSeq.ofSeq [ 1; 2; 3 ]
|> AsyncSeq.removeManyAt 1 10
|> AsyncSeq.toArrayAsync
|> Async.RunSynchronously
Assert.AreEqual([| 1 |], result)

[<Test>]
let ``AsyncSeq.removeManyAt raises ArgumentException for negative index`` () =
Assert.Throws<System.ArgumentException>(fun () ->
AsyncSeq.ofSeq [ 1; 2; 3 ]
|> AsyncSeq.removeManyAt -1 1
|> AsyncSeq.toArrayAsync
|> Async.RunSynchronously |> ignore)
|> ignore

[<Test>]
let ``AsyncSeq.removeManyAt raises ArgumentException for negative count`` () =
Assert.Throws<System.ArgumentException>(fun () ->
AsyncSeq.ofSeq [ 1; 2; 3 ]
|> AsyncSeq.removeManyAt 0 -1
|> AsyncSeq.toArrayAsync
|> Async.RunSynchronously |> ignore)
|> ignore

// ===== box / unbox / cast =====

[<Test>]
let ``AsyncSeq.box boxes each element to obj`` () =
let result =
AsyncSeq.ofSeq [ 1; 2; 3 ]
|> AsyncSeq.box
|> AsyncSeq.toArrayAsync
|> Async.RunSynchronously
Assert.AreEqual(3, result.Length)
Assert.AreEqual(box 1, result.[0])
Assert.AreEqual(box 2, result.[1])
Assert.AreEqual(box 3, result.[2])

[<Test>]
let ``AsyncSeq.unbox unboxes each element`` () =
let result =
AsyncSeq.ofSeq [ box 1; box 2; box 3 ]
|> AsyncSeq.unbox<int>
|> AsyncSeq.toArrayAsync
|> Async.RunSynchronously
Assert.AreEqual([| 1; 2; 3 |], result)

[<Test>]
let ``AsyncSeq.cast casts each element`` () =
let result =
AsyncSeq.ofSeq [ box 1; box 2; box 3 ]
|> AsyncSeq.cast<int>
|> AsyncSeq.toArrayAsync
|> Async.RunSynchronously
Assert.AreEqual([| 1; 2; 3 |], result)

[<Test>]
let ``AsyncSeq.box then unbox roundtrips`` () =
let original = [| 10; 20; 30 |]
let result =
AsyncSeq.ofSeq original
|> AsyncSeq.box
|> AsyncSeq.unbox<int>
|> AsyncSeq.toArrayAsync
|> Async.RunSynchronously
Assert.AreEqual(original, result)

// ===== lengthOrMax =====

[<Test>]
let ``AsyncSeq.lengthOrMax returns length when sequence is shorter than max`` () =
let result =
AsyncSeq.ofSeq [ 1; 2; 3 ]
|> AsyncSeq.lengthOrMax 10
|> Async.RunSynchronously
Assert.AreEqual(3, result)

[<Test>]
let ``AsyncSeq.lengthOrMax returns max when sequence is longer`` () =
let result =
AsyncSeq.ofSeq [ 1 .. 100 ]
|> AsyncSeq.lengthOrMax 5
|> Async.RunSynchronously
Assert.AreEqual(5, result)

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

[<Test>]
let ``AsyncSeq.lengthOrMax with max 0 returns 0`` () =
let result =
AsyncSeq.ofSeq [ 1; 2; 3 ]
|> AsyncSeq.lengthOrMax 0
|> Async.RunSynchronously
Assert.AreEqual(0, result)

[<Test>]
let ``AsyncSeq.lengthOrMax does not enumerate beyond max on infinite sequence`` () =
let result =
AsyncSeq.replicateInfinite 42
|> AsyncSeq.lengthOrMax 7
|> Async.RunSynchronously
Assert.AreEqual(7, result)

[<Test>]
let ``AsyncSeq.replicateUntilNoneAsync returns empty for immediate None`` () =
let result =
Expand Down