From 0f99b96aeecf7894f60f674b76c2951404f01655 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 11 Mar 2026 00:12:49 +0000 Subject: [PATCH 1/2] =?UTF-8?q?Add=20AsyncSeq.transpose=20=E2=80=94=20mirr?= =?UTF-8?q?ors=20Seq.transpose?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Transposes a sequence of sequences: each element of the result is an array of the i-th elements of all inner sequences. All inner sequences must have the same length; the entire source is buffered before yielding. Available on all targets except Fable. Closes no specific issue; mirrors Seq.transpose. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- RELEASE_NOTES.md | 4 ++ src/FSharp.Control.AsyncSeq/AsyncSeq.fs | 11 +++++ src/FSharp.Control.AsyncSeq/AsyncSeq.fsi | 6 +++ .../AsyncSeqTests.fs | 41 +++++++++++++++++++ 4 files changed, 62 insertions(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index c88d2e5..9ad3cad 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,3 +1,7 @@ +### 4.9.0 + +* Added `AsyncSeq.transpose` — transposes a sequence of sequences so each element of the result is an array of the i-th elements of all inner sequences; all inner sequences must have the same length and the entire source is buffered, mirroring `Seq.transpose`. + ### 4.8.0 * 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`. diff --git a/src/FSharp.Control.AsyncSeq/AsyncSeq.fs b/src/FSharp.Control.AsyncSeq/AsyncSeq.fs index bf536f4..69e98ba 100644 --- a/src/FSharp.Control.AsyncSeq/AsyncSeq.fs +++ b/src/FSharp.Control.AsyncSeq/AsyncSeq.fs @@ -2035,6 +2035,17 @@ module AsyncSeq = let! arr = toArrayAsync source for i in arr.Length - 1 .. -1 .. 0 do yield arr.[i] } + + let transpose (source: AsyncSeq<#seq<'T>>) : AsyncSeq<'T[]> = asyncSeq { + let! rows = toArrayAsync source + if rows.Length > 0 then + let rowArrays = rows |> Array.map (fun r -> (r :> seq<'T>) |> Seq.toArray) + let colCount = rowArrays.[0].Length + for row in rowArrays do + if row.Length <> colCount then + invalidArg "source" "All inner sequences must have the same length." + for c in 0 .. colCount - 1 do + yield [| for row in rowArrays -> row.[c] |] } #endif #if !FABLE_COMPILER diff --git a/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi b/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi index 5113dd6..248da0b 100644 --- a/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi +++ b/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi @@ -673,6 +673,12 @@ module AsyncSeq = /// sequence is buffered before yielding any elements, mirroring Seq.rev. /// This function should not be used with large or infinite sequences. val rev : source:AsyncSeq<'T> -> AsyncSeq<'T> + + /// Transposes a sequence of sequences: each element of the result is an array + /// of the i-th elements of all inner sequences, mirroring Seq.transpose. + /// All inner sequences must have the same length; the entire source is buffered. + /// This function should not be used with large or infinite sequences. + val transpose : source:AsyncSeq<#seq<'T>> -> AsyncSeq<'T[]> #endif /// Interleaves two async sequences of the same type into a resulting sequence. The provided diff --git a/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs b/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs index 17277e6..9ace089 100644 --- a/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs +++ b/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs @@ -3662,3 +3662,44 @@ let ``AsyncSeq.insertAt raises ArgumentException when index exceeds length`` () |> AsyncSeq.toArrayAsync |> Async.RunSynchronously |> ignore) |> ignore + +// ── AsyncSeq.transpose ──────────────────────────────────────────────────────── + +[] +let ``AsyncSeq.transpose transposes rows and columns`` () = + let source = asyncSeq { yield [1; 2; 3]; yield [4; 5; 6] } + let result = AsyncSeq.transpose source |> AsyncSeq.toArrayAsync |> Async.RunSynchronously + Assert.AreEqual([| [|1;4|]; [|2;5|]; [|3;6|] |], result) + +[] +let ``AsyncSeq.transpose single row`` () = + let source = asyncSeq { yield [10; 20; 30] } + let result = AsyncSeq.transpose source |> AsyncSeq.toArrayAsync |> Async.RunSynchronously + Assert.AreEqual([| [|10|]; [|20|]; [|30|] |], result) + +[] +let ``AsyncSeq.transpose empty source returns empty`` () = + let result = AsyncSeq.transpose AsyncSeq.empty |> AsyncSeq.toArrayAsync |> Async.RunSynchronously + Assert.AreEqual([||], result) + +[] +let ``AsyncSeq.transpose single column`` () = + let source = asyncSeq { yield [1]; yield [2]; yield [3] } + let result = AsyncSeq.transpose source |> AsyncSeq.toArrayAsync |> Async.RunSynchronously + Assert.AreEqual([| [|1;2;3|] |], result) + +[] +let ``AsyncSeq.transpose square matrix`` () = + let source = asyncSeq { yield [1; 2]; yield [3; 4] } + let result = AsyncSeq.transpose source |> AsyncSeq.toArrayAsync |> Async.RunSynchronously + Assert.AreEqual([| [|1;3|]; [|2;4|] |], result) + +[] +let ``AsyncSeq.transpose raises on mismatched row lengths`` () = + Assert.Throws(fun () -> + asyncSeq { yield [1; 2]; yield [3] } + |> AsyncSeq.transpose + |> AsyncSeq.toArrayAsync + |> Async.RunSynchronously + |> ignore) + |> ignore From 9d099a0977d4600735d38ad53a33186918cf01fa Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 11 Mar 2026 00:18:31 +0000 Subject: [PATCH 2/2] ci: trigger checks