Skip to content

Commit 82e30ea

Browse files
test: add SideEffects module to TaskSeq.OfXXX.Tests.fs
Documents re-iteration semantics for the ofXxx conversion functions: - ofSeq: re-evaluates the underlying IEnumerable on each re-enumeration - ofTaskSeq with lazy seq: creates fresh Task objects on each re-enumeration - ofTaskArray: tasks run once upfront; re-enumeration awaits cached results Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent fc687a3 commit 82e30ea

2 files changed

Lines changed: 74 additions & 0 deletions

File tree

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
Unreleased
5+
- test: add SideEffects module to TaskSeq.OfXXX.Tests.fs documenting re-iteration semantics (ofSeq re-evaluates source, ofTaskArray re-awaits cached tasks)
56
- test: add SideEffects module and ImmTaskSeq variant tests to TaskSeq.ChunkBy.Tests.fs, improving coverage for chunkBy and chunkByAsync
67
- fixes: `Async.bind` signature corrected from `(Async<'T> -> Async<'U>)` to `('T -> Async<'U>)` to match standard monadic bind semantics (same as `Task.bind`); the previous signature made the function effectively equivalent to direct application
78
- refactor: simplify splitAt 'rest' taskSeq to use while!, removing redundant go2 mutable and manual MoveNextAsync pre-advance

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

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,76 @@ module Immutable =
121121

122122
[<Fact>]
123123
let ``TaskSeq-ofSeq should succeed`` () = Seq.init 10 id |> TaskSeq.ofSeq |> validateSequence
124+
125+
module SideEffects =
126+
[<Fact>]
127+
let ``ofSeq re-evaluates the underlying source seq on each re-enumeration`` () = task {
128+
let mutable count = 0
129+
130+
// a lazy IEnumerable — each GetEnumerator() call re-executes the body
131+
let lazySeq = seq {
132+
for i in 1..3 do
133+
count <- count + 1
134+
yield i
135+
}
136+
137+
let ts = TaskSeq.ofSeq lazySeq
138+
let! arr1 = ts |> TaskSeq.toArrayAsync
139+
// each item triggered the side effect once
140+
count |> should equal 3
141+
142+
let! arr2 = ts |> TaskSeq.toArrayAsync
143+
// the underlying seq is re-traversed on the second GetAsyncEnumerator call
144+
count |> should equal 6
145+
arr1 |> should equal arr2
146+
}
147+
148+
[<Fact>]
149+
let ``ofTaskSeq with lazy seq of tasks re-creates tasks on each re-enumeration`` () = task {
150+
let mutable count = 0
151+
152+
// a lazy IEnumerable of Task objects — each seq iteration creates fresh Task objects
153+
let lazyTaskSeq = seq {
154+
for i in 1..3 do
155+
yield task {
156+
count <- count + 1
157+
return i
158+
}
159+
}
160+
161+
let ts = TaskSeq.ofTaskSeq lazyTaskSeq
162+
let! arr1 = ts |> TaskSeq.toArrayAsync
163+
count |> should equal 3
164+
165+
let! arr2 = ts |> TaskSeq.toArrayAsync
166+
// the underlying seq is re-iterated; new Task objects are created and run
167+
count |> should equal 6
168+
arr1 |> should equal arr2
169+
}
170+
171+
[<Fact>]
172+
let ``ofTaskArray does not re-run tasks on re-enumeration; task results are cached`` () = task {
173+
let mutable count = 0
174+
175+
// tasks are created upfront; they run synchronously to completion when constructed
176+
let tasks =
177+
Array.init 3 (fun i -> task {
178+
count <- count + 1
179+
return i + 1
180+
})
181+
182+
// all three tasks have already completed synchronously
183+
count |> should equal 3
184+
185+
let ts = TaskSeq.ofTaskArray tasks
186+
let! arr1 = ts |> TaskSeq.toArrayAsync
187+
188+
// awaiting already-completed tasks does not re-run them
189+
count |> should equal 3
190+
arr1 |> should equal [| 1; 2; 3 |]
191+
192+
let! arr2 = ts |> TaskSeq.toArrayAsync
193+
// the second enumeration re-awaits the same cached task results
194+
count |> should equal 3
195+
arr2 |> should equal arr1
196+
}

0 commit comments

Comments
 (0)