Skip to content

Commit a2305ba

Browse files
feat: add TaskSeq.zip4 (53 tests)
Adds TaskSeq.zip4 which combines four task sequences into a task sequence of 4-tuples, truncating to the shortest source sequence. This follows the same pattern as the existing zip and zip3 functions. Files changed: - TaskSeqInternal.fs: zip4 implementation - TaskSeq.fs: public static member wrapper - TaskSeq.fsi: XML-documented signature - TaskSeq.Zip.Tests.fs: 53 new tests (null checks, empty variants, immutable, side effects, unequal lengths, mixed types) - README.md: marks zip4 as implemented - release-notes.txt: updated Co-authored-by: Copilot <[email protected]>
1 parent 5a03593 commit a2305ba

6 files changed

Lines changed: 177 additions & 1 deletion

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,7 @@ This is what has been implemented so far, is planned or skipped:
385385
| &#x2705; [#258][] | `windowed` | `windowed` | | |
386386
| &#x2705; [#2][] | `zip` | `zip` | | |
387387
| &#x2705; | `zip3` | `zip3` | | |
388-
| | | `zip4` | | |
388+
| &#x2705; | | `zip4` | | |
389389

390390

391391
<sup>¹⁾ <a id="note1"></a>_These functions require a form of pre-materializing through `TaskSeq.cache`, similar to the approach taken in the corresponding `Seq` functions. It doesn't make much sense to have a cached async sequence. However, `AsyncSeq` does implement these, so we'll probably do so eventually as well._</sup>

release-notes.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Release notes:
33

44
0.6.0
5+
- adds TaskSeq.zip4
56
- adds TaskSeq.scan and TaskSeq.scanAsync, #289
67
- adds TaskSeq.pairwise, #289
78
- adds TaskSeq.groupBy and TaskSeq.groupByAsync, #289

src/FSharp.Control.TaskSeq.Test/TaskSeq.Zip.Tests.fs

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,3 +304,132 @@ module SideEffectsZip3 =
304304

305305
combined |> should haveLength 10
306306
}
307+
308+
//
309+
// TaskSeq.zip4
310+
//
311+
312+
module EmptySeqZip4 =
313+
[<Fact>]
314+
let ``Null source is invalid for zip4`` () =
315+
assertNullArg
316+
<| fun () -> TaskSeq.zip4 null TaskSeq.empty TaskSeq.empty TaskSeq.empty
317+
318+
assertNullArg
319+
<| fun () -> TaskSeq.zip4 TaskSeq.empty null TaskSeq.empty TaskSeq.empty
320+
321+
assertNullArg
322+
<| fun () -> TaskSeq.zip4 TaskSeq.empty TaskSeq.empty null TaskSeq.empty
323+
324+
assertNullArg
325+
<| fun () -> TaskSeq.zip4 TaskSeq.empty TaskSeq.empty TaskSeq.empty null
326+
327+
assertNullArg <| fun () -> TaskSeq.zip4 null null null null
328+
329+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
330+
let ``TaskSeq-zip4 can zip empty sequences`` variant =
331+
TaskSeq.zip4 (Gen.getEmptyVariant variant) (Gen.getEmptyVariant variant) (Gen.getEmptyVariant variant) (Gen.getEmptyVariant variant)
332+
|> verifyEmpty
333+
334+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
335+
let ``TaskSeq-zip4 stops at first exhausted sequence`` variant =
336+
// remaining sequences are non-empty but first is empty → result is empty
337+
TaskSeq.zip4 (Gen.getEmptyVariant variant) (taskSeq { yield 1 }) (taskSeq { yield 2 }) (taskSeq { yield 3 })
338+
|> verifyEmpty
339+
340+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
341+
let ``TaskSeq-zip4 stops when second sequence is empty`` variant =
342+
TaskSeq.zip4 (taskSeq { yield 1 }) (Gen.getEmptyVariant variant) (taskSeq { yield 2 }) (taskSeq { yield 3 })
343+
|> verifyEmpty
344+
345+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
346+
let ``TaskSeq-zip4 stops when third sequence is empty`` variant =
347+
TaskSeq.zip4 (taskSeq { yield 1 }) (taskSeq { yield 2 }) (Gen.getEmptyVariant variant) (taskSeq { yield 3 })
348+
|> verifyEmpty
349+
350+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
351+
let ``TaskSeq-zip4 stops when fourth sequence is empty`` variant =
352+
TaskSeq.zip4 (taskSeq { yield 1 }) (taskSeq { yield 2 }) (taskSeq { yield 3 }) (Gen.getEmptyVariant variant)
353+
|> verifyEmpty
354+
355+
module ImmutableZip4 =
356+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
357+
let ``TaskSeq-zip4 zips in correct order`` variant = task {
358+
let one = Gen.getSeqImmutable variant
359+
let two = Gen.getSeqImmutable variant
360+
let three = Gen.getSeqImmutable variant
361+
let four = Gen.getSeqImmutable variant
362+
let! combined = TaskSeq.zip4 one two three four |> TaskSeq.toArrayAsync
363+
364+
combined |> should haveLength 10
365+
366+
combined
367+
|> should equal (Array.init 10 (fun x -> x + 1, x + 1, x + 1, x + 1))
368+
}
369+
370+
[<Fact>]
371+
let ``TaskSeq-zip4 produces correct 4-tuples with mixed types`` () = task {
372+
let one = taskSeq {
373+
yield "a"
374+
yield "b"
375+
}
376+
377+
let two = taskSeq {
378+
yield 1
379+
yield 2
380+
}
381+
382+
let three = taskSeq {
383+
yield true
384+
yield false
385+
}
386+
387+
let four = taskSeq {
388+
yield 1.0
389+
yield 2.0
390+
}
391+
392+
let! combined = TaskSeq.zip4 one two three four |> TaskSeq.toArrayAsync
393+
394+
combined
395+
|> should equal [| ("a", 1, true, 1.0); ("b", 2, false, 2.0) |]
396+
}
397+
398+
[<Fact>]
399+
let ``TaskSeq-zip4 truncates to shortest sequence`` () = task {
400+
let one = taskSeq { yield! [ 1..10 ] }
401+
let two = taskSeq { yield! [ 1..5 ] }
402+
let three = taskSeq { yield! [ 1..3 ] }
403+
let four = taskSeq { yield! [ 1..7 ] }
404+
let! combined = TaskSeq.zip4 one two three four |> TaskSeq.toArrayAsync
405+
406+
combined |> should haveLength 3
407+
408+
combined
409+
|> should equal [| (1, 1, 1, 1); (2, 2, 2, 2); (3, 3, 3, 3) |]
410+
}
411+
412+
[<Fact>]
413+
let ``TaskSeq-zip4 works with single-element sequences`` () = task {
414+
let! combined =
415+
TaskSeq.zip4 (TaskSeq.singleton 1) (TaskSeq.singleton "x") (TaskSeq.singleton true) (TaskSeq.singleton 42L)
416+
|> TaskSeq.toArrayAsync
417+
418+
combined |> should equal [| (1, "x", true, 42L) |]
419+
}
420+
421+
module SideEffectsZip4 =
422+
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
423+
let ``TaskSeq-zip4 can deal with side effects in sequences`` variant = task {
424+
let one = Gen.getSeqWithSideEffect variant
425+
let two = Gen.getSeqWithSideEffect variant
426+
let three = Gen.getSeqWithSideEffect variant
427+
let four = Gen.getSeqWithSideEffect variant
428+
let! combined = TaskSeq.zip4 one two three four |> TaskSeq.toArrayAsync
429+
430+
combined
431+
|> Array.forall (fun (x, y, z, w) -> x = y && y = z && z = w)
432+
|> should be True
433+
434+
combined |> should haveLength 10
435+
}

src/FSharp.Control.TaskSeq/TaskSeq.fs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,7 @@ type TaskSeq private () =
512512

513513
static member zip source1 source2 = Internal.zip source1 source2
514514
static member zip3 source1 source2 source3 = Internal.zip3 source1 source2 source3
515+
static member zip4 source1 source2 source3 source4 = Internal.zip4 source1 source2 source3 source4
515516
static member fold folder state source = Internal.fold (FolderAction folder) state source
516517
static member foldAsync folder state source = Internal.fold (AsyncFolderAction folder) state source
517518
static member scan folder state source = Internal.scan (FolderAction folder) state source

src/FSharp.Control.TaskSeq/TaskSeq.fsi

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1554,6 +1554,24 @@ type TaskSeq =
15541554
static member zip3:
15551555
source1: TaskSeq<'T1> -> source2: TaskSeq<'T2> -> source3: TaskSeq<'T3> -> TaskSeq<'T1 * 'T2 * 'T3>
15561556

1557+
/// <summary>
1558+
/// Combines the four task sequences into a new task sequence of 4-tuples. The four sequences need not have equal lengths:
1559+
/// when one sequence is exhausted any remaining elements in the other sequences are ignored.
1560+
/// </summary>
1561+
///
1562+
/// <param name="source1">The first input task sequence.</param>
1563+
/// <param name="source2">The second input task sequence.</param>
1564+
/// <param name="source3">The third input task sequence.</param>
1565+
/// <param name="source4">The fourth input task sequence.</param>
1566+
/// <returns>The result task sequence of 4-tuples.</returns>
1567+
/// <exception cref="T:ArgumentNullException">Thrown when any of the four input task sequences is null.</exception>
1568+
static member zip4:
1569+
source1: TaskSeq<'T1> ->
1570+
source2: TaskSeq<'T2> ->
1571+
source3: TaskSeq<'T3> ->
1572+
source4: TaskSeq<'T4> ->
1573+
TaskSeq<'T1 * 'T2 * 'T3 * 'T4>
1574+
15571575
/// <summary>
15581576
/// argument of type <typeref name="'State" /> through the computation. If the input function is <paramref name="f" /> and the elements are <paramref name="i0...iN" />
15591577
/// then computes<paramref name="f (... (f s i0)...) iN" />.

src/FSharp.Control.TaskSeq/TaskSeqInternal.fs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,33 @@ module internal TaskSeqInternal =
585585
go <- step1 && step2 && step3
586586
}
587587

588+
let zip4 (source1: TaskSeq<_>) (source2: TaskSeq<_>) (source3: TaskSeq<_>) (source4: TaskSeq<_>) =
589+
checkNonNull (nameof source1) source1
590+
checkNonNull (nameof source2) source2
591+
checkNonNull (nameof source3) source3
592+
checkNonNull (nameof source4) source4
593+
594+
taskSeq {
595+
use e1 = source1.GetAsyncEnumerator CancellationToken.None
596+
use e2 = source2.GetAsyncEnumerator CancellationToken.None
597+
use e3 = source3.GetAsyncEnumerator CancellationToken.None
598+
use e4 = source4.GetAsyncEnumerator CancellationToken.None
599+
let mutable go = true
600+
let! step1 = e1.MoveNextAsync()
601+
let! step2 = e2.MoveNextAsync()
602+
let! step3 = e3.MoveNextAsync()
603+
let! step4 = e4.MoveNextAsync()
604+
go <- step1 && step2 && step3 && step4
605+
606+
while go do
607+
yield e1.Current, e2.Current, e3.Current, e4.Current
608+
let! step1 = e1.MoveNextAsync()
609+
let! step2 = e2.MoveNextAsync()
610+
let! step3 = e3.MoveNextAsync()
611+
let! step4 = e4.MoveNextAsync()
612+
go <- step1 && step2 && step3 && step4
613+
}
614+
588615
let collect (binder: _ -> #IAsyncEnumerable<_>) (source: TaskSeq<_>) =
589616
checkNonNull (nameof source) source
590617

0 commit comments

Comments
 (0)