Skip to content

Commit f4c6fb0

Browse files
authored
Merge pull request #308 from fsprojects/repo-assist/feat-replicate-zip3-2026-03-6cd05ed182dd3f33
[Repo Assist] feat: add TaskSeq.replicate and TaskSeq.zip3 (60 tests)
2 parents 329fd86 + 5fd6062 commit f4c6fb0

File tree

7 files changed

+254
-4
lines changed

7 files changed

+254
-4
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ The `TaskSeq` project already has a wide array of functions and functionalities,
218218
- [ ] `mapFold`
219219
- [x] `pairwise` (see [#293])
220220
- [ ] `allpairs` / `permute` / `distinct` / `distinctBy`
221-
- [ ] `replicate`
221+
- [x] `replicate`
222222
- [x] `reduce` / `scan` (see [#299], [#296])
223223
- [x] `unfold` (see [#300])
224224
- [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:
341341
| 🚫 | `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.") |
342342
| ✅ [#236][]| `removeAt` | `removeAt` | | |
343343
| ✅ [#236][]| `removeManyAt` | `removeManyAt` | | |
344-
| | `replicate` | `replicate` | | |
344+
| ✅ | `replicate` | `replicate` | | |
345345
| ❓ | `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.") |
346346
| ✅ [#296][] | `scan` | `scan` | `scanAsync` | |
347347
| 🚫 | `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:
384384
| ✅ [#217][]| `where` | `where` | `whereAsync` | |
385385
| ✅ [#258][] | `windowed` | `windowed` | | |
386386
| ✅ [#2][] | `zip` | `zip` | | |
387-
| | `zip3` | `zip3` | | |
387+
| ✅ | `zip3` | `zip3` | | |
388388
| | | `zip4` | | |
389389

390390

src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
<Compile Include="TaskSeq.Pick.Tests.fs" />
5050
<Compile Include="TaskSeq.RemoveAt.Tests.fs" />
5151
<Compile Include="TaskSeq.Singleton.Tests.fs" />
52+
<Compile Include="TaskSeq.Replicate.Tests.fs" />
5253
<Compile Include="TaskSeq.Skip.Tests.fs" />
5354
<Compile Include="TaskSeq.SkipWhile.Tests.fs" />
5455
<Compile Include="TaskSeq.Tail.Tests.fs" />
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
module TaskSeq.Tests.Replicate
2+
3+
open System
4+
5+
open Xunit
6+
open FsUnit.Xunit
7+
8+
open FSharp.Control
9+
10+
//
11+
// TaskSeq.replicate
12+
//
13+
14+
module EmptySeq =
15+
[<Fact>]
16+
let ``TaskSeq-replicate with count 0 gives empty sequence`` () = TaskSeq.replicate 0 42 |> verifyEmpty
17+
18+
[<Fact>]
19+
let ``TaskSeq-replicate with negative count gives an error`` () =
20+
fun () ->
21+
TaskSeq.replicate -1 42
22+
|> TaskSeq.toArrayAsync
23+
|> Task.ignore
24+
25+
|> should throwAsyncExact typeof<ArgumentException>
26+
27+
fun () ->
28+
TaskSeq.replicate Int32.MinValue "hello"
29+
|> TaskSeq.toArrayAsync
30+
|> Task.ignore
31+
32+
|> should throwAsyncExact typeof<ArgumentException>
33+
34+
module Immutable =
35+
[<Fact>]
36+
let ``TaskSeq-replicate produces the correct count and value`` () = task {
37+
let! arr = TaskSeq.replicate 5 99 |> TaskSeq.toArrayAsync
38+
arr |> should haveLength 5
39+
arr |> should equal [| 99; 99; 99; 99; 99 |]
40+
}
41+
42+
[<Fact>]
43+
let ``TaskSeq-replicate with count 1 produces a singleton`` () = task {
44+
let! arr = TaskSeq.replicate 1 "x" |> TaskSeq.toArrayAsync
45+
arr |> should haveLength 1
46+
arr[0] |> should equal "x"
47+
}
48+
49+
[<Fact>]
50+
let ``TaskSeq-replicate with large count`` () = task {
51+
let count = 10_000
52+
let! arr = TaskSeq.replicate count 7 |> TaskSeq.toArrayAsync
53+
arr |> should haveLength count
54+
arr |> Array.forall ((=) 7) |> should be True
55+
}
56+
57+
[<Fact>]
58+
let ``TaskSeq-replicate works with null as value`` () = task {
59+
let! arr = TaskSeq.replicate 3 null |> TaskSeq.toArrayAsync
60+
arr |> should haveLength 3
61+
arr |> Array.forall (fun x -> x = null) |> should be True
62+
}
63+
64+
[<Fact>]
65+
let ``TaskSeq-replicate can be consumed multiple times`` () = task {
66+
let ts = TaskSeq.replicate 4 "a"
67+
let! arr1 = ts |> TaskSeq.toArrayAsync
68+
let! arr2 = ts |> TaskSeq.toArrayAsync
69+
arr1 |> should equal arr2
70+
}
71+
72+
module SideEffects =
73+
[<Fact>]
74+
let ``TaskSeq-replicate with a mutable value captures the value, not a reference`` () = task {
75+
let mutable x = 1
76+
let ts = TaskSeq.replicate 3 x
77+
x <- 999
78+
let! arr = ts |> TaskSeq.toArrayAsync
79+
// replicate captures the value at call time (value type)
80+
arr |> should equal [| 1; 1; 1 |]
81+
}

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

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,3 +191,116 @@ module Other =
191191

192192
combined |> should equal [| ("one", 42L); ("two", 43L) |]
193193
}
194+
195+
//
196+
// TaskSeq.zip3
197+
//
198+
199+
module EmptySeqZip3 =
200+
[<Fact>]
201+
let ``Null source is invalid for zip3`` () =
202+
assertNullArg
203+
<| fun () -> TaskSeq.zip3 null TaskSeq.empty TaskSeq.empty
204+
205+
assertNullArg
206+
<| fun () -> TaskSeq.zip3 TaskSeq.empty null TaskSeq.empty
207+
208+
assertNullArg
209+
<| fun () -> TaskSeq.zip3 TaskSeq.empty TaskSeq.empty null
210+
211+
assertNullArg <| fun () -> TaskSeq.zip3 null null null
212+
213+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
214+
let ``TaskSeq-zip3 can zip empty sequences`` variant =
215+
TaskSeq.zip3 (Gen.getEmptyVariant variant) (Gen.getEmptyVariant variant) (Gen.getEmptyVariant variant)
216+
|> verifyEmpty
217+
218+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
219+
let ``TaskSeq-zip3 stops at first exhausted sequence`` variant =
220+
// second and third are non-empty, first is empty → result is empty
221+
TaskSeq.zip3 (Gen.getEmptyVariant variant) (taskSeq { yield 1 }) (taskSeq { yield 2 })
222+
|> verifyEmpty
223+
224+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
225+
let ``TaskSeq-zip3 stops when second sequence is empty`` variant =
226+
TaskSeq.zip3 (taskSeq { yield 1 }) (Gen.getEmptyVariant variant) (taskSeq { yield 2 })
227+
|> verifyEmpty
228+
229+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
230+
let ``TaskSeq-zip3 stops when third sequence is empty`` variant =
231+
TaskSeq.zip3 (taskSeq { yield 1 }) (taskSeq { yield 2 }) (Gen.getEmptyVariant variant)
232+
|> verifyEmpty
233+
234+
module ImmutableZip3 =
235+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
236+
let ``TaskSeq-zip3 zips in correct order`` variant = task {
237+
let one = Gen.getSeqImmutable variant
238+
let two = Gen.getSeqImmutable variant
239+
let three = Gen.getSeqImmutable variant
240+
let! combined = TaskSeq.zip3 one two three |> TaskSeq.toArrayAsync
241+
242+
combined |> should haveLength 10
243+
244+
combined
245+
|> should equal (Array.init 10 (fun x -> x + 1, x + 1, x + 1))
246+
}
247+
248+
[<Fact>]
249+
let ``TaskSeq-zip3 produces correct triples with mixed types`` () = task {
250+
let one = taskSeq {
251+
yield "a"
252+
yield "b"
253+
}
254+
255+
let two = taskSeq {
256+
yield 1
257+
yield 2
258+
}
259+
260+
let three = taskSeq {
261+
yield true
262+
yield false
263+
}
264+
265+
let! combined = TaskSeq.zip3 one two three |> TaskSeq.toArrayAsync
266+
267+
combined
268+
|> should equal [| ("a", 1, true); ("b", 2, false) |]
269+
}
270+
271+
[<Fact>]
272+
let ``TaskSeq-zip3 truncates to shortest sequence`` () = task {
273+
let one = taskSeq { yield! [ 1..10 ] }
274+
let two = taskSeq { yield! [ 1..5 ] }
275+
let three = taskSeq { yield! [ 1..3 ] }
276+
let! combined = TaskSeq.zip3 one two three |> TaskSeq.toArrayAsync
277+
278+
combined |> should haveLength 3
279+
280+
combined
281+
|> should equal [| (1, 1, 1); (2, 2, 2); (3, 3, 3) |]
282+
}
283+
284+
[<Fact>]
285+
let ``TaskSeq-zip3 works with a single-element sequences`` () = task {
286+
let! combined =
287+
TaskSeq.zip3 (TaskSeq.singleton 1) (TaskSeq.singleton "x") (TaskSeq.singleton true)
288+
|> TaskSeq.toArrayAsync
289+
290+
combined |> should equal [| (1, "x", true) |]
291+
}
292+
293+
module SideEffectsZip3 =
294+
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
295+
let ``TaskSeq-zip3 can deal with side effects in sequences`` variant = task {
296+
let one = Gen.getSeqWithSideEffect variant
297+
let two = Gen.getSeqWithSideEffect variant
298+
let three = Gen.getSeqWithSideEffect variant
299+
let! combined = TaskSeq.zip3 one two three |> TaskSeq.toArrayAsync
300+
301+
combined
302+
|> Array.forall (fun (x, y, z) -> x = y && y = z)
303+
|> should be True
304+
305+
combined |> should haveLength 10
306+
}

src/FSharp.Control.TaskSeq/TaskSeq.fs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ type TaskSeq private () =
123123
// the 'private ()' ensure that a constructor is emitted, which is required by IL
124124

125125
static member singleton(value: 'T) = Internal.singleton value
126+
static member replicate count value = Internal.replicate count value
126127

127128
static member isEmpty source = Internal.isEmpty source
128129

@@ -510,6 +511,7 @@ type TaskSeq private () =
510511
//
511512

512513
static member zip source1 source2 = Internal.zip source1 source2
514+
static member zip3 source1 source2 source3 = Internal.zip3 source1 source2 source3
513515
static member fold folder state source = Internal.fold (FolderAction folder) state source
514516
static member foldAsync folder state source = Internal.fold (AsyncFolderAction folder) state source
515517
static member scan folder state source = Internal.scan (FolderAction folder) state source

src/FSharp.Control.TaskSeq/TaskSeq.fsi

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,16 @@ type TaskSeq =
103103
/// <param name="value">The input item to use as the single item of the task sequence.</param>
104104
static member singleton: value: 'T -> TaskSeq<'T>
105105

106+
/// <summary>
107+
/// Creates a task sequence by replicating <paramref name="value" /> a total of <paramref name="count" /> times.
108+
/// </summary>
109+
///
110+
/// <param name="count">The number of times to replicate the value.</param>
111+
/// <param name="value">The value to replicate.</param>
112+
/// <returns>A task sequence containing <paramref name="count" /> copies of <paramref name="value" />.</returns>
113+
/// <exception cref="T:ArgumentException">Thrown when <paramref name="count" /> is negative.</exception>
114+
static member replicate: count: int -> value: 'T -> TaskSeq<'T>
115+
106116
/// <summary>
107117
/// Returns <see cref="true" /> if the task sequence contains no elements, <see cref="false" /> otherwise.
108118
/// </summary>
@@ -1532,7 +1542,19 @@ type TaskSeq =
15321542
static member zip: source1: TaskSeq<'T> -> source2: TaskSeq<'U> -> TaskSeq<'T * 'U>
15331543

15341544
/// <summary>
1535-
/// Applies the function <paramref name="folder" /> to each element in the task sequence, threading an accumulator
1545+
/// Combines the three task sequences into a new task sequence of triples. The three sequences need not have equal lengths:
1546+
/// when one sequence is exhausted any remaining elements in the other sequences are ignored.
1547+
/// </summary>
1548+
///
1549+
/// <param name="source1">The first input task sequence.</param>
1550+
/// <param name="source2">The second input task sequence.</param>
1551+
/// <param name="source3">The third input task sequence.</param>
1552+
/// <returns>The result task sequence of triples.</returns>
1553+
/// <exception cref="T:ArgumentNullException">Thrown when any of the three input task sequences is null.</exception>
1554+
static member zip3:
1555+
source1: TaskSeq<'T1> -> source2: TaskSeq<'T2> -> source3: TaskSeq<'T3> -> TaskSeq<'T1 * 'T2 * 'T3>
1556+
1557+
/// <summary>
15361558
/// 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" />
15371559
/// then computes<paramref name="f (... (f s i0)...) iN" />.
15381560
/// If the accumulator function <paramref name="folder" /> is asynchronous, consider using <see cref="TaskSeq.foldAsync" />.

src/FSharp.Control.TaskSeq/TaskSeqInternal.fs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,14 @@ module internal TaskSeqInternal =
144144
}
145145
}
146146

147+
let replicate count value =
148+
raiseCannotBeNegative (nameof count) count
149+
150+
taskSeq {
151+
for _ in 1..count do
152+
yield value
153+
}
154+
147155
/// Returns length unconditionally, or based on a predicate
148156
let lengthBy predicate (source: TaskSeq<_>) =
149157
checkNonNull (nameof source) source
@@ -554,6 +562,29 @@ module internal TaskSeqInternal =
554562
go <- step1 && step2
555563
}
556564

565+
let zip3 (source1: TaskSeq<_>) (source2: TaskSeq<_>) (source3: TaskSeq<_>) =
566+
checkNonNull (nameof source1) source1
567+
checkNonNull (nameof source2) source2
568+
checkNonNull (nameof source3) source3
569+
570+
taskSeq {
571+
use e1 = source1.GetAsyncEnumerator CancellationToken.None
572+
use e2 = source2.GetAsyncEnumerator CancellationToken.None
573+
use e3 = source3.GetAsyncEnumerator CancellationToken.None
574+
let mutable go = true
575+
let! step1 = e1.MoveNextAsync()
576+
let! step2 = e2.MoveNextAsync()
577+
let! step3 = e3.MoveNextAsync()
578+
go <- step1 && step2 && step3
579+
580+
while go do
581+
yield e1.Current, e2.Current, e3.Current
582+
let! step1 = e1.MoveNextAsync()
583+
let! step2 = e2.MoveNextAsync()
584+
let! step3 = e3.MoveNextAsync()
585+
go <- step1 && step2 && step3
586+
}
587+
557588
let collect (binder: _ -> #IAsyncEnumerable<_>) (source: TaskSeq<_>) =
558589
checkNonNull (nameof source) source
559590

0 commit comments

Comments
 (0)