Skip to content

Commit b6ef08f

Browse files
Add AsyncSeq.transpose — mirror Seq.transpose for async sequences
Transposes rows and columns of an AsyncSeq<seq<'T>>, yielding each column as a 'T array. Buffers all rows before yielding columns. Raises InvalidOperationException if inner sequences have different lengths. - Signature in AsyncSeq.fsi - 6 tests covering basic transpose, empty, single row, single column, singleton, and jagged-input error case - RELEASE_NOTES.md: 4.9.0 entry 323/323 tests pass. Co-authored-by: Copilot <[email protected]>
1 parent 99617d8 commit b6ef08f

4 files changed

Lines changed: 88 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.9.0
2+
3+
* Added `AsyncSeq.transpose` — transposes an async sequence of sequences, yielding each column as an array; buffers all rows before yielding; mirrors `Seq.transpose`. Raises `InvalidOperationException` if inner sequences have different lengths.
4+
15
### 4.8.0
26

37
* Added `AsyncSeq.mapFoldAsync` — maps each element using an asynchronous folder that also threads an accumulator state, returning both the array of results and the final state; mirrors `Seq.mapFold`.

src/FSharp.Control.AsyncSeq/AsyncSeq.fs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2035,6 +2035,18 @@ module AsyncSeq =
20352035
let! arr = toArrayAsync source
20362036
for i in arr.Length - 1 .. -1 .. 0 do
20372037
yield arr.[i] }
2038+
2039+
/// Transposes the rows and columns of an async sequence of sequences.
2040+
/// Buffers the entire source sequence. Raises InvalidOperationException if inner sequences
2041+
/// have different lengths. Mirrors Seq.transpose.
2042+
let transpose (source: AsyncSeq<seq<'T>>) : AsyncSeq<'T[]> = asyncSeq {
2043+
let! rows = toListAsync (source |> map Seq.toArray)
2044+
if not rows.IsEmpty then
2045+
let firstLen = rows.Head.Length
2046+
if rows |> List.exists (fun row -> row.Length <> firstLen) then
2047+
invalidOp "The input sequences have different lengths."
2048+
for col in 0 .. firstLen - 1 do
2049+
yield rows |> List.map (fun row -> row.[col]) |> List.toArray }
20382050
#endif
20392051

20402052
#if !FABLE_COMPILER

src/FSharp.Control.AsyncSeq/AsyncSeq.fsi

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -673,6 +673,12 @@ module AsyncSeq =
673673
/// sequence is buffered before yielding any elements, mirroring Seq.rev.
674674
/// This function should not be used with large or infinite sequences.
675675
val rev : source:AsyncSeq<'T> -> AsyncSeq<'T>
676+
677+
/// Transposes the rows and columns of an async sequence of sequences, yielding each
678+
/// column as an array. The entire source sequence is buffered before any column is yielded,
679+
/// mirroring Seq.transpose. Raises InvalidOperationException if inner sequences have
680+
/// different lengths. This function should not be used with large or infinite sequences.
681+
val transpose : source:AsyncSeq<seq<'T>> -> AsyncSeq<'T[]>
676682
#endif
677683

678684
/// Interleaves two async sequences of the same type into a resulting sequence. The provided

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

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3662,3 +3662,69 @@ let ``AsyncSeq.insertAt raises ArgumentException when index exceeds length`` ()
36623662
|> AsyncSeq.toArrayAsync
36633663
|> Async.RunSynchronously |> ignore)
36643664
|> ignore
3665+
3666+
// ===== transpose =====
3667+
3668+
[<Test>]
3669+
let ``AsyncSeq.transpose basic 2x3 matrix`` () =
3670+
let source =
3671+
asyncSeq {
3672+
yield seq { yield 1; yield 2; yield 3 }
3673+
yield seq { yield 4; yield 5; yield 6 }
3674+
}
3675+
let result = AsyncSeq.transpose source |> AsyncSeq.toArrayAsync |> Async.RunSynchronously
3676+
Assert.AreEqual(3, result.Length)
3677+
Assert.AreEqual([| 1; 4 |], result.[0])
3678+
Assert.AreEqual([| 2; 5 |], result.[1])
3679+
Assert.AreEqual([| 3; 6 |], result.[2])
3680+
3681+
[<Test>]
3682+
let ``AsyncSeq.transpose empty outer sequence yields empty`` () =
3683+
let result =
3684+
AsyncSeq.empty<seq<int>>
3685+
|> AsyncSeq.transpose
3686+
|> AsyncSeq.toArrayAsync
3687+
|> Async.RunSynchronously
3688+
Assert.AreEqual([||], result)
3689+
3690+
[<Test>]
3691+
let ``AsyncSeq.transpose single row returns one column per element`` () =
3692+
let source = asyncSeq { yield seq { yield 1; yield 2; yield 3 } }
3693+
let result = AsyncSeq.transpose source |> AsyncSeq.toArrayAsync |> Async.RunSynchronously
3694+
Assert.AreEqual(3, result.Length)
3695+
Assert.AreEqual([| 1 |], result.[0])
3696+
Assert.AreEqual([| 2 |], result.[1])
3697+
Assert.AreEqual([| 3 |], result.[2])
3698+
3699+
[<Test>]
3700+
let ``AsyncSeq.transpose single column returns one row per element`` () =
3701+
let source =
3702+
asyncSeq {
3703+
yield seq { yield 1 }
3704+
yield seq { yield 2 }
3705+
yield seq { yield 3 }
3706+
}
3707+
let result = AsyncSeq.transpose source |> AsyncSeq.toArrayAsync |> Async.RunSynchronously
3708+
Assert.AreEqual(1, result.Length)
3709+
Assert.AreEqual([| 1; 2; 3 |], result.[0])
3710+
3711+
[<Test>]
3712+
let ``AsyncSeq.transpose of singleton rows yields one column`` () =
3713+
let source = asyncSeq { yield seq { yield 7 }; yield seq { yield 8 } }
3714+
let result = AsyncSeq.transpose source |> AsyncSeq.toArrayAsync |> Async.RunSynchronously
3715+
Assert.AreEqual(1, result.Length)
3716+
Assert.AreEqual([| 7; 8 |], result.[0])
3717+
3718+
[<Test>]
3719+
let ``AsyncSeq.transpose raises InvalidOperationException for jagged input`` () =
3720+
let source =
3721+
asyncSeq {
3722+
yield seq { yield 1; yield 2 }
3723+
yield seq { yield 3 }
3724+
}
3725+
Assert.Throws<System.InvalidOperationException>(fun () ->
3726+
AsyncSeq.transpose source
3727+
|> AsyncSeq.toArrayAsync
3728+
|> Async.RunSynchronously
3729+
|> ignore)
3730+
|> ignore

0 commit comments

Comments
 (0)