Skip to content

Commit 3f4372e

Browse files
Design parity with TaskSeq, batch 3: insertManyAt, removeManyAt, box, unbox, cast, lengthOrMax (#277)
Co-authored-by: Copilot <[email protected]>
1 parent 3249128 commit 3f4372e

5 files changed

Lines changed: 258 additions & 1 deletion

File tree

RELEASE_NOTES.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
### 4.11.0
2+
3+
* Added `AsyncSeq.insertManyAt` — inserts multiple values before the element at the given index. Mirrors `Seq.insertManyAt` and `TaskSeq.insertManyAt`.
4+
* Added `AsyncSeq.removeManyAt` — removes a run of elements starting at the given index. Mirrors `Seq.removeManyAt` and `TaskSeq.removeManyAt`.
5+
* Added `AsyncSeq.box` — boxes each element to `obj`. Mirrors `TaskSeq.box`.
6+
* Added `AsyncSeq.unbox<'T>` — unboxes each `obj` element to `'T`. Mirrors `TaskSeq.unbox`.
7+
* Added `AsyncSeq.cast<'T>` — dynamically casts each `obj` element to `'T`. Mirrors `TaskSeq.cast`.
8+
* Added `AsyncSeq.lengthOrMax` — counts elements up to a maximum, avoiding full enumeration of long or infinite sequences. Mirrors `TaskSeq.lengthOrMax`.
9+
* Note: `AsyncSeq.except` already accepts `seq<'T>` for the excluded collection, so no separate `exceptOfSeq` is needed.
10+
* Part of ongoing design-parity work with FSharp.Control.TaskSeq (see #277).
11+
112
### 4.10.0
213

314
* Added `AsyncSeq.withCancellation` — returns a new `AsyncSeq` that passes the given `CancellationToken` to `GetAsyncEnumerator`, overriding whatever token would otherwise be supplied. Mirrors `TaskSeq.withCancellation` and is useful when consuming sequences from libraries (e.g. Entity Framework) that accept a cancellation token through `GetAsyncEnumerator`. Part of ongoing design-parity work with FSharp.Control.TaskSeq (see #277).

src/FSharp.Control.AsyncSeq/AsyncSeq.fs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1528,6 +1528,34 @@ module AsyncSeq =
15281528
elif i.Value < index then
15291529
invalidArg "index" "The index is outside the range of elements in the collection." }
15301530

1531+
let insertManyAt (index : int) (values : seq<'T>) (source : AsyncSeq<'T>) : AsyncSeq<'T> = asyncSeq {
1532+
if index < 0 then invalidArg "index" "must be non-negative"
1533+
let i = ref 0
1534+
for x in source do
1535+
if i.Value = index then yield! ofSeq values
1536+
yield x
1537+
i := i.Value + 1
1538+
if i.Value = index then yield! ofSeq values
1539+
elif i.Value < index then
1540+
invalidArg "index" "The index is outside the range of elements in the collection." }
1541+
1542+
let removeManyAt (index : int) (count : int) (source : AsyncSeq<'T>) : AsyncSeq<'T> = asyncSeq {
1543+
if index < 0 then invalidArg "index" "must be non-negative"
1544+
if count < 0 then invalidArg "count" "must be non-negative"
1545+
let i = ref 0
1546+
for x in source do
1547+
if i.Value < index || i.Value >= index + count then yield x
1548+
i := i.Value + 1 }
1549+
1550+
let box (source : AsyncSeq<'T>) : AsyncSeq<obj> =
1551+
map Microsoft.FSharp.Core.Operators.box source
1552+
1553+
let unbox<'T> (source : AsyncSeq<obj>) : AsyncSeq<'T> =
1554+
map Microsoft.FSharp.Core.Operators.unbox source
1555+
1556+
let cast<'T> (source : AsyncSeq<obj>) : AsyncSeq<'T> =
1557+
map Microsoft.FSharp.Core.Operators.unbox source
1558+
15311559
#if !FABLE_COMPILER
15321560
let iterAsyncParallel (f:'a -> Async<unit>) (s:AsyncSeq<'a>) : Async<unit> = async {
15331561
use mb = MailboxProcessor.Start (ignore >> async.Return)
@@ -1914,6 +1942,12 @@ module AsyncSeq =
19141942

19151943
let truncate count source = take count source
19161944

1945+
let lengthOrMax (max : int) (source : AsyncSeq<'T>) : Async<int> =
1946+
async {
1947+
let! n = source |> take max |> length
1948+
return int n
1949+
}
1950+
19171951
let skip count (source : AsyncSeq<'T>) : AsyncSeq<_> = asyncSeq {
19181952
if (count < 0) then invalidArg "count" "must be non-negative"
19191953
use ie = source.GetEnumerator()

src/FSharp.Control.AsyncSeq/AsyncSeq.fsi

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,11 @@ module AsyncSeq =
395395
/// Asynchronously determine the number of elements in the sequence
396396
val length : source:AsyncSeq<'T> -> Async<int64>
397397

398+
/// Asynchronously counts elements up to a maximum. Returns the actual count if the sequence has
399+
/// fewer than 'max' elements, otherwise returns 'max'. Avoids full enumeration of long sequences.
400+
/// Mirrors TaskSeq.lengthOrMax.
401+
val lengthOrMax : max:int -> source:AsyncSeq<'T> -> Async<int>
402+
398403
/// Same as AsyncSeq.scanAsync, but the specified function is synchronous.
399404
val scan : folder:('State -> 'T -> 'State) -> state:'State -> source:AsyncSeq<'T> -> AsyncSeq<'State>
400405

@@ -431,6 +436,25 @@ module AsyncSeq =
431436
/// Raises ArgumentException if index is negative or greater than the sequence length. Mirrors Seq.insertAt.
432437
val insertAt : index:int -> value:'T -> source:AsyncSeq<'T> -> AsyncSeq<'T>
433438

439+
/// Returns a new asynchronous sequence with the given values inserted before the element at the specified index.
440+
/// An index equal to the length of the sequence appends the values at the end.
441+
/// Raises ArgumentException if index is negative or greater than the sequence length. Mirrors Seq.insertManyAt.
442+
val insertManyAt : index:int -> values:seq<'T> -> source:AsyncSeq<'T> -> AsyncSeq<'T>
443+
444+
/// Returns a new asynchronous sequence with 'count' elements removed starting at the specified index.
445+
/// Raises ArgumentException if index or count is negative. Mirrors Seq.removeManyAt.
446+
val removeManyAt : index:int -> count:int -> source:AsyncSeq<'T> -> AsyncSeq<'T>
447+
448+
/// Returns a new asynchronous sequence where each element is boxed to type obj.
449+
val box : source:AsyncSeq<'T> -> AsyncSeq<obj>
450+
451+
/// Returns a new asynchronous sequence where each obj element is unboxed to type 'T.
452+
val unbox<'T> : source:AsyncSeq<obj> -> AsyncSeq<'T>
453+
454+
/// Returns a new asynchronous sequence where each obj element is dynamically cast to type 'T.
455+
/// Raises InvalidCastException if an element cannot be cast.
456+
val cast<'T> : source:AsyncSeq<obj> -> AsyncSeq<'T>
457+
434458
/// Creates an asynchronous sequence that lazily takes element from an
435459
/// input synchronous sequence and returns them one-by-one.
436460
val ofSeq : source:seq<'T> -> AsyncSeq<'T>

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

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3719,3 +3719,191 @@ let ``AsyncSeq.withCancellation with cancelled token raises OperationCanceledExc
37193719
|> Async.RunSynchronously
37203720
|> ignore)
37213721
|> ignore
3722+
3723+
// ===== insertManyAt =====
3724+
3725+
[<Test>]
3726+
let ``AsyncSeq.insertManyAt inserts values at middle index`` () =
3727+
let result =
3728+
AsyncSeq.ofSeq [ 1; 4; 5 ]
3729+
|> AsyncSeq.insertManyAt 1 [ 2; 3 ]
3730+
|> AsyncSeq.toArrayAsync
3731+
|> Async.RunSynchronously
3732+
Assert.AreEqual([| 1; 2; 3; 4; 5 |], result)
3733+
3734+
[<Test>]
3735+
let ``AsyncSeq.insertManyAt inserts at index 0 (prepend)`` () =
3736+
let result =
3737+
AsyncSeq.ofSeq [ 3; 4 ]
3738+
|> AsyncSeq.insertManyAt 0 [ 1; 2 ]
3739+
|> AsyncSeq.toArrayAsync
3740+
|> Async.RunSynchronously
3741+
Assert.AreEqual([| 1; 2; 3; 4 |], result)
3742+
3743+
[<Test>]
3744+
let ``AsyncSeq.insertManyAt appends when index equals sequence length`` () =
3745+
let result =
3746+
AsyncSeq.ofSeq [ 1; 2 ]
3747+
|> AsyncSeq.insertManyAt 2 [ 3; 4 ]
3748+
|> AsyncSeq.toArrayAsync
3749+
|> Async.RunSynchronously
3750+
Assert.AreEqual([| 1; 2; 3; 4 |], result)
3751+
3752+
[<Test>]
3753+
let ``AsyncSeq.insertManyAt with empty values is identity`` () =
3754+
let result =
3755+
AsyncSeq.ofSeq [ 1; 2; 3 ]
3756+
|> AsyncSeq.insertManyAt 1 []
3757+
|> AsyncSeq.toArrayAsync
3758+
|> Async.RunSynchronously
3759+
Assert.AreEqual([| 1; 2; 3 |], result)
3760+
3761+
[<Test>]
3762+
let ``AsyncSeq.insertManyAt raises ArgumentException for negative index`` () =
3763+
Assert.Throws<System.ArgumentException>(fun () ->
3764+
AsyncSeq.ofSeq [ 1; 2 ]
3765+
|> AsyncSeq.insertManyAt -1 [ 0 ]
3766+
|> AsyncSeq.toArrayAsync
3767+
|> Async.RunSynchronously |> ignore)
3768+
|> ignore
3769+
3770+
// ===== removeManyAt =====
3771+
3772+
[<Test>]
3773+
let ``AsyncSeq.removeManyAt removes elements at middle index`` () =
3774+
let result =
3775+
AsyncSeq.ofSeq [ 1; 2; 3; 4; 5 ]
3776+
|> AsyncSeq.removeManyAt 1 3
3777+
|> AsyncSeq.toArrayAsync
3778+
|> Async.RunSynchronously
3779+
Assert.AreEqual([| 1; 5 |], result)
3780+
3781+
[<Test>]
3782+
let ``AsyncSeq.removeManyAt removes from start`` () =
3783+
let result =
3784+
AsyncSeq.ofSeq [ 1; 2; 3; 4 ]
3785+
|> AsyncSeq.removeManyAt 0 2
3786+
|> AsyncSeq.toArrayAsync
3787+
|> Async.RunSynchronously
3788+
Assert.AreEqual([| 3; 4 |], result)
3789+
3790+
[<Test>]
3791+
let ``AsyncSeq.removeManyAt count zero returns all elements`` () =
3792+
let result =
3793+
AsyncSeq.ofSeq [ 1; 2; 3 ]
3794+
|> AsyncSeq.removeManyAt 1 0
3795+
|> AsyncSeq.toArrayAsync
3796+
|> Async.RunSynchronously
3797+
Assert.AreEqual([| 1; 2; 3 |], result)
3798+
3799+
[<Test>]
3800+
let ``AsyncSeq.removeManyAt count greater than remaining removes to end`` () =
3801+
let result =
3802+
AsyncSeq.ofSeq [ 1; 2; 3 ]
3803+
|> AsyncSeq.removeManyAt 1 10
3804+
|> AsyncSeq.toArrayAsync
3805+
|> Async.RunSynchronously
3806+
Assert.AreEqual([| 1 |], result)
3807+
3808+
[<Test>]
3809+
let ``AsyncSeq.removeManyAt raises ArgumentException for negative index`` () =
3810+
Assert.Throws<System.ArgumentException>(fun () ->
3811+
AsyncSeq.ofSeq [ 1; 2; 3 ]
3812+
|> AsyncSeq.removeManyAt -1 1
3813+
|> AsyncSeq.toArrayAsync
3814+
|> Async.RunSynchronously |> ignore)
3815+
|> ignore
3816+
3817+
[<Test>]
3818+
let ``AsyncSeq.removeManyAt raises ArgumentException for negative count`` () =
3819+
Assert.Throws<System.ArgumentException>(fun () ->
3820+
AsyncSeq.ofSeq [ 1; 2; 3 ]
3821+
|> AsyncSeq.removeManyAt 0 -1
3822+
|> AsyncSeq.toArrayAsync
3823+
|> Async.RunSynchronously |> ignore)
3824+
|> ignore
3825+
3826+
// ===== box / unbox / cast =====
3827+
3828+
[<Test>]
3829+
let ``AsyncSeq.box boxes each element to obj`` () =
3830+
let result =
3831+
AsyncSeq.ofSeq [ 1; 2; 3 ]
3832+
|> AsyncSeq.box
3833+
|> AsyncSeq.toArrayAsync
3834+
|> Async.RunSynchronously
3835+
Assert.AreEqual(3, result.Length)
3836+
Assert.AreEqual(box 1, result.[0])
3837+
Assert.AreEqual(box 2, result.[1])
3838+
Assert.AreEqual(box 3, result.[2])
3839+
3840+
[<Test>]
3841+
let ``AsyncSeq.unbox unboxes each element`` () =
3842+
let result =
3843+
AsyncSeq.ofSeq [ box 1; box 2; box 3 ]
3844+
|> AsyncSeq.unbox<int>
3845+
|> AsyncSeq.toArrayAsync
3846+
|> Async.RunSynchronously
3847+
Assert.AreEqual([| 1; 2; 3 |], result)
3848+
3849+
[<Test>]
3850+
let ``AsyncSeq.cast casts each element`` () =
3851+
let result =
3852+
AsyncSeq.ofSeq [ box 1; box 2; box 3 ]
3853+
|> AsyncSeq.cast<int>
3854+
|> AsyncSeq.toArrayAsync
3855+
|> Async.RunSynchronously
3856+
Assert.AreEqual([| 1; 2; 3 |], result)
3857+
3858+
[<Test>]
3859+
let ``AsyncSeq.box then unbox roundtrips`` () =
3860+
let original = [| 10; 20; 30 |]
3861+
let result =
3862+
AsyncSeq.ofSeq original
3863+
|> AsyncSeq.box
3864+
|> AsyncSeq.unbox<int>
3865+
|> AsyncSeq.toArrayAsync
3866+
|> Async.RunSynchronously
3867+
Assert.AreEqual(original, result)
3868+
3869+
// ===== lengthOrMax =====
3870+
3871+
[<Test>]
3872+
let ``AsyncSeq.lengthOrMax returns length when sequence is shorter than max`` () =
3873+
let result =
3874+
AsyncSeq.ofSeq [ 1; 2; 3 ]
3875+
|> AsyncSeq.lengthOrMax 10
3876+
|> Async.RunSynchronously
3877+
Assert.AreEqual(3, result)
3878+
3879+
[<Test>]
3880+
let ``AsyncSeq.lengthOrMax returns max when sequence is longer`` () =
3881+
let result =
3882+
AsyncSeq.ofSeq [ 1 .. 100 ]
3883+
|> AsyncSeq.lengthOrMax 5
3884+
|> Async.RunSynchronously
3885+
Assert.AreEqual(5, result)
3886+
3887+
[<Test>]
3888+
let ``AsyncSeq.lengthOrMax returns 0 for empty sequence`` () =
3889+
let result =
3890+
AsyncSeq.empty<int>
3891+
|> AsyncSeq.lengthOrMax 5
3892+
|> Async.RunSynchronously
3893+
Assert.AreEqual(0, result)
3894+
3895+
[<Test>]
3896+
let ``AsyncSeq.lengthOrMax with max 0 returns 0`` () =
3897+
let result =
3898+
AsyncSeq.ofSeq [ 1; 2; 3 ]
3899+
|> AsyncSeq.lengthOrMax 0
3900+
|> Async.RunSynchronously
3901+
Assert.AreEqual(0, result)
3902+
3903+
[<Test>]
3904+
let ``AsyncSeq.lengthOrMax does not enumerate beyond max on infinite sequence`` () =
3905+
let result =
3906+
AsyncSeq.replicateInfinite 42
3907+
|> AsyncSeq.lengthOrMax 7
3908+
|> Async.RunSynchronously
3909+
Assert.AreEqual(7, result)

version.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<Project>
22
<PropertyGroup>
3-
<Version>4.10.0</Version>
3+
<Version>4.11.0</Version>
44
</PropertyGroup>
55
</Project>

0 commit comments

Comments
 (0)