Skip to content

Commit 9e7e535

Browse files
Repo AssistCopilot
authored andcommitted
feat: add TaskSeq.chunkBySize and TaskSeq.windowed (closes #258, ref #289)
- TaskSeq.chunkBySize: divides a task sequence into non-overlapping chunks of at most chunkSize elements. Uses a fixed-size array buffer (vs. ResizeArray) to avoid intermediate allocations and resizing. - TaskSeq.windowed: returns overlapping sliding windows of a fixed size. Uses a ring buffer internally so that only a single allocation (per window) is needed; no redundant element copies on each step. Both functions validate their size argument eagerly (before enumeration starts), raise ArgumentException for non-positive sizes, and are fully documented in the .fsi signature file. Also: - Update README.md to mark chunkBySize, windowed, pairwise, scan/scanAsync, reduce/reduceAsync, and unfold/unfoldAsync as implemented (these were merged in PRs #293, #296, #299, #300 respectively). - Update release-notes.txt for 0.5.0. - 171 new tests across TaskSeq.ChunkBySize.Tests.fs and TaskSeq.Windowed.Tests.fs. All 4021 existing tests continue to pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 43eff83 commit 9e7e535

File tree

8 files changed

+524
-10
lines changed

8 files changed

+524
-10
lines changed

README.md

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -211,15 +211,16 @@ The `TaskSeq` project already has a wide array of functions and functionalities,
211211
- [ ] `average` / `averageBy`, `sum` and related
212212
- [x] `forall` / `forallAsync` (see [#240])
213213
- [x] `skip` / `drop` / `truncate` / `take` (see [#209])
214-
- [ ] `chunkBySize` / `windowed`
214+
- [x] `chunkBySize` / `windowed` (see [#258])
215215
- [ ] `compareWith`
216216
- [ ] `distinct`
217217
- [ ] `exists2` / `map2` / `fold2` / `iter2` and related '2'-functions
218218
- [ ] `mapFold`
219-
- [ ] `pairwise` / `allpairs` / `permute` / `distinct` / `distinctBy`
219+
- [x] `pairwise` (see [#293])
220+
- [ ] `allpairs` / `permute` / `distinct` / `distinctBy`
220221
- [ ] `replicate`
221-
- [ ] `reduce` / `scan`
222-
- [ ] `unfold`
222+
- [x] `reduce` / `scan` (see [#299], [#296])
223+
- [x] `unfold` (see [#300])
223224
- [x] Publish package on Nuget, **DONE, PUBLISHED SINCE: 7 November 2022**. See https://www.nuget.org/packages/FSharp.Control.TaskSeq
224225
- [x] Make `TaskSeq` interoperable with `Task` by expanding the latter with a `for .. in .. do` that acceps task sequences
225226
- [x] Add to/from functions to seq, list, array
@@ -263,7 +264,7 @@ This is what has been implemented so far, is planned or skipped:
263264
| &#x2705; [#67][] | | | `box` | |
264265
| &#x2705; [#67][] | | | `unbox` | |
265266
| &#x2705; [#23][] | `choose` | `choose` | `chooseAsync` | |
266-
| | `chunkBySize` | `chunkBySize` | | |
267+
| &#x2705; [#258][] | `chunkBySize` | `chunkBySize` | | |
267268
| &#x2705; [#11][] | `collect` | `collect` | `collectAsync` | |
268269
| &#x2705; [#11][] | | `collectSeq` | `collectSeqAsync` | |
269270
| | `compareWith` | `compareWith` | `compareWithAsync` | |
@@ -332,17 +333,17 @@ This is what has been implemented so far, is planned or skipped:
332333
| &#x2705; [#2][] | | `ofTaskArray` | | |
333334
| &#x2705; [#2][] | | `ofTaskList` | | |
334335
| &#x2705; [#2][] | | `ofTaskSeq` | | |
335-
| | `pairwise` | `pairwise` | | |
336+
| &#x2705; [#293][] | `pairwise` | `pairwise` | | |
336337
| | `permute` | `permute` | `permuteAsync` | |
337338
| &#x2705; [#23][] | `pick` | `pick` | `pickAsync` | |
338339
| &#x1f6ab; | `readOnly` | | | [note #3](#note3 "The motivation for 'readOnly' in 'Seq' is that a cast from a mutable array or list to a 'seq<_>' is valid and can be cast back, leading to a mutable sequence. Since 'TaskSeq' doesn't implement 'IEnumerable<_>', such casts are not possible.") |
339-
| | `reduce` | `reduce` | `reduceAsync` | |
340+
| &#x2705; [#299][] | `reduce` | `reduce` | `reduceAsync` | |
340341
| &#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.") |
341342
| &#x2705; [#236][]| `removeAt` | `removeAt` | | |
342343
| &#x2705; [#236][]| `removeManyAt` | `removeManyAt` | | |
343344
| | `replicate` | `replicate` | | |
344345
| &#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.") |
345-
| | `scan` | `scan` | `scanAsync` | |
346+
| &#x2705; [#296][] | `scan` | `scan` | `scanAsync` | |
346347
| &#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.") |
347348
| &#x2705; [#90][] | `singleton` | `singleton` | | |
348349
| &#x2705; [#209][]| `skip` | `skip` | | |
@@ -378,10 +379,10 @@ This is what has been implemented so far, is planned or skipped:
378379
| &#x2705; [#23][] | `tryLast` | `tryLast` | | |
379380
| &#x2705; [#23][] | `tryPick` | `tryPick` | `tryPickAsync` | |
380381
| &#x2705; [#76][] | | `tryTail` | | |
381-
| | `unfold` | `unfold` | `unfoldAsync` | |
382+
| &#x2705; [#300][] | `unfold` | `unfold` | `unfoldAsync` | |
382383
| &#x2705; [#236][]| `updateAt` | `updateAt` | | |
383384
| &#x2705; [#217][]| `where` | `where` | `whereAsync` | |
384-
| | `windowed` | `windowed` | | |
385+
| &#x2705; [#258][] | `windowed` | `windowed` | | |
385386
| &#x2705; [#2][] | `zip` | `zip` | | |
386387
| | `zip3` | `zip3` | | |
387388
| | | `zip4` | | |
@@ -653,6 +654,11 @@ module TaskSeq =
653654
[#237]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/237
654655
[#236]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/236
655656
[#240]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/240
657+
[#258]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/258
658+
[#293]: https://github.com/fsprojects/FSharp.Control.TaskSeq/pull/293
659+
[#296]: https://github.com/fsprojects/FSharp.Control.TaskSeq/pull/296
660+
[#299]: https://github.com/fsprojects/FSharp.Control.TaskSeq/pull/299
661+
[#300]: https://github.com/fsprojects/FSharp.Control.TaskSeq/pull/300
656662

657663
[issues]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues
658664
[nuget]: https://www.nuget.org/packages/FSharp.Control.TaskSeq/

release-notes.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ Release notes:
55
- update engineering to .NET 9/10
66
- adds TaskSeq.scan and TaskSeq.scanAsync, #289
77
- adds TaskSeq.pairwise, #289
8+
- adds TaskSeq.reduce and TaskSeq.reduceAsync, #289
9+
- adds TaskSeq.unfold and TaskSeq.unfoldAsync, #289
10+
- adds TaskSeq.chunkBySize (closes #258) and TaskSeq.windowed, #289
811

912
0.4.0
1013
- overhaul all doc comments, add exceptions, improve IDE quick-info experience, #136, #220, #234

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@
5353
<Compile Include="TaskSeq.ToXXX.Tests.fs" />
5454
<Compile Include="TaskSeq.UpdateAt.Tests.fs" />
5555
<Compile Include="TaskSeq.Zip.Tests.fs" />
56+
<Compile Include="TaskSeq.ChunkBySize.Tests.fs" />
57+
<Compile Include="TaskSeq.Windowed.Tests.fs" />
5658
<Compile Include="TaskSeq.Tests.CE.fs" />
5759
<Compile Include="TaskSeq.StateTransitionBug.Tests.CE.fs" />
5860
<Compile Include="TaskSeq.StateTransitionBug-delayed.Tests.CE.fs" />
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
module TaskSeq.Tests.ChunkBySize
2+
3+
open Xunit
4+
open FsUnit.Xunit
5+
6+
open FSharp.Control
7+
8+
//
9+
// TaskSeq.chunkBySize
10+
//
11+
12+
module EmptySeq =
13+
[<Fact>]
14+
let ``TaskSeq-chunkBySize with null source raises`` () = assertNullArg <| fun () -> TaskSeq.chunkBySize 1 null
15+
16+
[<Fact>]
17+
let ``TaskSeq-chunkBySize with zero raises ArgumentException before awaiting`` () =
18+
fun () -> TaskSeq.empty<int> |> TaskSeq.chunkBySize 0 |> ignore // throws eagerly, before enumeration
19+
|> should throw typeof<System.ArgumentException>
20+
21+
[<Fact>]
22+
let ``TaskSeq-chunkBySize with negative raises ArgumentException before awaiting`` () =
23+
fun () -> TaskSeq.empty<int> |> TaskSeq.chunkBySize -1 |> ignore
24+
|> should throw typeof<System.ArgumentException>
25+
26+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
27+
let ``TaskSeq-chunkBySize on empty sequence yields empty`` variant =
28+
Gen.getEmptyVariant variant
29+
|> TaskSeq.chunkBySize 1
30+
|> verifyEmpty
31+
32+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
33+
let ``TaskSeq-chunkBySize(99) on empty sequence yields empty`` variant =
34+
Gen.getEmptyVariant variant
35+
|> TaskSeq.chunkBySize 99
36+
|> verifyEmpty
37+
38+
module Immutable =
39+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
40+
let ``TaskSeq-chunkBySize preserves all elements in order`` variant = task {
41+
do!
42+
Gen.getSeqImmutable variant
43+
|> TaskSeq.chunkBySize 3
44+
|> TaskSeq.collect TaskSeq.ofArray
45+
|> verify1To10
46+
}
47+
48+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
49+
let ``TaskSeq-chunkBySize(2) returns 5 chunks of 2 for a 10-element sequence`` variant = task {
50+
let! chunks =
51+
Gen.getSeqImmutable variant
52+
|> TaskSeq.chunkBySize 2
53+
|> TaskSeq.toArrayAsync
54+
55+
chunks
56+
|> should equal [| [| 1; 2 |]; [| 3; 4 |]; [| 5; 6 |]; [| 7; 8 |]; [| 9; 10 |] |]
57+
}
58+
59+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
60+
let ``TaskSeq-chunkBySize(5) returns 2 full chunks for a 10-element sequence`` variant = task {
61+
let! chunks =
62+
Gen.getSeqImmutable variant
63+
|> TaskSeq.chunkBySize 5
64+
|> TaskSeq.toArrayAsync
65+
66+
chunks |> should equal [| [| 1..5 |]; [| 6..10 |] |]
67+
}
68+
69+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
70+
let ``TaskSeq-chunkBySize(1) returns each element as its own array`` variant = task {
71+
let! chunks =
72+
Gen.getSeqImmutable variant
73+
|> TaskSeq.chunkBySize 1
74+
|> TaskSeq.toArrayAsync
75+
76+
chunks |> Array.length |> should equal 10
77+
78+
chunks
79+
|> Array.iteri (fun i chunk -> chunk |> should equal [| i + 1 |])
80+
}
81+
82+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
83+
let ``TaskSeq-chunkBySize last chunk contains remainder when sequence does not divide evenly`` variant = task {
84+
// 10 elements with chunk size 3 → chunks [1;2;3] [4;5;6] [7;8;9] [10]
85+
let! chunks =
86+
Gen.getSeqImmutable variant
87+
|> TaskSeq.chunkBySize 3
88+
|> TaskSeq.toArrayAsync
89+
90+
chunks |> Array.length |> should equal 4
91+
chunks |> Array.last |> should equal [| 10 |]
92+
}
93+
94+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
95+
let ``TaskSeq-chunkBySize larger than sequence returns single chunk with all elements`` variant = task {
96+
let! chunks =
97+
Gen.getSeqImmutable variant
98+
|> TaskSeq.chunkBySize 11
99+
|> TaskSeq.toArrayAsync
100+
101+
chunks |> Array.length |> should equal 1
102+
chunks.[0] |> should equal [| 1..10 |]
103+
}
104+
105+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
106+
let ``TaskSeq-chunkBySize equal to sequence length returns single full chunk`` variant = task {
107+
let! chunks =
108+
Gen.getSeqImmutable variant
109+
|> TaskSeq.chunkBySize 10
110+
|> TaskSeq.toArrayAsync
111+
112+
chunks |> Array.length |> should equal 1
113+
chunks.[0] |> should equal [| 1..10 |]
114+
}
115+
116+
[<Fact>]
117+
let ``TaskSeq-chunkBySize each chunk array is independent - modifying one does not affect others`` () = task {
118+
let! chunks =
119+
taskSeq { yield! [ 1..6 ] }
120+
|> TaskSeq.chunkBySize 3
121+
|> TaskSeq.toArrayAsync
122+
123+
// Mutate the first chunk
124+
chunks.[0].[0] <- 99
125+
126+
// The second chunk must be unaffected
127+
chunks.[1] |> should equal [| 4; 5; 6 |]
128+
chunks.[0].[0] |> should equal 99
129+
}
130+
131+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
132+
let ``TaskSeq-chunkBySize remainder sizes`` variant = task {
133+
let verifyLastChunkSize chunkSize expectedLast =
134+
Gen.getSeqImmutable variant
135+
|> TaskSeq.chunkBySize chunkSize
136+
|> TaskSeq.toArrayAsync
137+
|> Task.map (Array.last >> Array.length >> should equal expectedLast)
138+
139+
do! verifyLastChunkSize 3 1 // 10 mod 3 = 1
140+
do! verifyLastChunkSize 4 2 // 10 mod 4 = 2
141+
do! verifyLastChunkSize 6 4 // 10 mod 6 = 4
142+
do! verifyLastChunkSize 7 3 // 10 mod 7 = 3
143+
do! verifyLastChunkSize 9 1 // 10 mod 9 = 1
144+
}
145+
146+
module SideEffects =
147+
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
148+
let ``TaskSeq-chunkBySize gets all items`` variant =
149+
Gen.getSeqWithSideEffect variant
150+
|> TaskSeq.chunkBySize 5
151+
|> TaskSeq.toArrayAsync
152+
|> Task.map (should equal [| [| 1..5 |]; [| 6..10 |] |])
153+
154+
[<Fact>]
155+
let ``TaskSeq-chunkBySize executes side-effects from empty source`` () = task {
156+
let mutable sideEffects = 0
157+
158+
let ts = taskSeq {
159+
sideEffects <- sideEffects + 1
160+
sideEffects <- sideEffects + 1
161+
}
162+
163+
do! ts |> TaskSeq.chunkBySize 1 |> consumeTaskSeq
164+
do! ts |> TaskSeq.chunkBySize 3 |> consumeTaskSeq
165+
sideEffects |> should equal 4
166+
}
167+
168+
[<Fact>]
169+
let ``TaskSeq-chunkBySize executes all source side-effects`` () = task {
170+
let mutable sideEffects = 0
171+
172+
let ts = taskSeq {
173+
sideEffects <- sideEffects + 1
174+
yield 1
175+
sideEffects <- sideEffects + 1
176+
yield 2
177+
sideEffects <- sideEffects + 1 // executed even after last yield
178+
}
179+
180+
do! ts |> TaskSeq.chunkBySize 2 |> consumeTaskSeq
181+
sideEffects |> should equal 3
182+
}
183+
184+
[<Fact>]
185+
let ``TaskSeq-chunkBySize propagates exception from source`` () =
186+
let items = taskSeq {
187+
yield 1
188+
yield 2
189+
failwith "boom"
190+
yield 3
191+
}
192+
193+
fun () -> items |> TaskSeq.chunkBySize 2 |> consumeTaskSeq
194+
|> should throwAsyncExact typeof<System.Exception>

0 commit comments

Comments
 (0)