Skip to content

Commit fbff564

Browse files
feat: add TaskSeq.replicate and TaskSeq.zip3 (60 tests, ref #289)
- TaskSeq.replicate: creates a task sequence containing a value n times - TaskSeq.zip3: combines three task sequences into triples, truncating to shortest - 13 tests for replicate (empty, negative count, correctness, reusability) - 47 tests for zip3 (null checks, empty, truncation, mixed types, side effects) - Update README.md: mark replicate and zip3 as implemented Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 43eff83 commit fbff564

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
@@ -217,7 +217,7 @@ The `TaskSeq` project already has a wide array of functions and functionalities,
217217
- [ ] `exists2` / `map2` / `fold2` / `iter2` and related '2'-functions
218218
- [ ] `mapFold`
219219
- [ ] `pairwise` / `allpairs` / `permute` / `distinct` / `distinctBy`
220-
- [ ] `replicate`
220+
- [x] `replicate`
221221
- [ ] `reduce` / `scan`
222222
- [ ] `unfold`
223223
- [x] Publish package on Nuget, **DONE, PUBLISHED SINCE: 7 November 2022**. See https://www.nuget.org/packages/FSharp.Control.TaskSeq
@@ -340,7 +340,7 @@ This is what has been implemented so far, is planned or skipped:
340340
| &#x1f6ab; | `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.") |
341341
| &#x2705; [#236][]| `removeAt` | `removeAt` | | |
342342
| &#x2705; [#236][]| `removeManyAt` | `removeManyAt` | | |
343-
| | `replicate` | `replicate` | | |
343+
| &#x2705; | `replicate` | `replicate` | | |
344344
| &#x2753; | `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.") |
345345
| | `scan` | `scan` | `scanAsync` | |
346346
| &#x1f6ab; | `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.") |
@@ -383,7 +383,7 @@ This is what has been implemented so far, is planned or skipped:
383383
| &#x2705; [#217][]| `where` | `where` | `whereAsync` | |
384384
| | `windowed` | `windowed` | | |
385385
| &#x2705; [#2][] | `zip` | `zip` | | |
386-
| | `zip3` | `zip3` | | |
386+
| &#x2705; | `zip3` | `zip3` | | |
387387
| | | `zip4` | | |
388388

389389

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
<Compile Include="TaskSeq.Pick.Tests.fs" />
4646
<Compile Include="TaskSeq.RemoveAt.Tests.fs" />
4747
<Compile Include="TaskSeq.Singleton.Tests.fs" />
48+
<Compile Include="TaskSeq.Replicate.Tests.fs" />
4849
<Compile Include="TaskSeq.Skip.Tests.fs" />
4950
<Compile Include="TaskSeq.SkipWhile.Tests.fs" />
5051
<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
@@ -22,6 +22,7 @@ type TaskSeq private () =
2222
// the 'private ()' ensure that a constructor is emitted, which is required by IL
2323

2424
static member singleton(value: 'T) = Internal.singleton value
25+
static member replicate count value = Internal.replicate count value
2526

2627
static member isEmpty source = Internal.isEmpty source
2728

@@ -408,6 +409,7 @@ type TaskSeq private () =
408409
//
409410

410411
static member zip source1 source2 = Internal.zip source1 source2
412+
static member zip3 source1 source2 source3 = Internal.zip3 source1 source2 source3
411413
static member fold folder state source = Internal.fold (FolderAction folder) state source
412414
static member foldAsync folder state source = Internal.fold (AsyncFolderAction folder) state source
413415
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
@@ -19,6 +19,16 @@ type TaskSeq =
1919
/// <param name="value">The input item to use as the single item of the task sequence.</param>
2020
static member singleton: value: 'T -> TaskSeq<'T>
2121

22+
/// <summary>
23+
/// Creates a task sequence by replicating <paramref name="value" /> a total of <paramref name="count" /> times.
24+
/// </summary>
25+
///
26+
/// <param name="count">The number of times to replicate the value.</param>
27+
/// <param name="value">The value to replicate.</param>
28+
/// <returns>A task sequence containing <paramref name="count" /> copies of <paramref name="value" />.</returns>
29+
/// <exception cref="T:ArgumentException">Thrown when <paramref name="count" /> is negative.</exception>
30+
static member replicate: count: int -> value: 'T -> TaskSeq<'T>
31+
2232
/// <summary>
2333
/// Returns <see cref="true" /> if the task sequence contains no elements, <see cref="false" /> otherwise.
2434
/// </summary>
@@ -1358,7 +1368,19 @@ type TaskSeq =
13581368
static member zip: source1: TaskSeq<'T> -> source2: TaskSeq<'U> -> TaskSeq<'T * 'U>
13591369

13601370
/// <summary>
1361-
/// Applies the function <paramref name="folder" /> to each element in the task sequence, threading an accumulator
1371+
/// Combines the three task sequences into a new task sequence of triples. The three sequences need not have equal lengths:
1372+
/// when one sequence is exhausted any remaining elements in the other sequences are ignored.
1373+
/// </summary>
1374+
///
1375+
/// <param name="source1">The first input task sequence.</param>
1376+
/// <param name="source2">The second input task sequence.</param>
1377+
/// <param name="source3">The third input task sequence.</param>
1378+
/// <returns>The result task sequence of triples.</returns>
1379+
/// <exception cref="T:ArgumentNullException">Thrown when any of the three input task sequences is null.</exception>
1380+
static member zip3:
1381+
source1: TaskSeq<'T1> -> source2: TaskSeq<'T2> -> source3: TaskSeq<'T3> -> TaskSeq<'T1 * 'T2 * 'T3>
1382+
1383+
/// <summary>
13621384
/// 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" />
13631385
/// then computes<paramref name="f (... (f s i0)...) iN" />.
13641386
/// 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
@@ -134,6 +134,14 @@ module internal TaskSeqInternal =
134134
}
135135
}
136136

137+
let replicate count value =
138+
raiseCannotBeNegative (nameof count) count
139+
140+
taskSeq {
141+
for _ in 1..count do
142+
yield value
143+
}
144+
137145
/// Returns length unconditionally, or based on a predicate
138146
let lengthBy predicate (source: TaskSeq<_>) =
139147
checkNonNull (nameof source) source
@@ -513,6 +521,29 @@ module internal TaskSeqInternal =
513521
go <- step1 && step2
514522
}
515523

524+
let zip3 (source1: TaskSeq<_>) (source2: TaskSeq<_>) (source3: TaskSeq<_>) =
525+
checkNonNull (nameof source1) source1
526+
checkNonNull (nameof source2) source2
527+
checkNonNull (nameof source3) source3
528+
529+
taskSeq {
530+
use e1 = source1.GetAsyncEnumerator CancellationToken.None
531+
use e2 = source2.GetAsyncEnumerator CancellationToken.None
532+
use e3 = source3.GetAsyncEnumerator CancellationToken.None
533+
let mutable go = true
534+
let! step1 = e1.MoveNextAsync()
535+
let! step2 = e2.MoveNextAsync()
536+
let! step3 = e3.MoveNextAsync()
537+
go <- step1 && step2 && step3
538+
539+
while go do
540+
yield e1.Current, e2.Current, e3.Current
541+
let! step1 = e1.MoveNextAsync()
542+
let! step2 = e2.MoveNextAsync()
543+
let! step3 = e3.MoveNextAsync()
544+
go <- step1 && step2 && step3
545+
}
546+
516547
let collect (binder: _ -> #IAsyncEnumerable<_>) (source: TaskSeq<_>) =
517548
checkNonNull (nameof source) source
518549

0 commit comments

Comments
 (0)