Skip to content

Commit 8124cd8

Browse files
Add AsyncSeq.insertManyAt, removeManyAt, splitInto
API parity with F# 6 collection modules (Seq/List/Array): - insertManyAt: insert multiple values before the element at index - removeManyAt: remove a range of elements starting at index - splitInto: split sequence into at most N equal-sized chunks 11 new tests covering normal cases, edge cases, and argument validation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 2e24353 commit 8124cd8

4 files changed

Lines changed: 244 additions & 0 deletions

File tree

RELEASE_NOTES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
### 4.16.0
22

33
* Performance: Replaced `ref` cells with `mutable` locals in the `ofSeq`, `tryWith`, and `tryFinally` enumerator state machines. Each call to `ofSeq` (or any async CE block using `try...with` / `try...finally` / `use`) previously heap-allocated a `Ref<T>` wrapper object per enumerator; it now uses a direct mutable field in the generated class, reducing GC pressure. The change is equivalent to the `mutable`-for-`ref` improvement introduced in 4.11.0 for other enumerators.
4+
* Added `AsyncSeq.insertManyAt` — inserts multiple values before the element at the specified index; mirrors `Seq.insertManyAt` / `List.insertManyAt`.
5+
* Added `AsyncSeq.removeManyAt` — removes a range of elements starting at the specified index; mirrors `Seq.removeManyAt` / `List.removeManyAt`.
6+
* Added `AsyncSeq.splitInto` — splits the sequence into at most N chunks of as-equal-as-possible size; mirrors `Seq.splitInto` / `Array.splitInto`.
47

58
### 4.15.0
69

src/FSharp.Control.AsyncSeq/AsyncSeq.fs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1731,6 +1731,27 @@ module AsyncSeq =
17311731
elif i < index then
17321732
invalidArg "index" "The index is outside the range of elements in the collection." }
17331733

1734+
let insertManyAt (index : int) (values : seq<'T>) (source : AsyncSeq<'T>) : AsyncSeq<'T> = asyncSeq {
1735+
if index < 0 then invalidArg "index" "must be non-negative"
1736+
let mutable i = 0
1737+
for x in source do
1738+
if i = index then yield! values
1739+
yield x
1740+
i <- i + 1
1741+
if i = index then yield! values
1742+
elif i < index then
1743+
invalidArg "index" "The index is outside the range of elements in the collection." }
1744+
1745+
let removeManyAt (index : int) (count : int) (source : AsyncSeq<'T>) : AsyncSeq<'T> = asyncSeq {
1746+
if index < 0 then invalidArg "index" "must be non-negative"
1747+
if count < 0 then invalidArg "count" "must be non-negative"
1748+
let mutable i = 0
1749+
for x in source do
1750+
if i < index || i >= index + count then yield x
1751+
i <- i + 1
1752+
if count > 0 && i < index + count then
1753+
invalidArg "index" "The index or count is outside the range of elements in the collection." }
1754+
17341755
#if !FABLE_COMPILER
17351756
let iterAsyncParallel (f:'a -> Async<unit>) (s:AsyncSeq<'a>) : Async<unit> = async {
17361757
use mb = MailboxProcessor.Start (ignore >> async.Return)
@@ -2215,6 +2236,22 @@ module AsyncSeq =
22152236
let toArraySynchronously (source:AsyncSeq<'T>) = toArrayAsync source |> Async.RunSynchronously
22162237
#endif
22172238

2239+
let splitInto (count : int) (source : AsyncSeq<'T>) : Async<'T[] array> = async {
2240+
if count < 1 then invalidArg "count" "must be positive"
2241+
let! arr = toArrayAsync source
2242+
let total = arr.Length
2243+
let result =
2244+
if total = 0 then [||]
2245+
else
2246+
let n = Operators.min count total
2247+
let minSize = total / n
2248+
let extras = total % n
2249+
Array.init n (fun i ->
2250+
let chunkStart = i * minSize + Operators.min i extras
2251+
let chunkSize = minSize + (if i < extras then 1 else 0)
2252+
Array.sub arr chunkStart chunkSize)
2253+
return result }
2254+
22182255
let cycle (source: AsyncSeq<'T>) : AsyncSeq<'T> =
22192256
asyncSeq {
22202257
let! arr = source |> toArrayAsync

src/FSharp.Control.AsyncSeq/AsyncSeq.fsi

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,20 @@ module AsyncSeq =
489489
/// Raises ArgumentException if index is negative or greater than the sequence length. Mirrors Seq.insertAt.
490490
val insertAt : index:int -> value:'T -> source:AsyncSeq<'T> -> AsyncSeq<'T>
491491

492+
/// Returns a new asynchronous sequence with the given values inserted before the element at the specified index.
493+
/// An index equal to the length of the sequence appends the values at the end.
494+
/// Raises ArgumentException if index is negative or greater than the sequence length. Mirrors Seq.insertManyAt.
495+
val insertManyAt : index:int -> values:seq<'T> -> source:AsyncSeq<'T> -> AsyncSeq<'T>
496+
497+
/// Returns a new asynchronous sequence with the given number of elements removed starting at the specified index.
498+
/// Raises ArgumentException if index is negative, count is negative, or index + count exceeds the sequence length. Mirrors Seq.removeManyAt.
499+
val removeManyAt : index:int -> count:int -> source:AsyncSeq<'T> -> AsyncSeq<'T>
500+
501+
/// Splits the input asynchronous sequence into at most <c>count</c> chunks of as-equal-as-possible size.
502+
/// The first (length mod count) chunks have one extra element. Materialises the source sequence into memory.
503+
/// Raises ArgumentException if count is not positive. Mirrors Seq.splitInto.
504+
val splitInto : count:int -> source:AsyncSeq<'T> -> Async<'T[] array>
505+
492506
/// Creates an asynchronous sequence that lazily takes element from an
493507
/// input synchronous sequence and returns them one-by-one.
494508
val ofSeq : source:seq<'T> -> AsyncSeq<'T>

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

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3914,6 +3914,196 @@ let ``AsyncSeq.insertAt raises ArgumentException when index exceeds length`` ()
39143914
|> Async.RunSynchronously |> ignore)
39153915
|> ignore
39163916

3917+
// ===== insertManyAt =====
3918+
3919+
[<Test>]
3920+
let ``AsyncSeq.insertManyAt inserts multiple elements at specified index`` () =
3921+
let result =
3922+
AsyncSeq.ofSeq [ 1; 2; 3 ]
3923+
|> AsyncSeq.insertManyAt 1 [ 10; 20 ]
3924+
|> AsyncSeq.toArrayAsync
3925+
|> Async.RunSynchronously
3926+
Assert.AreEqual([| 1; 10; 20; 2; 3 |], result)
3927+
3928+
[<Test>]
3929+
let ``AsyncSeq.insertManyAt prepends when index is 0`` () =
3930+
let result =
3931+
AsyncSeq.ofSeq [ 1; 2; 3 ]
3932+
|> AsyncSeq.insertManyAt 0 [ 10; 20 ]
3933+
|> AsyncSeq.toArrayAsync
3934+
|> Async.RunSynchronously
3935+
Assert.AreEqual([| 10; 20; 1; 2; 3 |], result)
3936+
3937+
[<Test>]
3938+
let ``AsyncSeq.insertManyAt appends when index equals sequence length`` () =
3939+
let result =
3940+
AsyncSeq.ofSeq [ 1; 2; 3 ]
3941+
|> AsyncSeq.insertManyAt 3 [ 10; 20 ]
3942+
|> AsyncSeq.toArrayAsync
3943+
|> Async.RunSynchronously
3944+
Assert.AreEqual([| 1; 2; 3; 10; 20 |], result)
3945+
3946+
[<Test>]
3947+
let ``AsyncSeq.insertManyAt with empty values returns original sequence`` () =
3948+
let result =
3949+
AsyncSeq.ofSeq [ 1; 2; 3 ]
3950+
|> AsyncSeq.insertManyAt 1 []
3951+
|> AsyncSeq.toArrayAsync
3952+
|> Async.RunSynchronously
3953+
Assert.AreEqual([| 1; 2; 3 |], result)
3954+
3955+
[<Test>]
3956+
let ``AsyncSeq.insertManyAt raises ArgumentException for negative index`` () =
3957+
Assert.Throws<System.ArgumentException>(fun () ->
3958+
AsyncSeq.ofSeq [ 1; 2; 3 ]
3959+
|> AsyncSeq.insertManyAt -1 [ 10 ]
3960+
|> AsyncSeq.toArrayAsync
3961+
|> Async.RunSynchronously |> ignore)
3962+
|> ignore
3963+
3964+
[<Test>]
3965+
let ``AsyncSeq.insertManyAt raises ArgumentException when index exceeds length`` () =
3966+
Assert.Throws<System.ArgumentException>(fun () ->
3967+
AsyncSeq.ofSeq [ 1; 2; 3 ]
3968+
|> AsyncSeq.insertManyAt 5 [ 10 ]
3969+
|> AsyncSeq.toArrayAsync
3970+
|> Async.RunSynchronously |> ignore)
3971+
|> ignore
3972+
3973+
// ===== removeManyAt =====
3974+
3975+
[<Test>]
3976+
let ``AsyncSeq.removeManyAt removes elements at specified index and count`` () =
3977+
let result =
3978+
AsyncSeq.ofSeq [ 0; 1; 2; 3; 4 ]
3979+
|> AsyncSeq.removeManyAt 1 2
3980+
|> AsyncSeq.toArrayAsync
3981+
|> Async.RunSynchronously
3982+
Assert.AreEqual([| 0; 3; 4 |], result)
3983+
3984+
[<Test>]
3985+
let ``AsyncSeq.removeManyAt removes from beginning`` () =
3986+
let result =
3987+
AsyncSeq.ofSeq [ 1; 2; 3; 4 ]
3988+
|> AsyncSeq.removeManyAt 0 2
3989+
|> AsyncSeq.toArrayAsync
3990+
|> Async.RunSynchronously
3991+
Assert.AreEqual([| 3; 4 |], result)
3992+
3993+
[<Test>]
3994+
let ``AsyncSeq.removeManyAt removes from end`` () =
3995+
let result =
3996+
AsyncSeq.ofSeq [ 1; 2; 3; 4 ]
3997+
|> AsyncSeq.removeManyAt 2 2
3998+
|> AsyncSeq.toArrayAsync
3999+
|> Async.RunSynchronously
4000+
Assert.AreEqual([| 1; 2 |], result)
4001+
4002+
[<Test>]
4003+
let ``AsyncSeq.removeManyAt with count 0 returns original sequence`` () =
4004+
let result =
4005+
AsyncSeq.ofSeq [ 1; 2; 3 ]
4006+
|> AsyncSeq.removeManyAt 1 0
4007+
|> AsyncSeq.toArrayAsync
4008+
|> Async.RunSynchronously
4009+
Assert.AreEqual([| 1; 2; 3 |], result)
4010+
4011+
[<Test>]
4012+
let ``AsyncSeq.removeManyAt removes all elements`` () =
4013+
let result =
4014+
AsyncSeq.ofSeq [ 1; 2; 3 ]
4015+
|> AsyncSeq.removeManyAt 0 3
4016+
|> AsyncSeq.toArrayAsync
4017+
|> Async.RunSynchronously
4018+
Assert.AreEqual([||], result)
4019+
4020+
[<Test>]
4021+
let ``AsyncSeq.removeManyAt raises ArgumentException for negative index`` () =
4022+
Assert.Throws<System.ArgumentException>(fun () ->
4023+
AsyncSeq.ofSeq [ 1; 2; 3 ]
4024+
|> AsyncSeq.removeManyAt -1 1
4025+
|> AsyncSeq.toArrayAsync
4026+
|> Async.RunSynchronously |> ignore)
4027+
|> ignore
4028+
4029+
[<Test>]
4030+
let ``AsyncSeq.removeManyAt raises ArgumentException for negative count`` () =
4031+
Assert.Throws<System.ArgumentException>(fun () ->
4032+
AsyncSeq.ofSeq [ 1; 2; 3 ]
4033+
|> AsyncSeq.removeManyAt 0 -1
4034+
|> AsyncSeq.toArrayAsync
4035+
|> Async.RunSynchronously |> ignore)
4036+
|> ignore
4037+
4038+
[<Test>]
4039+
let ``AsyncSeq.removeManyAt raises ArgumentException when range exceeds sequence`` () =
4040+
Assert.Throws<System.ArgumentException>(fun () ->
4041+
AsyncSeq.ofSeq [ 1; 2; 3 ]
4042+
|> AsyncSeq.removeManyAt 2 2
4043+
|> AsyncSeq.toArrayAsync
4044+
|> Async.RunSynchronously |> ignore)
4045+
|> ignore
4046+
4047+
// ===== splitInto =====
4048+
4049+
[<Test>]
4050+
let ``AsyncSeq.splitInto splits sequence into equal chunks`` () =
4051+
let result =
4052+
AsyncSeq.ofSeq [ 1; 2; 3; 4; 5; 6 ]
4053+
|> AsyncSeq.splitInto 3
4054+
|> Async.RunSynchronously
4055+
Assert.AreEqual(3, result.Length)
4056+
Assert.AreEqual([| 1; 2 |], result.[0])
4057+
Assert.AreEqual([| 3; 4 |], result.[1])
4058+
Assert.AreEqual([| 5; 6 |], result.[2])
4059+
4060+
[<Test>]
4061+
let ``AsyncSeq.splitInto distributes remainder to first chunks`` () =
4062+
let result =
4063+
AsyncSeq.ofSeq [ 1 .. 7 ]
4064+
|> AsyncSeq.splitInto 3
4065+
|> Async.RunSynchronously
4066+
Assert.AreEqual(3, result.Length)
4067+
Assert.AreEqual([| 1; 2; 3 |], result.[0])
4068+
Assert.AreEqual([| 4; 5 |], result.[1])
4069+
Assert.AreEqual([| 6; 7 |], result.[2])
4070+
4071+
[<Test>]
4072+
let ``AsyncSeq.splitInto with count 1 returns single chunk`` () =
4073+
let result =
4074+
AsyncSeq.ofSeq [ 1; 2; 3 ]
4075+
|> AsyncSeq.splitInto 1
4076+
|> Async.RunSynchronously
4077+
Assert.AreEqual(1, result.Length)
4078+
Assert.AreEqual([| 1; 2; 3 |], result.[0])
4079+
4080+
[<Test>]
4081+
let ``AsyncSeq.splitInto with count greater than length returns one chunk per element`` () =
4082+
let result =
4083+
AsyncSeq.ofSeq [ 1; 2; 3 ]
4084+
|> AsyncSeq.splitInto 10
4085+
|> Async.RunSynchronously
4086+
Assert.AreEqual(3, result.Length)
4087+
Assert.AreEqual([| 1 |], result.[0])
4088+
Assert.AreEqual([| 2 |], result.[1])
4089+
Assert.AreEqual([| 3 |], result.[2])
4090+
4091+
[<Test>]
4092+
let ``AsyncSeq.splitInto with empty sequence returns empty array`` () =
4093+
let result =
4094+
AsyncSeq.empty<int>
4095+
|> AsyncSeq.splitInto 3
4096+
|> Async.RunSynchronously
4097+
Assert.AreEqual([||], result)
4098+
4099+
[<Test>]
4100+
let ``AsyncSeq.splitInto raises ArgumentException when count is zero`` () =
4101+
Assert.Throws<System.ArgumentException>(fun () ->
4102+
AsyncSeq.ofSeq [ 1; 2; 3 ]
4103+
|> AsyncSeq.splitInto 0
4104+
|> Async.RunSynchronously |> ignore)
4105+
|> ignore
4106+
39174107
[<Test>]
39184108
let ``AsyncSeq.take more than length returns all elements`` () =
39194109
let result =

0 commit comments

Comments
 (0)