Skip to content

Commit 4b05e9c

Browse files
Add AsyncSeq.unzip, unzip3, map2, map3 for API parity with F# collections
- unzip: splits AsyncSeq<'T1 * 'T2> into Async<'T1[] * 'T2[]>, mirrors List.unzip - unzip3: splits AsyncSeq<'T1 * 'T2 * 'T3> into Async<'T1[] * 'T2[] * 'T3[]>, mirrors List.unzip3 - map2: applies function pairwise over two async sequences, mirrors Seq.map2 (thin wrapper over zipWith) - map3: applies function over three async sequences, mirrors List.map3 (thin wrapper over zipWith3) 9 new tests; 431/431 pass Co-authored-by: Copilot <[email protected]>
1 parent 2e24353 commit 4b05e9c

4 files changed

Lines changed: 129 additions & 1 deletion

File tree

RELEASE_NOTES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
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.unzip` — splits an async sequence of pairs into two arrays. Mirrors `List.unzip`.
5+
* Added `AsyncSeq.unzip3` — splits an async sequence of triples into three arrays. Mirrors `List.unzip3`.
6+
* Added `AsyncSeq.map2` — applies a function to corresponding elements of two async sequences; stops when either is exhausted. Mirrors `Seq.map2`.
7+
* Added `AsyncSeq.map3` — applies a function to corresponding elements of three async sequences; stops when any is exhausted. Mirrors `List.map3`.
48

59
### 4.15.0
610

src/FSharp.Control.AsyncSeq/AsyncSeq.fs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1994,6 +1994,42 @@ module AsyncSeq =
19941994
let! next1 = ie1.MoveNext()
19951995
b1 <- next1 }
19961996

1997+
let unzip (source: AsyncSeq<'T1 * 'T2>) : Async<'T1[] * 'T2[]> = async {
1998+
let as1 = System.Collections.Generic.List<'T1>()
1999+
let as2 = System.Collections.Generic.List<'T2>()
2000+
use ie = source.GetEnumerator()
2001+
let! move = ie.MoveNext()
2002+
let mutable cur = move
2003+
while cur.IsSome do
2004+
let (a, b) = cur.Value
2005+
as1.Add(a)
2006+
as2.Add(b)
2007+
let! next = ie.MoveNext()
2008+
cur <- next
2009+
return (as1.ToArray(), as2.ToArray()) }
2010+
2011+
let unzip3 (source: AsyncSeq<'T1 * 'T2 * 'T3>) : Async<'T1[] * 'T2[] * 'T3[]> = async {
2012+
let as1 = System.Collections.Generic.List<'T1>()
2013+
let as2 = System.Collections.Generic.List<'T2>()
2014+
let as3 = System.Collections.Generic.List<'T3>()
2015+
use ie = source.GetEnumerator()
2016+
let! move = ie.MoveNext()
2017+
let mutable cur = move
2018+
while cur.IsSome do
2019+
let (a, b, c) = cur.Value
2020+
as1.Add(a)
2021+
as2.Add(b)
2022+
as3.Add(c)
2023+
let! next = ie.MoveNext()
2024+
cur <- next
2025+
return (as1.ToArray(), as2.ToArray(), as3.ToArray()) }
2026+
2027+
let map2 (mapping: 'T1 -> 'T2 -> 'U) (source1: AsyncSeq<'T1>) (source2: AsyncSeq<'T2>) : AsyncSeq<'U> =
2028+
zipWith mapping source1 source2
2029+
2030+
let map3 (mapping: 'T1 -> 'T2 -> 'T3 -> 'U) (source1: AsyncSeq<'T1>) (source2: AsyncSeq<'T2>) (source3: AsyncSeq<'T3>) : AsyncSeq<'U> =
2031+
zipWith3 mapping source1 source2 source3
2032+
19972033
let zappAsync (fs:AsyncSeq<'T -> Async<'U>>) (s:AsyncSeq<'T>) : AsyncSeq<'U> =
19982034
zipWithAsync (|>) s fs
19992035

src/FSharp.Control.AsyncSeq/AsyncSeq.fsi

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,22 @@ module AsyncSeq =
576576
/// The second sequence is fully buffered before iteration begins, mirroring Seq.allPairs.
577577
val allPairs : source1:AsyncSeq<'T1> -> source2:AsyncSeq<'T2> -> AsyncSeq<'T1 * 'T2>
578578

579+
/// Splits an async sequence of pairs into two arrays. Mirrors List.unzip.
580+
val unzip : source:AsyncSeq<'T1 * 'T2> -> Async<'T1[] * 'T2[]>
581+
582+
/// Splits an async sequence of triples into three arrays. Mirrors List.unzip3.
583+
val unzip3 : source:AsyncSeq<'T1 * 'T2 * 'T3> -> Async<'T1[] * 'T2[] * 'T3[]>
584+
585+
/// Builds a new async sequence whose elements are the results of applying the given
586+
/// function to the corresponding elements of the two sequences. Stops when either
587+
/// sequence is exhausted. Mirrors Seq.map2.
588+
val map2 : mapping:('T1 -> 'T2 -> 'U) -> source1:AsyncSeq<'T1> -> source2:AsyncSeq<'T2> -> AsyncSeq<'U>
589+
590+
/// Builds a new async sequence whose elements are the results of applying the given
591+
/// function to the corresponding elements of three sequences. Stops when any
592+
/// sequence is exhausted. Mirrors List.map3.
593+
val map3 : mapping:('T1 -> 'T2 -> 'T3 -> 'U) -> source1:AsyncSeq<'T1> -> source2:AsyncSeq<'T2> -> source3:AsyncSeq<'T3> -> AsyncSeq<'U>
594+
579595
/// Builds a new asynchronous sequence whose elements are generated by
580596
/// applying the specified function to all elements of the input sequence.
581597
///

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

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3684,7 +3684,79 @@ let ``AsyncSeq.allPairs returns empty when second source is empty`` () =
36843684
|> AsyncSeq.toArrayAsync |> Async.RunSynchronously
36853685
Assert.AreEqual([||], result)
36863686

3687-
// ── AsyncSeq.rev ─────────────────────────────────────────────────────────────
3687+
// ── AsyncSeq.unzip ───────────────────────────────────────────────────────────
3688+
3689+
[<Test>]
3690+
let ``AsyncSeq.unzip splits pairs into two arrays`` () =
3691+
let source = asyncSeq { yield (1, 'a'); yield (2, 'b'); yield (3, 'c') }
3692+
let (lefts, rights) = AsyncSeq.unzip source |> Async.RunSynchronously
3693+
Assert.AreEqual([| 1; 2; 3 |], lefts)
3694+
Assert.AreEqual([| 'a'; 'b'; 'c' |], rights)
3695+
3696+
[<Test>]
3697+
let ``AsyncSeq.unzip empty source returns two empty arrays`` () =
3698+
let (lefts, rights) = AsyncSeq.unzip AsyncSeq.empty<int * string> |> Async.RunSynchronously
3699+
Assert.AreEqual([||], lefts)
3700+
Assert.AreEqual([||], rights)
3701+
3702+
[<Test>]
3703+
let ``AsyncSeq.unzip mirrors List.unzip`` () =
3704+
let pairs = [ (1, 'x'); (2, 'y'); (3, 'z') ]
3705+
let (expL, expR) = List.unzip pairs
3706+
let (actL, actR) = AsyncSeq.unzip (AsyncSeq.ofList pairs) |> Async.RunSynchronously
3707+
Assert.AreEqual(expL |> Array.ofList, actL)
3708+
Assert.AreEqual(expR |> Array.ofList, actR)
3709+
3710+
// ── AsyncSeq.unzip3 ──────────────────────────────────────────────────────────
3711+
3712+
[<Test>]
3713+
let ``AsyncSeq.unzip3 splits triples into three arrays`` () =
3714+
let source = asyncSeq { yield (1, 'a', true); yield (2, 'b', false); yield (3, 'c', true) }
3715+
let (as1, as2, as3) = AsyncSeq.unzip3 source |> Async.RunSynchronously
3716+
Assert.AreEqual([| 1; 2; 3 |], as1)
3717+
Assert.AreEqual([| 'a'; 'b'; 'c' |], as2)
3718+
Assert.AreEqual([| true; false; true |], as3)
3719+
3720+
[<Test>]
3721+
let ``AsyncSeq.unzip3 empty source returns three empty arrays`` () =
3722+
let (as1, as2, as3) = AsyncSeq.unzip3 AsyncSeq.empty<int * string * bool> |> Async.RunSynchronously
3723+
Assert.AreEqual([||], as1)
3724+
Assert.AreEqual([||], as2)
3725+
Assert.AreEqual([||], as3)
3726+
3727+
// ── AsyncSeq.map2 ────────────────────────────────────────────────────────────
3728+
3729+
[<Test>]
3730+
let ``AsyncSeq.map2 applies function pairwise`` () =
3731+
let s1 = asyncSeq { yield 1; yield 2; yield 3 }
3732+
let s2 = asyncSeq { yield 10; yield 20; yield 30 }
3733+
let result = AsyncSeq.map2 (+) s1 s2 |> AsyncSeq.toArrayAsync |> Async.RunSynchronously
3734+
Assert.AreEqual([| 11; 22; 33 |], result)
3735+
3736+
[<Test>]
3737+
let ``AsyncSeq.map2 stops at shorter sequence`` () =
3738+
let s1 = asyncSeq { yield 1; yield 2; yield 3 }
3739+
let s2 = asyncSeq { yield 10; yield 20 }
3740+
let result = AsyncSeq.map2 (+) s1 s2 |> AsyncSeq.toArrayAsync |> Async.RunSynchronously
3741+
Assert.AreEqual([| 11; 22 |], result)
3742+
3743+
// ── AsyncSeq.map3 ────────────────────────────────────────────────────────────
3744+
3745+
[<Test>]
3746+
let ``AsyncSeq.map3 applies function to three sequences`` () =
3747+
let s1 = asyncSeq { yield 1; yield 2; yield 3 }
3748+
let s2 = asyncSeq { yield 10; yield 20; yield 30 }
3749+
let s3 = asyncSeq { yield 100; yield 200; yield 300 }
3750+
let result = AsyncSeq.map3 (fun a b c -> a + b + c) s1 s2 s3 |> AsyncSeq.toArrayAsync |> Async.RunSynchronously
3751+
Assert.AreEqual([| 111; 222; 333 |], result)
3752+
3753+
[<Test>]
3754+
let ``AsyncSeq.map3 stops at shortest sequence`` () =
3755+
let s1 = asyncSeq { yield 1; yield 2; yield 3 }
3756+
let s2 = asyncSeq { yield 10; yield 20; yield 30 }
3757+
let s3 = asyncSeq { yield 100 }
3758+
let result = AsyncSeq.map3 (fun a b c -> a + b + c) s1 s2 s3 |> AsyncSeq.toArrayAsync |> Async.RunSynchronously
3759+
Assert.AreEqual([| 111 |], result)
36883760

36893761
[<Test>]
36903762
let ``AsyncSeq.rev reverses a sequence`` () =

0 commit comments

Comments
 (0)