diff --git a/README.md b/README.md index dcc1cbf..2559e94 100644 --- a/README.md +++ b/README.md @@ -385,7 +385,7 @@ This is what has been implemented so far, is planned or skipped: | ✅ [#258][] | `windowed` | `windowed` | | | | ✅ [#2][] | `zip` | `zip` | | | | ✅ | `zip3` | `zip3` | | | -| | | `zip4` | | | +| ✅ | | `zip4` | | | ¹⁾ _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._ diff --git a/release-notes.txt b/release-notes.txt index 5c0b97e..e029cee 100644 --- a/release-notes.txt +++ b/release-notes.txt @@ -2,6 +2,7 @@ Release notes: 0.6.0 + - adds TaskSeq.zip4 - adds TaskSeq.scan and TaskSeq.scanAsync, #289 - adds TaskSeq.pairwise, #289 - adds TaskSeq.groupBy and TaskSeq.groupByAsync, #289 diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Zip.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Zip.Tests.fs index 67db6a4..5e31dfa 100644 --- a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Zip.Tests.fs +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Zip.Tests.fs @@ -304,3 +304,132 @@ module SideEffectsZip3 = combined |> should haveLength 10 } + +// +// TaskSeq.zip4 +// + +module EmptySeqZip4 = + [] + let ``Null source is invalid for zip4`` () = + assertNullArg + <| fun () -> TaskSeq.zip4 null TaskSeq.empty TaskSeq.empty TaskSeq.empty + + assertNullArg + <| fun () -> TaskSeq.zip4 TaskSeq.empty null TaskSeq.empty TaskSeq.empty + + assertNullArg + <| fun () -> TaskSeq.zip4 TaskSeq.empty TaskSeq.empty null TaskSeq.empty + + assertNullArg + <| fun () -> TaskSeq.zip4 TaskSeq.empty TaskSeq.empty TaskSeq.empty null + + assertNullArg <| fun () -> TaskSeq.zip4 null null null null + + [)>] + let ``TaskSeq-zip4 can zip empty sequences`` variant = + TaskSeq.zip4 (Gen.getEmptyVariant variant) (Gen.getEmptyVariant variant) (Gen.getEmptyVariant variant) (Gen.getEmptyVariant variant) + |> verifyEmpty + + [)>] + let ``TaskSeq-zip4 stops at first exhausted sequence`` variant = + // remaining sequences are non-empty but first is empty → result is empty + TaskSeq.zip4 (Gen.getEmptyVariant variant) (taskSeq { yield 1 }) (taskSeq { yield 2 }) (taskSeq { yield 3 }) + |> verifyEmpty + + [)>] + let ``TaskSeq-zip4 stops when second sequence is empty`` variant = + TaskSeq.zip4 (taskSeq { yield 1 }) (Gen.getEmptyVariant variant) (taskSeq { yield 2 }) (taskSeq { yield 3 }) + |> verifyEmpty + + [)>] + let ``TaskSeq-zip4 stops when third sequence is empty`` variant = + TaskSeq.zip4 (taskSeq { yield 1 }) (taskSeq { yield 2 }) (Gen.getEmptyVariant variant) (taskSeq { yield 3 }) + |> verifyEmpty + + [)>] + let ``TaskSeq-zip4 stops when fourth sequence is empty`` variant = + TaskSeq.zip4 (taskSeq { yield 1 }) (taskSeq { yield 2 }) (taskSeq { yield 3 }) (Gen.getEmptyVariant variant) + |> verifyEmpty + +module ImmutableZip4 = + [)>] + let ``TaskSeq-zip4 zips in correct order`` variant = task { + let one = Gen.getSeqImmutable variant + let two = Gen.getSeqImmutable variant + let three = Gen.getSeqImmutable variant + let four = Gen.getSeqImmutable variant + let! combined = TaskSeq.zip4 one two three four |> TaskSeq.toArrayAsync + + combined |> should haveLength 10 + + combined + |> should equal (Array.init 10 (fun x -> x + 1, x + 1, x + 1, x + 1)) + } + + [] + let ``TaskSeq-zip4 produces correct 4-tuples with mixed types`` () = task { + let one = taskSeq { + yield "a" + yield "b" + } + + let two = taskSeq { + yield 1 + yield 2 + } + + let three = taskSeq { + yield true + yield false + } + + let four = taskSeq { + yield 1.0 + yield 2.0 + } + + let! combined = TaskSeq.zip4 one two three four |> TaskSeq.toArrayAsync + + combined + |> should equal [| ("a", 1, true, 1.0); ("b", 2, false, 2.0) |] + } + + [] + let ``TaskSeq-zip4 truncates to shortest sequence`` () = task { + let one = taskSeq { yield! [ 1..10 ] } + let two = taskSeq { yield! [ 1..5 ] } + let three = taskSeq { yield! [ 1..3 ] } + let four = taskSeq { yield! [ 1..7 ] } + let! combined = TaskSeq.zip4 one two three four |> TaskSeq.toArrayAsync + + combined |> should haveLength 3 + + combined + |> should equal [| (1, 1, 1, 1); (2, 2, 2, 2); (3, 3, 3, 3) |] + } + + [] + let ``TaskSeq-zip4 works with single-element sequences`` () = task { + let! combined = + TaskSeq.zip4 (TaskSeq.singleton 1) (TaskSeq.singleton "x") (TaskSeq.singleton true) (TaskSeq.singleton 42L) + |> TaskSeq.toArrayAsync + + combined |> should equal [| (1, "x", true, 42L) |] + } + +module SideEffectsZip4 = + [)>] + let ``TaskSeq-zip4 can deal with side effects in sequences`` variant = task { + let one = Gen.getSeqWithSideEffect variant + let two = Gen.getSeqWithSideEffect variant + let three = Gen.getSeqWithSideEffect variant + let four = Gen.getSeqWithSideEffect variant + let! combined = TaskSeq.zip4 one two three four |> TaskSeq.toArrayAsync + + combined + |> Array.forall (fun (x, y, z, w) -> x = y && y = z && z = w) + |> should be True + + combined |> should haveLength 10 + } diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fs b/src/FSharp.Control.TaskSeq/TaskSeq.fs index aced3a5..a1fd555 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeq.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeq.fs @@ -512,6 +512,7 @@ type TaskSeq private () = static member zip source1 source2 = Internal.zip source1 source2 static member zip3 source1 source2 source3 = Internal.zip3 source1 source2 source3 + static member zip4 source1 source2 source3 source4 = Internal.zip4 source1 source2 source3 source4 static member fold folder state source = Internal.fold (FolderAction folder) state source static member foldAsync folder state source = Internal.fold (AsyncFolderAction folder) state source static member scan folder state source = Internal.scan (FolderAction folder) state source diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fsi b/src/FSharp.Control.TaskSeq/TaskSeq.fsi index fcf45cf..d92bdb0 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeq.fsi +++ b/src/FSharp.Control.TaskSeq/TaskSeq.fsi @@ -1554,6 +1554,24 @@ type TaskSeq = static member zip3: source1: TaskSeq<'T1> -> source2: TaskSeq<'T2> -> source3: TaskSeq<'T3> -> TaskSeq<'T1 * 'T2 * 'T3> + /// + /// Combines the four task sequences into a new task sequence of 4-tuples. The four sequences need not have equal lengths: + /// when one sequence is exhausted any remaining elements in the other sequences are ignored. + /// + /// + /// The first input task sequence. + /// The second input task sequence. + /// The third input task sequence. + /// The fourth input task sequence. + /// The result task sequence of 4-tuples. + /// Thrown when any of the four input task sequences is null. + static member zip4: + source1: TaskSeq<'T1> -> + source2: TaskSeq<'T2> -> + source3: TaskSeq<'T3> -> + source4: TaskSeq<'T4> -> + TaskSeq<'T1 * 'T2 * 'T3 * 'T4> + /// /// argument of type through the computation. If the input function is and the elements are /// then computes. diff --git a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs index 26b02a6..66363e0 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs @@ -585,6 +585,33 @@ module internal TaskSeqInternal = go <- step1 && step2 && step3 } + let zip4 (source1: TaskSeq<_>) (source2: TaskSeq<_>) (source3: TaskSeq<_>) (source4: TaskSeq<_>) = + checkNonNull (nameof source1) source1 + checkNonNull (nameof source2) source2 + checkNonNull (nameof source3) source3 + checkNonNull (nameof source4) source4 + + taskSeq { + use e1 = source1.GetAsyncEnumerator CancellationToken.None + use e2 = source2.GetAsyncEnumerator CancellationToken.None + use e3 = source3.GetAsyncEnumerator CancellationToken.None + use e4 = source4.GetAsyncEnumerator CancellationToken.None + let mutable go = true + let! step1 = e1.MoveNextAsync() + let! step2 = e2.MoveNextAsync() + let! step3 = e3.MoveNextAsync() + let! step4 = e4.MoveNextAsync() + go <- step1 && step2 && step3 && step4 + + while go do + yield e1.Current, e2.Current, e3.Current, e4.Current + let! step1 = e1.MoveNextAsync() + let! step2 = e2.MoveNextAsync() + let! step3 = e3.MoveNextAsync() + let! step4 = e4.MoveNextAsync() + go <- step1 && step2 && step3 && step4 + } + let collect (binder: _ -> #IAsyncEnumerable<_>) (source: TaskSeq<_>) = checkNonNull (nameof source) source