Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` | | |


<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>
Expand Down
1 change: 1 addition & 0 deletions release-notes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
129 changes: 129 additions & 0 deletions src/FSharp.Control.TaskSeq.Test/TaskSeq.Zip.Tests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -304,3 +304,132 @@ module SideEffectsZip3 =

combined |> should haveLength 10
}

//
// TaskSeq.zip4
//

module EmptySeqZip4 =
[<Fact>]
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

[<Theory; ClassData(typeof<TestEmptyVariants>)>]
let ``TaskSeq-zip4 can zip empty sequences`` variant =
TaskSeq.zip4 (Gen.getEmptyVariant variant) (Gen.getEmptyVariant variant) (Gen.getEmptyVariant variant) (Gen.getEmptyVariant variant)
|> verifyEmpty

[<Theory; ClassData(typeof<TestEmptyVariants>)>]
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

[<Theory; ClassData(typeof<TestEmptyVariants>)>]
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

[<Theory; ClassData(typeof<TestEmptyVariants>)>]
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

[<Theory; ClassData(typeof<TestEmptyVariants>)>]
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 =
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
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))
}

[<Fact>]
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) |]
}

[<Fact>]
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) |]
}

[<Fact>]
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 =
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
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
}
1 change: 1 addition & 0 deletions src/FSharp.Control.TaskSeq/TaskSeq.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions src/FSharp.Control.TaskSeq/TaskSeq.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -1554,6 +1554,24 @@ type TaskSeq =
static member zip3:
source1: TaskSeq<'T1> -> source2: TaskSeq<'T2> -> source3: TaskSeq<'T3> -> TaskSeq<'T1 * 'T2 * 'T3>

/// <summary>
/// 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.
/// </summary>
///
/// <param name="source1">The first input task sequence.</param>
/// <param name="source2">The second input task sequence.</param>
/// <param name="source3">The third input task sequence.</param>
/// <param name="source4">The fourth input task sequence.</param>
/// <returns>The result task sequence of 4-tuples.</returns>
/// <exception cref="T:ArgumentNullException">Thrown when any of the four input task sequences is null.</exception>
static member zip4:
source1: TaskSeq<'T1> ->
source2: TaskSeq<'T2> ->
source3: TaskSeq<'T3> ->
source4: TaskSeq<'T4> ->
TaskSeq<'T1 * 'T2 * 'T3 * 'T4>

/// <summary>
/// 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" />
/// then computes<paramref name="f (... (f s i0)...) iN" />.
Expand Down
27 changes: 27 additions & 0 deletions src/FSharp.Control.TaskSeq/TaskSeqInternal.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down