Skip to content

Commit 07e5c25

Browse files
authored
Merge pull request #318 from fsprojects/repo-assist/improve-unzip-map2-map3-20260423-500215a10a9a2b49
[Repo Assist] Add AsyncSeq.unzip, unzip3, map2, map3
2 parents a9b4437 + d0e29b0 commit 07e5c25

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
@@ -6,6 +6,10 @@
66
### 4.16.0
77

88
* 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.
9+
* Added `AsyncSeq.unzip` — splits an async sequence of pairs into two arrays. Mirrors `List.unzip`.
10+
* Added `AsyncSeq.unzip3` — splits an async sequence of triples into three arrays. Mirrors `List.unzip3`.
11+
* Added `AsyncSeq.map2` — applies a function to corresponding elements of two async sequences; stops when either is exhausted. Mirrors `Seq.map2`.
12+
* Added `AsyncSeq.map3` — applies a function to corresponding elements of three async sequences; stops when any is exhausted. Mirrors `List.map3`.
913

1014
### 4.15.0
1115

src/FSharp.Control.AsyncSeq/AsyncSeq.fs

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

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

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
@@ -3691,7 +3691,79 @@ let ``AsyncSeq.allPairs returns empty when second source is empty`` () =
36913691
|> AsyncSeq.toArrayAsync |> Async.RunSynchronously
36923692
Assert.AreEqual([||], result)
36933693

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

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

0 commit comments

Comments
 (0)