diff --git a/README.md b/README.md
index 0d28e59b..57a47784 100644
--- a/README.md
+++ b/README.md
@@ -218,7 +218,7 @@ The `TaskSeq` project already has a wide array of functions and functionalities,
- [ ] `mapFold`
- [x] `pairwise` (see [#293])
- [ ] `allpairs` / `permute` / `distinct` / `distinctBy`
- - [ ] `replicate`
+ - [x] `replicate`
- [x] `reduce` / `scan` (see [#299], [#296])
- [x] `unfold` (see [#300])
- [x] Publish package on Nuget, **DONE, PUBLISHED SINCE: 7 November 2022**. See https://www.nuget.org/packages/FSharp.Control.TaskSeq
@@ -341,7 +341,7 @@ This is what has been implemented so far, is planned or skipped:
| 🚫 | `reduceBack` | | | [note #2](#note2 "Because of the async nature of TaskSeq sequences, iterating from the back would be bad practice. Instead, materialize the sequence to a list or array and then apply the 'Back' iterators.") |
| ✅ [#236][]| `removeAt` | `removeAt` | | |
| ✅ [#236][]| `removeManyAt` | `removeManyAt` | | |
-| | `replicate` | `replicate` | | |
+| ✅ | `replicate` | `replicate` | | |
| ❓ | `rev` | | | [note #1](#note1 "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.") |
| ✅ [#296][] | `scan` | `scan` | `scanAsync` | |
| 🚫 | `scanBack` | | | [note #2](#note2 "Because of the async nature of TaskSeq sequences, iterating from the back would be bad practice. Instead, materialize the sequence to a list or array and then apply the 'Back' iterators.") |
@@ -384,7 +384,7 @@ This is what has been implemented so far, is planned or skipped:
| ✅ [#217][]| `where` | `where` | `whereAsync` | |
| ✅ [#258][] | `windowed` | `windowed` | | |
| ✅ [#2][] | `zip` | `zip` | | |
-| | `zip3` | `zip3` | | |
+| ✅ | `zip3` | `zip3` | | |
| | | `zip4` | | |
diff --git a/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj b/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj
index 09181383..429bdafa 100644
--- a/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj
+++ b/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj
@@ -47,6 +47,7 @@
+
diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Replicate.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Replicate.Tests.fs
new file mode 100644
index 00000000..71012c25
--- /dev/null
+++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Replicate.Tests.fs
@@ -0,0 +1,81 @@
+module TaskSeq.Tests.Replicate
+
+open System
+
+open Xunit
+open FsUnit.Xunit
+
+open FSharp.Control
+
+//
+// TaskSeq.replicate
+//
+
+module EmptySeq =
+ []
+ let ``TaskSeq-replicate with count 0 gives empty sequence`` () = TaskSeq.replicate 0 42 |> verifyEmpty
+
+ []
+ let ``TaskSeq-replicate with negative count gives an error`` () =
+ fun () ->
+ TaskSeq.replicate -1 42
+ |> TaskSeq.toArrayAsync
+ |> Task.ignore
+
+ |> should throwAsyncExact typeof
+
+ fun () ->
+ TaskSeq.replicate Int32.MinValue "hello"
+ |> TaskSeq.toArrayAsync
+ |> Task.ignore
+
+ |> should throwAsyncExact typeof
+
+module Immutable =
+ []
+ let ``TaskSeq-replicate produces the correct count and value`` () = task {
+ let! arr = TaskSeq.replicate 5 99 |> TaskSeq.toArrayAsync
+ arr |> should haveLength 5
+ arr |> should equal [| 99; 99; 99; 99; 99 |]
+ }
+
+ []
+ let ``TaskSeq-replicate with count 1 produces a singleton`` () = task {
+ let! arr = TaskSeq.replicate 1 "x" |> TaskSeq.toArrayAsync
+ arr |> should haveLength 1
+ arr[0] |> should equal "x"
+ }
+
+ []
+ let ``TaskSeq-replicate with large count`` () = task {
+ let count = 10_000
+ let! arr = TaskSeq.replicate count 7 |> TaskSeq.toArrayAsync
+ arr |> should haveLength count
+ arr |> Array.forall ((=) 7) |> should be True
+ }
+
+ []
+ let ``TaskSeq-replicate works with null as value`` () = task {
+ let! arr = TaskSeq.replicate 3 null |> TaskSeq.toArrayAsync
+ arr |> should haveLength 3
+ arr |> Array.forall (fun x -> x = null) |> should be True
+ }
+
+ []
+ let ``TaskSeq-replicate can be consumed multiple times`` () = task {
+ let ts = TaskSeq.replicate 4 "a"
+ let! arr1 = ts |> TaskSeq.toArrayAsync
+ let! arr2 = ts |> TaskSeq.toArrayAsync
+ arr1 |> should equal arr2
+ }
+
+module SideEffects =
+ []
+ let ``TaskSeq-replicate with a mutable value captures the value, not a reference`` () = task {
+ let mutable x = 1
+ let ts = TaskSeq.replicate 3 x
+ x <- 999
+ let! arr = ts |> TaskSeq.toArrayAsync
+ // replicate captures the value at call time (value type)
+ arr |> should equal [| 1; 1; 1 |]
+ }
diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Zip.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Zip.Tests.fs
index 7443f7fe..67db6a4a 100644
--- a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Zip.Tests.fs
+++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Zip.Tests.fs
@@ -191,3 +191,116 @@ module Other =
combined |> should equal [| ("one", 42L); ("two", 43L) |]
}
+
+//
+// TaskSeq.zip3
+//
+
+module EmptySeqZip3 =
+ []
+ let ``Null source is invalid for zip3`` () =
+ assertNullArg
+ <| fun () -> TaskSeq.zip3 null TaskSeq.empty TaskSeq.empty
+
+ assertNullArg
+ <| fun () -> TaskSeq.zip3 TaskSeq.empty null TaskSeq.empty
+
+ assertNullArg
+ <| fun () -> TaskSeq.zip3 TaskSeq.empty TaskSeq.empty null
+
+ assertNullArg <| fun () -> TaskSeq.zip3 null null null
+
+ [)>]
+ let ``TaskSeq-zip3 can zip empty sequences`` variant =
+ TaskSeq.zip3 (Gen.getEmptyVariant variant) (Gen.getEmptyVariant variant) (Gen.getEmptyVariant variant)
+ |> verifyEmpty
+
+ [)>]
+ let ``TaskSeq-zip3 stops at first exhausted sequence`` variant =
+ // second and third are non-empty, first is empty → result is empty
+ TaskSeq.zip3 (Gen.getEmptyVariant variant) (taskSeq { yield 1 }) (taskSeq { yield 2 })
+ |> verifyEmpty
+
+ [)>]
+ let ``TaskSeq-zip3 stops when second sequence is empty`` variant =
+ TaskSeq.zip3 (taskSeq { yield 1 }) (Gen.getEmptyVariant variant) (taskSeq { yield 2 })
+ |> verifyEmpty
+
+ [)>]
+ let ``TaskSeq-zip3 stops when third sequence is empty`` variant =
+ TaskSeq.zip3 (taskSeq { yield 1 }) (taskSeq { yield 2 }) (Gen.getEmptyVariant variant)
+ |> verifyEmpty
+
+module ImmutableZip3 =
+ [)>]
+ let ``TaskSeq-zip3 zips in correct order`` variant = task {
+ let one = Gen.getSeqImmutable variant
+ let two = Gen.getSeqImmutable variant
+ let three = Gen.getSeqImmutable variant
+ let! combined = TaskSeq.zip3 one two three |> TaskSeq.toArrayAsync
+
+ combined |> should haveLength 10
+
+ combined
+ |> should equal (Array.init 10 (fun x -> x + 1, x + 1, x + 1))
+ }
+
+ []
+ let ``TaskSeq-zip3 produces correct triples 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! combined = TaskSeq.zip3 one two three |> TaskSeq.toArrayAsync
+
+ combined
+ |> should equal [| ("a", 1, true); ("b", 2, false) |]
+ }
+
+ []
+ let ``TaskSeq-zip3 truncates to shortest sequence`` () = task {
+ let one = taskSeq { yield! [ 1..10 ] }
+ let two = taskSeq { yield! [ 1..5 ] }
+ let three = taskSeq { yield! [ 1..3 ] }
+ let! combined = TaskSeq.zip3 one two three |> TaskSeq.toArrayAsync
+
+ combined |> should haveLength 3
+
+ combined
+ |> should equal [| (1, 1, 1); (2, 2, 2); (3, 3, 3) |]
+ }
+
+ []
+ let ``TaskSeq-zip3 works with a single-element sequences`` () = task {
+ let! combined =
+ TaskSeq.zip3 (TaskSeq.singleton 1) (TaskSeq.singleton "x") (TaskSeq.singleton true)
+ |> TaskSeq.toArrayAsync
+
+ combined |> should equal [| (1, "x", true) |]
+ }
+
+module SideEffectsZip3 =
+ [)>]
+ let ``TaskSeq-zip3 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! combined = TaskSeq.zip3 one two three |> TaskSeq.toArrayAsync
+
+ combined
+ |> Array.forall (fun (x, y, z) -> x = y && y = z)
+ |> should be True
+
+ combined |> should haveLength 10
+ }
diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fs b/src/FSharp.Control.TaskSeq/TaskSeq.fs
index c9c96cc9..11e8dc8d 100644
--- a/src/FSharp.Control.TaskSeq/TaskSeq.fs
+++ b/src/FSharp.Control.TaskSeq/TaskSeq.fs
@@ -123,6 +123,7 @@ type TaskSeq private () =
// the 'private ()' ensure that a constructor is emitted, which is required by IL
static member singleton(value: 'T) = Internal.singleton value
+ static member replicate count value = Internal.replicate count value
static member isEmpty source = Internal.isEmpty source
@@ -512,6 +513,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 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 76b7ffaa..9a6ccc04 100644
--- a/src/FSharp.Control.TaskSeq/TaskSeq.fsi
+++ b/src/FSharp.Control.TaskSeq/TaskSeq.fsi
@@ -103,6 +103,16 @@ type TaskSeq =
/// The input item to use as the single item of the task sequence.
static member singleton: value: 'T -> TaskSeq<'T>
+ ///
+ /// Creates a task sequence by replicating a total of times.
+ ///
+ ///
+ /// The number of times to replicate the value.
+ /// The value to replicate.
+ /// A task sequence containing copies of .
+ /// Thrown when is negative.
+ static member replicate: count: int -> value: 'T -> TaskSeq<'T>
+
///
/// Returns if the task sequence contains no elements, otherwise.
///
@@ -1476,7 +1486,19 @@ type TaskSeq =
static member zip: source1: TaskSeq<'T> -> source2: TaskSeq<'U> -> TaskSeq<'T * 'U>
///
- /// Applies the function to each element in the task sequence, threading an accumulator
+ /// Combines the three task sequences into a new task sequence of triples. The three 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 result task sequence of triples.
+ /// Thrown when any of the three input task sequences is null.
+ static member zip3:
+ source1: TaskSeq<'T1> -> source2: TaskSeq<'T2> -> source3: TaskSeq<'T3> -> TaskSeq<'T1 * 'T2 * 'T3>
+
+ ///
/// argument of type through the computation. If the input function is and the elements are
/// then computes.
/// If the accumulator function is asynchronous, consider using .
diff --git a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs
index b98d8178..e7f0e5f5 100644
--- a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs
+++ b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs
@@ -139,6 +139,14 @@ module internal TaskSeqInternal =
}
}
+ let replicate count value =
+ raiseCannotBeNegative (nameof count) count
+
+ taskSeq {
+ for _ in 1..count do
+ yield value
+ }
+
/// Returns length unconditionally, or based on a predicate
let lengthBy predicate (source: TaskSeq<_>) =
checkNonNull (nameof source) source
@@ -549,6 +557,29 @@ module internal TaskSeqInternal =
go <- step1 && step2
}
+ let zip3 (source1: TaskSeq<_>) (source2: TaskSeq<_>) (source3: TaskSeq<_>) =
+ checkNonNull (nameof source1) source1
+ checkNonNull (nameof source2) source2
+ checkNonNull (nameof source3) source3
+
+ taskSeq {
+ use e1 = source1.GetAsyncEnumerator CancellationToken.None
+ use e2 = source2.GetAsyncEnumerator CancellationToken.None
+ use e3 = source3.GetAsyncEnumerator CancellationToken.None
+ let mutable go = true
+ let! step1 = e1.MoveNextAsync()
+ let! step2 = e2.MoveNextAsync()
+ let! step3 = e3.MoveNextAsync()
+ go <- step1 && step2 && step3
+
+ while go do
+ yield e1.Current, e2.Current, e3.Current
+ let! step1 = e1.MoveNextAsync()
+ let! step2 = e2.MoveNextAsync()
+ let! step3 = e3.MoveNextAsync()
+ go <- step1 && step2 && step3
+ }
+
let collect (binder: _ -> #IAsyncEnumerable<_>) (source: TaskSeq<_>) =
checkNonNull (nameof source) source