Skip to content

Commit 536f607

Browse files
feat: add TaskSeq.iteri2, iteri2Async, mapi2, mapi2Async
Implements the indexed-pair variants from the README roadmap: TaskSeq.iteri2 : (int -> 'T -> 'U -> unit) -> TaskSeq<'T> -> TaskSeq<'U> -> Task<unit> TaskSeq.iteri2Async : (int -> 'T -> 'U -> #Task<unit>) -> TaskSeq<'T> -> TaskSeq<'U> -> Task<unit> TaskSeq.mapi2 : (int -> 'T -> 'U -> 'V) -> TaskSeq<'T> -> TaskSeq<'U> -> TaskSeq<'V> TaskSeq.mapi2Async : (int -> 'T -> 'U -> #Task<'V>) -> TaskSeq<'T> -> TaskSeq<'U> -> TaskSeq<'V> Semantics: iteration stops at the shorter sequence (matching zip/map2/iter2). Files changed: - TaskSeqInternal.fs: iteri2, iteri2Async, mapi2, mapi2Async implementations - TaskSeq.fs: public static members - TaskSeq.fsi: XML-documented signatures after iteriAsync and mapiAsync - TaskSeq.Iteri2Mapi2.Tests.fs: new test file - FSharp.Control.TaskSeq.Test.fsproj: add test file reference - README.md: mark iteri2/iteri2Async and mapi2/mapi2Async as implemented - release-notes.txt: add entry under v0.6.0 Co-authored-by: Copilot <[email protected]>
1 parent 5a03593 commit 536f607

7 files changed

Lines changed: 472 additions & 2 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ This is what has been implemented so far, is planned or skipped:
307307
| &#x2705; [#2][] | `iter` | `iter` | `iterAsync` | |
308308
| | `iter2` | `iter2` | `iter2Async` | |
309309
| &#x2705; [#2][] | `iteri` | `iteri` | `iteriAsync` | |
310-
| | `iteri2` | `iteri2` | `iteri2Async` | |
310+
| | `iteri2` | `iteri2` | `iteri2Async` | |
311311
| &#x2705; [#23][] | `last` | `last` | | |
312312
| &#x2705; [#53][] | `length` | `length` | | |
313313
| &#x2705; [#53][] | | `lengthBy` | `lengthByAsync` | |
@@ -317,7 +317,7 @@ This is what has been implemented so far, is planned or skipped:
317317
| | `mapFold` | `mapFold` | `mapFoldAsync` | |
318318
| &#x1f6ab; | `mapFoldBack` | | | [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.") |
319319
| &#x2705; [#2][] | `mapi` | `mapi` | `mapiAsync` | |
320-
| | `mapi2` | `mapi2` | `mapi2Async` | |
320+
| | `mapi2` | `mapi2` | `mapi2Async` | |
321321
| &#x2705; [#221][]| `max` | `max` | | |
322322
| &#x2705; [#221][]| `maxBy` | `maxBy` | `maxByAsync` | |
323323
| &#x2705; [#221][]| `min` | `min` | | |

release-notes.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Release notes:
33

44
0.6.0
5+
- adds TaskSeq.iteri2, iteri2Async, mapi2, mapi2Async
56
- adds TaskSeq.scan and TaskSeq.scanAsync, #289
67
- adds TaskSeq.pairwise, #289
78
- adds TaskSeq.groupBy and TaskSeq.groupByAsync, #289

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
<Compile Include="TaskSeq.IsEmpty.fs" />
4141
<Compile Include="TaskSeq.Item.Tests.fs" />
4242
<Compile Include="TaskSeq.Iter.Tests.fs" />
43+
<Compile Include="TaskSeq.Iteri2Mapi2.Tests.fs" />
4344
<Compile Include="TaskSeq.Last.Tests.fs" />
4445
<Compile Include="TaskSeq.Length.Tests.fs" />
4546
<Compile Include="TaskSeq.Map.Tests.fs" />
Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
module TaskSeq.Tests.Iteri2Mapi2
2+
3+
open Xunit
4+
open FsUnit.Xunit
5+
6+
open FSharp.Control
7+
8+
//
9+
// TaskSeq.iteri2
10+
// TaskSeq.iteri2Async
11+
// TaskSeq.mapi2
12+
// TaskSeq.mapi2Async
13+
//
14+
15+
module EmptySeq =
16+
[<Fact>]
17+
let ``Null source is invalid for iteri2`` () =
18+
assertNullArg
19+
<| fun () -> TaskSeq.iteri2 (fun _ _ _ -> ()) null (TaskSeq.empty<int>)
20+
21+
assertNullArg
22+
<| fun () -> TaskSeq.iteri2 (fun _ _ _ -> ()) (TaskSeq.empty<int>) null
23+
24+
[<Fact>]
25+
let ``Null source is invalid for iteri2Async`` () =
26+
assertNullArg
27+
<| fun () -> TaskSeq.iteri2Async (fun _ _ _ -> Task.fromResult ()) null (TaskSeq.empty<int>)
28+
29+
assertNullArg
30+
<| fun () -> TaskSeq.iteri2Async (fun _ _ _ -> Task.fromResult ()) (TaskSeq.empty<int>) null
31+
32+
[<Fact>]
33+
let ``Null source is invalid for mapi2`` () =
34+
assertNullArg
35+
<| fun () -> TaskSeq.mapi2 (fun _ x _ -> x) null (TaskSeq.empty<int>)
36+
37+
assertNullArg
38+
<| fun () -> TaskSeq.mapi2 (fun _ x _ -> x) (TaskSeq.empty<int>) null
39+
40+
[<Fact>]
41+
let ``Null source is invalid for mapi2Async`` () =
42+
assertNullArg
43+
<| fun () -> TaskSeq.mapi2Async (fun _ x _ -> Task.fromResult x) null (TaskSeq.empty<int>)
44+
45+
assertNullArg
46+
<| fun () -> TaskSeq.mapi2Async (fun _ x _ -> Task.fromResult x) (TaskSeq.empty<int>) null
47+
48+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
49+
let ``TaskSeq-iteri2 does nothing when first source is empty`` variant = task {
50+
let tq = Gen.getEmptyVariant variant
51+
let mutable count = 0
52+
do! TaskSeq.iteri2 (fun _ _ _ -> count <- count + 1) tq (TaskSeq.ofSeq [ 1; 2; 3 ])
53+
count |> should equal 0
54+
}
55+
56+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
57+
let ``TaskSeq-iteri2 does nothing when second source is empty`` variant = task {
58+
let tq = Gen.getEmptyVariant variant
59+
let mutable count = 0
60+
do! TaskSeq.iteri2 (fun _ _ _ -> count <- count + 1) (TaskSeq.ofSeq [ 1; 2; 3 ]) tq
61+
count |> should equal 0
62+
}
63+
64+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
65+
let ``TaskSeq-mapi2 returns empty when first source is empty`` variant =
66+
TaskSeq.mapi2 (fun i x y -> i + x + y) (Gen.getEmptyVariant variant) (TaskSeq.ofSeq [ 1..10 ])
67+
|> verifyEmpty
68+
69+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
70+
let ``TaskSeq-mapi2 returns empty when second source is empty`` variant =
71+
TaskSeq.mapi2 (fun i x y -> i + x + y) (TaskSeq.ofSeq [ 1..10 ]) (Gen.getEmptyVariant variant)
72+
|> verifyEmpty
73+
74+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
75+
let ``TaskSeq-mapi2Async returns empty when first source is empty`` variant =
76+
TaskSeq.mapi2Async (fun i x y -> task { return i + x + y }) (Gen.getEmptyVariant variant) (TaskSeq.ofSeq [ 1..10 ])
77+
|> verifyEmpty
78+
79+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
80+
let ``TaskSeq-mapi2Async returns empty when second source is empty`` variant =
81+
TaskSeq.mapi2Async (fun i x y -> task { return i + x + y }) (TaskSeq.ofSeq [ 1..10 ]) (Gen.getEmptyVariant variant)
82+
|> verifyEmpty
83+
84+
85+
module Immutable =
86+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
87+
let ``TaskSeq-iteri2 visits all elements of equal-length sequences`` variant = task {
88+
let tq = Gen.getSeqImmutable variant
89+
let mutable sum = 0
90+
do! TaskSeq.iteri2 (fun _ x y -> sum <- sum + x + y) tq (TaskSeq.ofSeq [ 1..10 ])
91+
sum |> should equal 110 // (1..10) + (1..10) = 55 + 55
92+
}
93+
94+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
95+
let ``TaskSeq-iteri2 passes correct zero-based indices`` variant = task {
96+
let tq = Gen.getSeqImmutable variant
97+
let mutable indexSum = 0
98+
do! TaskSeq.iteri2 (fun i _ _ -> indexSum <- indexSum + i) tq (TaskSeq.ofSeq [ 1..10 ])
99+
indexSum |> should equal 45 // 0+1+2+...+9
100+
}
101+
102+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
103+
let ``TaskSeq-iteri2Async visits all elements of equal-length sequences`` variant = task {
104+
let tq = Gen.getSeqImmutable variant
105+
let mutable sum = 0
106+
107+
do! TaskSeq.iteri2Async (fun _ x y -> task { sum <- sum + x + y }) tq (TaskSeq.ofSeq [ 1..10 ])
108+
109+
sum |> should equal 110
110+
}
111+
112+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
113+
let ``TaskSeq-iteri2Async passes correct zero-based indices`` variant = task {
114+
let tq = Gen.getSeqImmutable variant
115+
let mutable indexSum = 0
116+
117+
do! TaskSeq.iteri2Async (fun i _ _ -> task { indexSum <- indexSum + i }) tq (TaskSeq.ofSeq [ 1..10 ])
118+
119+
indexSum |> should equal 45
120+
}
121+
122+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
123+
let ``TaskSeq-mapi2 maps in correct order with correct indices`` variant = task {
124+
let tq = Gen.getSeqImmutable variant
125+
let results = ResizeArray()
126+
127+
do!
128+
TaskSeq.mapi2 (fun i x y -> (i, x, y)) tq (TaskSeq.ofSeq [ 10..19 ])
129+
|> TaskSeq.iter (fun t -> results.Add t)
130+
131+
let indices, xs, ys = results |> Seq.toList |> List.unzip3
132+
133+
indices |> should equal [ 0..9 ]
134+
xs |> should equal [ 1..10 ]
135+
ys |> should equal [ 10..19 ]
136+
}
137+
138+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
139+
let ``TaskSeq-mapi2Async maps in correct order with correct indices`` variant = task {
140+
let tq = Gen.getSeqImmutable variant
141+
let results = ResizeArray()
142+
143+
do!
144+
TaskSeq.mapi2Async (fun i x y -> task { return (i, x, y) }) tq (TaskSeq.ofSeq [ 10..19 ])
145+
|> TaskSeq.iter (fun t -> results.Add t)
146+
147+
let indices, xs, ys = results |> Seq.toList |> List.unzip3
148+
149+
indices |> should equal [ 0..9 ]
150+
xs |> should equal [ 1..10 ]
151+
ys |> should equal [ 10..19 ]
152+
}
153+
154+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
155+
let ``TaskSeq-mapi2 using heterogeneous element types`` variant = task {
156+
let tq = Gen.getSeqImmutable variant
157+
158+
let result =
159+
TaskSeq.mapi2
160+
(fun i x (s: string) -> sprintf "%d:%d:%s" i x s)
161+
tq
162+
(TaskSeq.ofSeq [ "a"; "b"; "c"; "d"; "e"; "f"; "g"; "h"; "i"; "j" ])
163+
164+
let! lst = result |> TaskSeq.toListAsync
165+
166+
lst
167+
|> should equal [ "0:1:a"; "1:2:b"; "2:3:c"; "3:4:d"; "4:5:e"; "5:6:f"; "6:7:g"; "7:8:h"; "8:9:i"; "9:10:j" ]
168+
}
169+
170+
171+
module Truncation =
172+
[<Fact>]
173+
let ``TaskSeq-iteri2 stops at the shorter first sequence`` () = task {
174+
let mutable count = 0
175+
do! TaskSeq.iteri2 (fun _ _ _ -> count <- count + 1) (TaskSeq.ofSeq [ 1..3 ]) (TaskSeq.ofSeq [ 1..10 ])
176+
count |> should equal 3
177+
}
178+
179+
[<Fact>]
180+
let ``TaskSeq-iteri2 stops at the shorter second sequence`` () = task {
181+
let mutable count = 0
182+
do! TaskSeq.iteri2 (fun _ _ _ -> count <- count + 1) (TaskSeq.ofSeq [ 1..10 ]) (TaskSeq.ofSeq [ 1..4 ])
183+
count |> should equal 4
184+
}
185+
186+
[<Fact>]
187+
let ``TaskSeq-iteri2Async stops at the shorter first sequence`` () = task {
188+
let mutable count = 0
189+
190+
do! TaskSeq.iteri2Async (fun _ _ _ -> task { count <- count + 1 }) (TaskSeq.ofSeq [ 1..3 ]) (TaskSeq.ofSeq [ 1..10 ])
191+
192+
count |> should equal 3
193+
}
194+
195+
[<Fact>]
196+
let ``TaskSeq-iteri2Async stops at the shorter second sequence`` () = task {
197+
let mutable count = 0
198+
199+
do! TaskSeq.iteri2Async (fun _ _ _ -> task { count <- count + 1 }) (TaskSeq.ofSeq [ 1..10 ]) (TaskSeq.ofSeq [ 1..4 ])
200+
201+
count |> should equal 4
202+
}
203+
204+
[<Fact>]
205+
let ``TaskSeq-mapi2 stops at the shorter first sequence`` () = task {
206+
let result = TaskSeq.mapi2 (fun i x y -> i + x + y) (TaskSeq.ofSeq [ 1..3 ]) (TaskSeq.ofSeq [ 1..10 ])
207+
208+
let! lst = result |> TaskSeq.toListAsync
209+
lst |> should equal [ 2; 5; 8 ] // (0+1+1), (1+2+2), (2+3+3)
210+
}
211+
212+
[<Fact>]
213+
let ``TaskSeq-mapi2 stops at the shorter second sequence`` () = task {
214+
let result = TaskSeq.mapi2 (fun i x y -> i + x + y) (TaskSeq.ofSeq [ 1..10 ]) (TaskSeq.ofSeq [ 1..4 ])
215+
216+
let! lst = result |> TaskSeq.toListAsync
217+
lst |> should equal [ 2; 5; 8; 11 ] // (0+1+1), (1+2+2), (2+3+3), (3+4+4)
218+
}
219+
220+
[<Fact>]
221+
let ``TaskSeq-mapi2Async stops at the shorter first sequence`` () = task {
222+
let result = TaskSeq.mapi2Async (fun i x y -> task { return i + x + y }) (TaskSeq.ofSeq [ 1..3 ]) (TaskSeq.ofSeq [ 1..10 ])
223+
224+
let! lst = result |> TaskSeq.toListAsync
225+
lst |> should equal [ 2; 5; 8 ]
226+
}
227+
228+
[<Fact>]
229+
let ``TaskSeq-mapi2Async stops at the shorter second sequence`` () = task {
230+
let result = TaskSeq.mapi2Async (fun i x y -> task { return i + x + y }) (TaskSeq.ofSeq [ 1..10 ]) (TaskSeq.ofSeq [ 1..4 ])
231+
232+
let! lst = result |> TaskSeq.toListAsync
233+
lst |> should equal [ 2; 5; 8; 11 ]
234+
}
235+
236+
[<Fact>]
237+
let ``TaskSeq-iteri2 index is always zero-based and matches iteration count even with truncation`` () = task {
238+
let indices = ResizeArray()
239+
do! TaskSeq.iteri2 (fun i _ _ -> indices.Add i) (TaskSeq.ofSeq [ 1..5 ]) (TaskSeq.ofSeq [ 10..12 ])
240+
indices |> Seq.toList |> should equal [ 0; 1; 2 ]
241+
}
242+
243+
[<Fact>]
244+
let ``TaskSeq-mapi2 index is always zero-based and matches iteration count even with truncation`` () = task {
245+
let result = TaskSeq.mapi2 (fun i _ _ -> i) (TaskSeq.ofSeq [ 1..5 ]) (TaskSeq.ofSeq [ 10..12 ])
246+
247+
let! lst = result |> TaskSeq.toListAsync
248+
lst |> should equal [ 0; 1; 2 ]
249+
}
250+
251+
252+
module SideEffects =
253+
[<Fact>]
254+
let ``TaskSeq-mapi2 is lazy and does not evaluate until iterated`` () =
255+
let mutable sideEffect = 0
256+
257+
let ts1 = taskSeq {
258+
sideEffect <- sideEffect + 1
259+
yield 1
260+
sideEffect <- sideEffect + 1
261+
yield 2
262+
}
263+
264+
let ts2 = TaskSeq.ofSeq [ 10; 20 ]
265+
266+
// building the mapped sequence should not evaluate anything
267+
let _ = TaskSeq.mapi2 (fun i x y -> (i, x, y)) ts1 ts2
268+
sideEffect |> should equal 0
269+
270+
[<Fact>]
271+
let ``TaskSeq-iteri2 evaluates side effects in both sequences`` () = task {
272+
let mutable s1 = 0
273+
let mutable s2 = 0
274+
275+
let ts1 = taskSeq {
276+
s1 <- s1 + 1
277+
yield 1
278+
s1 <- s1 + 1
279+
yield 2
280+
}
281+
282+
let ts2 = taskSeq {
283+
s2 <- s2 + 1
284+
yield 10
285+
s2 <- s2 + 1
286+
yield 20
287+
}
288+
289+
do! TaskSeq.iteri2 (fun _ _ _ -> ()) ts1 ts2
290+
s1 |> should equal 2
291+
s2 |> should equal 2
292+
}
293+
294+
[<Fact>]
295+
let ``TaskSeq-iteri2Async evaluates side effects in both sequences`` () = task {
296+
let mutable s1 = 0
297+
let mutable s2 = 0
298+
299+
let ts1 = taskSeq {
300+
s1 <- s1 + 1
301+
yield 1
302+
s1 <- s1 + 1
303+
yield 2
304+
}
305+
306+
let ts2 = taskSeq {
307+
s2 <- s2 + 1
308+
yield 10
309+
s2 <- s2 + 1
310+
yield 20
311+
}
312+
313+
do! TaskSeq.iteri2Async (fun _ _ _ -> task { () }) ts1 ts2
314+
s1 |> should equal 2
315+
s2 |> should equal 2
316+
}

src/FSharp.Control.TaskSeq/TaskSeq.fs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,10 +369,14 @@ type TaskSeq private () =
369369
static member iteri action source = Internal.iter (CountableAction action) source
370370
static member iterAsync action source = Internal.iter (AsyncSimpleAction action) source
371371
static member iteriAsync action source = Internal.iter (AsyncCountableAction action) source
372+
static member iteri2 action source1 source2 = Internal.iteri2 action source1 source2
373+
static member iteri2Async action source1 source2 = Internal.iteri2Async action source1 source2
372374
static member map (mapper: 'T -> 'U) source = Internal.map (SimpleAction mapper) source
373375
static member mapi (mapper: int -> 'T -> 'U) source = Internal.map (CountableAction mapper) source
374376
static member mapAsync mapper source = Internal.map (AsyncSimpleAction mapper) source
375377
static member mapiAsync mapper source = Internal.map (AsyncCountableAction mapper) source
378+
static member mapi2 mapper source1 source2 = Internal.mapi2 mapper source1 source2
379+
static member mapi2Async mapper source1 source2 = Internal.mapi2Async mapper source1 source2
376380
static member collect (binder: 'T -> #TaskSeq<'U>) source = Internal.collect binder source
377381
static member collectSeq (binder: 'T -> #seq<'U>) source = Internal.collectSeq binder source
378382
static member collectAsync (binder: 'T -> #Task<#TaskSeq<'U>>) source : TaskSeq<'U> = Internal.collectAsync binder source

0 commit comments

Comments
 (0)