Skip to content

Commit 4d36a51

Browse files
Add AsyncSeq.splitAt — splits at index, returning first N elements as array and remainder as AsyncSeq
Mirrors Seq.splitAt. Source is enumerated once; remainder is produced lazily. 6 new tests; 293/293 total pass. Co-authored-by: Copilot <[email protected]>
1 parent 4a60ed7 commit 4d36a51

4 files changed

Lines changed: 82 additions & 0 deletions

File tree

RELEASE_NOTES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
### 4.7.0
2+
3+
* Added `AsyncSeq.splitAt` — splits a sequence at the given index, returning the first `count` elements as an array and the remaining elements as a new `AsyncSeq`. Mirrors `Seq.splitAt`. The source is enumerated once.
4+
15
### 4.6.0
26

37
* Added `AsyncSeq.isEmpty` — returns `true` if the sequence contains no elements; short-circuits after the first element, mirroring `Seq.isEmpty`.

src/FSharp.Control.AsyncSeq/AsyncSeq.fs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1815,6 +1815,35 @@ module AsyncSeq =
18151815

18161816
let tail (source : AsyncSeq<'T>) : AsyncSeq<'T> = skip 1 source
18171817

1818+
/// Splits an async sequence at the given index, returning the first `count` elements as an array
1819+
/// and the remaining elements as a new AsyncSeq. The source is enumerated once.
1820+
let splitAt (count: int) (source: AsyncSeq<'T>) : Async<'T array * AsyncSeq<'T>> = async {
1821+
if count < 0 then invalidArg "count" "must be non-negative"
1822+
let ie = source.GetEnumerator()
1823+
let ra = ResizeArray<'T>()
1824+
let! m = ie.MoveNext()
1825+
let b = ref m
1826+
while b.Value.IsSome && ra.Count < count do
1827+
ra.Add b.Value.Value
1828+
let! next = ie.MoveNext()
1829+
b := next
1830+
let first = ra.ToArray()
1831+
let rest =
1832+
if b.Value.IsNone then
1833+
ie.Dispose()
1834+
empty<'T>
1835+
else
1836+
let cur = ref b.Value
1837+
asyncSeq {
1838+
try
1839+
while cur.Value.IsSome do
1840+
yield cur.Value.Value
1841+
let! next = ie.MoveNext()
1842+
cur := next
1843+
finally
1844+
ie.Dispose() }
1845+
return first, rest }
1846+
18181847
let toArrayAsync (source : AsyncSeq<'T>) : Async<'T[]> = async {
18191848
let ra = (new ResizeArray<_>())
18201849
use ie = source.GetEnumerator()

src/FSharp.Control.AsyncSeq/AsyncSeq.fsi

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,11 @@ module AsyncSeq =
586586
/// Returns an empty sequence if the source is empty.
587587
val tail : source:AsyncSeq<'T> -> AsyncSeq<'T>
588588

589+
/// Splits an async sequence at the given index. Returns an async computation that yields
590+
/// the first `count` elements as an array and the remaining elements as a new AsyncSeq.
591+
/// The source is enumerated once; the returned AsyncSeq lazily produces the remainder.
592+
val splitAt : count:int -> source:AsyncSeq<'T> -> Async<'T array * AsyncSeq<'T>>
593+
589594
/// Creates an async computation which iterates the AsyncSeq and collects the output into an array.
590595
val toArrayAsync : source:AsyncSeq<'T> -> Async<'T []>
591596

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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3408,3 +3408,47 @@ let ``AsyncSeq.sortWith sorts descending with negated comparer`` () =
34083408
let ``AsyncSeq.sortWith returns empty array for empty sequence`` () =
34093409
let result = AsyncSeq.sortWith compare AsyncSeq.empty<int>
34103410
Assert.AreEqual([||], result)
3411+
3412+
[<Test>]
3413+
let ``AsyncSeq.splitAt splits a sequence at the given index`` () =
3414+
let source = asyncSeq { yield 1; yield 2; yield 3; yield 4; yield 5 }
3415+
let first, rest = AsyncSeq.splitAt 3 source |> Async.RunSynchronously
3416+
let restArr = rest |> AsyncSeq.toArrayAsync |> Async.RunSynchronously
3417+
Assert.AreEqual([| 1; 2; 3 |], first)
3418+
Assert.AreEqual([| 4; 5 |], restArr)
3419+
3420+
[<Test>]
3421+
let ``AsyncSeq.splitAt with count=0 returns empty array and full rest`` () =
3422+
let source = asyncSeq { yield 10; yield 20 }
3423+
let first, rest = AsyncSeq.splitAt 0 source |> Async.RunSynchronously
3424+
let restArr = rest |> AsyncSeq.toArrayAsync |> Async.RunSynchronously
3425+
Assert.AreEqual([||], first)
3426+
Assert.AreEqual([| 10; 20 |], restArr)
3427+
3428+
[<Test>]
3429+
let ``AsyncSeq.splitAt with count >= length returns all elements in first and empty rest`` () =
3430+
let source = asyncSeq { yield 1; yield 2; yield 3 }
3431+
let first, rest = AsyncSeq.splitAt 10 source |> Async.RunSynchronously
3432+
let restArr = rest |> AsyncSeq.toArrayAsync |> Async.RunSynchronously
3433+
Assert.AreEqual([| 1; 2; 3 |], first)
3434+
Assert.AreEqual([||], restArr)
3435+
3436+
[<Test>]
3437+
let ``AsyncSeq.splitAt on empty sequence returns empty first and empty rest`` () =
3438+
let first, rest = AsyncSeq.splitAt 3 AsyncSeq.empty<int> |> Async.RunSynchronously
3439+
let restArr = rest |> AsyncSeq.toArrayAsync |> Async.RunSynchronously
3440+
Assert.AreEqual([||], first)
3441+
Assert.AreEqual([||], restArr)
3442+
3443+
[<Test>]
3444+
let ``AsyncSeq.splitAt with count equal to length returns all in first and empty rest`` () =
3445+
let source = asyncSeq { yield 7; yield 8; yield 9 }
3446+
let first, rest = AsyncSeq.splitAt 3 source |> Async.RunSynchronously
3447+
let restArr = rest |> AsyncSeq.toArrayAsync |> Async.RunSynchronously
3448+
Assert.AreEqual([| 7; 8; 9 |], first)
3449+
Assert.AreEqual([||], restArr)
3450+
3451+
[<Test>]
3452+
let ``AsyncSeq.splitAt with negative count throws ArgumentException`` () =
3453+
Assert.Throws<System.ArgumentException>(fun () ->
3454+
AsyncSeq.splitAt -1 AsyncSeq.empty<int> |> Async.RunSynchronously |> ignore) |> ignore

0 commit comments

Comments
 (0)