Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 16 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,15 +211,16 @@ The `TaskSeq` project already has a wide array of functions and functionalities,
- [ ] `average` / `averageBy`, `sum` and related
- [x] `forall` / `forallAsync` (see [#240])
- [x] `skip` / `drop` / `truncate` / `take` (see [#209])
- [ ] `chunkBySize` / `windowed`
- [x] `chunkBySize` / `windowed` (see [#258])
- [ ] `compareWith`
- [ ] `distinct`
- [ ] `exists2` / `map2` / `fold2` / `iter2` and related '2'-functions
- [ ] `mapFold`
- [ ] `pairwise` / `allpairs` / `permute` / `distinct` / `distinctBy`
- [x] `pairwise` (see [#293])
- [ ] `allpairs` / `permute` / `distinct` / `distinctBy`
- [ ] `replicate`
- [ ] `reduce` / `scan`
- [ ] `unfold`
- [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
- [x] Make `TaskSeq` interoperable with `Task` by expanding the latter with a `for .. in .. do` that acceps task sequences
- [x] Add to/from functions to seq, list, array
Expand Down Expand Up @@ -263,7 +264,7 @@ This is what has been implemented so far, is planned or skipped:
| ✅ [#67][] | | | `box` | |
| ✅ [#67][] | | | `unbox` | |
| ✅ [#23][] | `choose` | `choose` | `chooseAsync` | |
| | `chunkBySize` | `chunkBySize` | | |
| ✅ [#258][] | `chunkBySize` | `chunkBySize` | | |
| ✅ [#11][] | `collect` | `collect` | `collectAsync` | |
| ✅ [#11][] | | `collectSeq` | `collectSeqAsync` | |
| | `compareWith` | `compareWith` | `compareWithAsync` | |
Expand Down Expand Up @@ -332,17 +333,17 @@ This is what has been implemented so far, is planned or skipped:
| ✅ [#2][] | | `ofTaskArray` | | |
| ✅ [#2][] | | `ofTaskList` | | |
| ✅ [#2][] | | `ofTaskSeq` | | |
| | `pairwise` | `pairwise` | | |
| ✅ [#293][] | `pairwise` | `pairwise` | | |
| | `permute` | `permute` | `permuteAsync` | |
| ✅ [#23][] | `pick` | `pick` | `pickAsync` | |
| &#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.") |
| | `reduce` | `reduce` | `reduceAsync` | |
| &#x2705; [#299][] | `reduce` | `reduce` | `reduceAsync` | |
| &#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.") |
| &#x2705; [#236][]| `removeAt` | `removeAt` | | |
| &#x2705; [#236][]| `removeManyAt` | `removeManyAt` | | |
| | `replicate` | `replicate` | | |
| &#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.") |
| | `scan` | `scan` | `scanAsync` | |
| &#x2705; [#296][] | `scan` | `scan` | `scanAsync` | |
| &#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.") |
| &#x2705; [#90][] | `singleton` | `singleton` | | |
| &#x2705; [#209][]| `skip` | `skip` | | |
Expand Down Expand Up @@ -378,10 +379,10 @@ This is what has been implemented so far, is planned or skipped:
| &#x2705; [#23][] | `tryLast` | `tryLast` | | |
| &#x2705; [#23][] | `tryPick` | `tryPick` | `tryPickAsync` | |
| &#x2705; [#76][] | | `tryTail` | | |
| | `unfold` | `unfold` | `unfoldAsync` | |
| &#x2705; [#300][] | `unfold` | `unfold` | `unfoldAsync` | |
| &#x2705; [#236][]| `updateAt` | `updateAt` | | |
| &#x2705; [#217][]| `where` | `where` | `whereAsync` | |
| | `windowed` | `windowed` | | |
| &#x2705; [#258][] | `windowed` | `windowed` | | |
| &#x2705; [#2][] | `zip` | `zip` | | |
| | `zip3` | `zip3` | | |
| | | `zip4` | | |
Expand Down Expand Up @@ -653,6 +654,11 @@ module TaskSeq =
[#237]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/237
[#236]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/236
[#240]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/240
[#258]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/258
[#293]: https://github.com/fsprojects/FSharp.Control.TaskSeq/pull/293
[#296]: https://github.com/fsprojects/FSharp.Control.TaskSeq/pull/296
[#299]: https://github.com/fsprojects/FSharp.Control.TaskSeq/pull/299
[#300]: https://github.com/fsprojects/FSharp.Control.TaskSeq/pull/300

[issues]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues
[nuget]: https://www.nuget.org/packages/FSharp.Control.TaskSeq/
3 changes: 3 additions & 0 deletions release-notes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ Release notes:
- update engineering to .NET 9/10
- adds TaskSeq.scan and TaskSeq.scanAsync, #289
- adds TaskSeq.pairwise, #289
- adds TaskSeq.reduce and TaskSeq.reduceAsync, #289
- adds TaskSeq.unfold and TaskSeq.unfoldAsync, #289
- adds TaskSeq.chunkBySize (closes #258) and TaskSeq.windowed, #289
- fixes: CancellationToken passed to GetAsyncEnumerator is now honored in MoveNextAsync, #179

0.4.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@
<Compile Include="TaskSeq.ToXXX.Tests.fs" />
<Compile Include="TaskSeq.UpdateAt.Tests.fs" />
<Compile Include="TaskSeq.Zip.Tests.fs" />
<Compile Include="TaskSeq.ChunkBySize.Tests.fs" />
<Compile Include="TaskSeq.Windowed.Tests.fs" />
<Compile Include="TaskSeq.Tests.CE.fs" />
<Compile Include="TaskSeq.StateTransitionBug.Tests.CE.fs" />
<Compile Include="TaskSeq.StateTransitionBug-delayed.Tests.CE.fs" />
Expand Down
194 changes: 194 additions & 0 deletions src/FSharp.Control.TaskSeq.Test/TaskSeq.ChunkBySize.Tests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
module TaskSeq.Tests.ChunkBySize

open Xunit
open FsUnit.Xunit

open FSharp.Control

//
// TaskSeq.chunkBySize
//

module EmptySeq =
[<Fact>]
let ``TaskSeq-chunkBySize with null source raises`` () = assertNullArg <| fun () -> TaskSeq.chunkBySize 1 null

[<Fact>]
let ``TaskSeq-chunkBySize with zero raises ArgumentException before awaiting`` () =
fun () -> TaskSeq.empty<int> |> TaskSeq.chunkBySize 0 |> ignore // throws eagerly, before enumeration
|> should throw typeof<System.ArgumentException>

[<Fact>]
let ``TaskSeq-chunkBySize with negative raises ArgumentException before awaiting`` () =
fun () -> TaskSeq.empty<int> |> TaskSeq.chunkBySize -1 |> ignore
|> should throw typeof<System.ArgumentException>

[<Theory; ClassData(typeof<TestEmptyVariants>)>]
let ``TaskSeq-chunkBySize on empty sequence yields empty`` variant =
Gen.getEmptyVariant variant
|> TaskSeq.chunkBySize 1
|> verifyEmpty

[<Theory; ClassData(typeof<TestEmptyVariants>)>]
let ``TaskSeq-chunkBySize(99) on empty sequence yields empty`` variant =
Gen.getEmptyVariant variant
|> TaskSeq.chunkBySize 99
|> verifyEmpty

module Immutable =
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
let ``TaskSeq-chunkBySize preserves all elements in order`` variant = task {
do!
Gen.getSeqImmutable variant
|> TaskSeq.chunkBySize 3
|> TaskSeq.collect TaskSeq.ofArray
|> verify1To10
}

[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
let ``TaskSeq-chunkBySize(2) returns 5 chunks of 2 for a 10-element sequence`` variant = task {
let! chunks =
Gen.getSeqImmutable variant
|> TaskSeq.chunkBySize 2
|> TaskSeq.toArrayAsync

chunks
|> should equal [| [| 1; 2 |]; [| 3; 4 |]; [| 5; 6 |]; [| 7; 8 |]; [| 9; 10 |] |]
}

[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
let ``TaskSeq-chunkBySize(5) returns 2 full chunks for a 10-element sequence`` variant = task {
let! chunks =
Gen.getSeqImmutable variant
|> TaskSeq.chunkBySize 5
|> TaskSeq.toArrayAsync

chunks |> should equal [| [| 1..5 |]; [| 6..10 |] |]
}

[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
let ``TaskSeq-chunkBySize(1) returns each element as its own array`` variant = task {
let! chunks =
Gen.getSeqImmutable variant
|> TaskSeq.chunkBySize 1
|> TaskSeq.toArrayAsync

chunks |> Array.length |> should equal 10

chunks
|> Array.iteri (fun i chunk -> chunk |> should equal [| i + 1 |])
}

[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
let ``TaskSeq-chunkBySize last chunk contains remainder when sequence does not divide evenly`` variant = task {
// 10 elements with chunk size 3 β†’ chunks [1;2;3] [4;5;6] [7;8;9] [10]
let! chunks =
Gen.getSeqImmutable variant
|> TaskSeq.chunkBySize 3
|> TaskSeq.toArrayAsync

chunks |> Array.length |> should equal 4
chunks |> Array.last |> should equal [| 10 |]
}

[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
let ``TaskSeq-chunkBySize larger than sequence returns single chunk with all elements`` variant = task {
let! chunks =
Gen.getSeqImmutable variant
|> TaskSeq.chunkBySize 11
|> TaskSeq.toArrayAsync

chunks |> Array.length |> should equal 1
chunks.[0] |> should equal [| 1..10 |]
}

[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
let ``TaskSeq-chunkBySize equal to sequence length returns single full chunk`` variant = task {
let! chunks =
Gen.getSeqImmutable variant
|> TaskSeq.chunkBySize 10
|> TaskSeq.toArrayAsync

chunks |> Array.length |> should equal 1
chunks.[0] |> should equal [| 1..10 |]
}

[<Fact>]
let ``TaskSeq-chunkBySize each chunk array is independent - modifying one does not affect others`` () = task {
let! chunks =
taskSeq { yield! [ 1..6 ] }
|> TaskSeq.chunkBySize 3
|> TaskSeq.toArrayAsync

// Mutate the first chunk
chunks.[0].[0] <- 99

// The second chunk must be unaffected
chunks.[1] |> should equal [| 4; 5; 6 |]
chunks.[0].[0] |> should equal 99
}

[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
let ``TaskSeq-chunkBySize remainder sizes`` variant = task {
let verifyLastChunkSize chunkSize expectedLast =
Gen.getSeqImmutable variant
|> TaskSeq.chunkBySize chunkSize
|> TaskSeq.toArrayAsync
|> Task.map (Array.last >> Array.length >> should equal expectedLast)

do! verifyLastChunkSize 3 1 // 10 mod 3 = 1
do! verifyLastChunkSize 4 2 // 10 mod 4 = 2
do! verifyLastChunkSize 6 4 // 10 mod 6 = 4
do! verifyLastChunkSize 7 3 // 10 mod 7 = 3
do! verifyLastChunkSize 9 1 // 10 mod 9 = 1
}

module SideEffects =
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
let ``TaskSeq-chunkBySize gets all items`` variant =
Gen.getSeqWithSideEffect variant
|> TaskSeq.chunkBySize 5
|> TaskSeq.toArrayAsync
|> Task.map (should equal [| [| 1..5 |]; [| 6..10 |] |])

[<Fact>]
let ``TaskSeq-chunkBySize executes side-effects from empty source`` () = task {
let mutable sideEffects = 0

let ts = taskSeq {
sideEffects <- sideEffects + 1
sideEffects <- sideEffects + 1
}

do! ts |> TaskSeq.chunkBySize 1 |> consumeTaskSeq
do! ts |> TaskSeq.chunkBySize 3 |> consumeTaskSeq
sideEffects |> should equal 4
}

[<Fact>]
let ``TaskSeq-chunkBySize executes all source side-effects`` () = task {
let mutable sideEffects = 0

let ts = taskSeq {
sideEffects <- sideEffects + 1
yield 1
sideEffects <- sideEffects + 1
yield 2
sideEffects <- sideEffects + 1 // executed even after last yield
}

do! ts |> TaskSeq.chunkBySize 2 |> consumeTaskSeq
sideEffects |> should equal 3
}

[<Fact>]
let ``TaskSeq-chunkBySize propagates exception from source`` () =
let items = taskSeq {
yield 1
yield 2
failwith "boom"
yield 3
}

fun () -> items |> TaskSeq.chunkBySize 2 |> consumeTaskSeq
|> should throwAsyncExact typeof<System.Exception>
Loading