Skip to content

Commit 0d3967a

Browse files
authored
Merge pull request #401 from OnurGumus/add-foldUntil
feat: add TaskSeq.foldWhile and foldWhileAsync
2 parents 41538fc + 7acdb0e commit 0d3967a

6 files changed

Lines changed: 356 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+
- adds TaskSeq.foldWhile and TaskSeq.foldWhileAsync: fold with early termination via a (state, element) -> bool predicate (takeWhile-style, exclusive). When the predicate returns false, iteration halts without folding that element; no further elements are enumerated.
56
- feat: add `TaskSeq.toChannelAsync` and `TaskSeq.ofChannel` for bidirectional `System.Threading.Channels` integration, closing #415
67
- eng: update PackageValidationBaselineVersion from 0.4.0 to 1.1.1 to enforce binary compatibility checks against the current stable release
78
- test: add SideEffects module and ImmTaskSeq variant tests to TaskSeq.ChunkBy.Tests.fs, improving coverage for chunkBy and chunkByAsync

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
<Compile Include="TaskSeq.FindIndex.Tests.fs" />
2929
<Compile Include="TaskSeq.Find.Tests.fs" />
3030
<Compile Include="TaskSeq.Fold.Tests.fs" />
31+
<Compile Include="TaskSeq.FoldWhile.Tests.fs" />
3132
<Compile Include="TaskSeq.Scan.Tests.fs" />
3233
<Compile Include="TaskSeq.MapFold.Tests.fs" />
3334
<Compile Include="TaskSeq.Reduce.Tests.fs" />
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
module TaskSeq.Tests.FoldWhile
2+
3+
open Xunit
4+
open FsUnit.Xunit
5+
6+
open FSharp.Control
7+
8+
//
9+
// TaskSeq.foldWhile
10+
// TaskSeq.foldWhileAsync
11+
//
12+
// Semantics match TaskSeq.takeWhile: the predicate is evaluated against (state, element)
13+
// before that element is folded in. When the predicate returns false, iteration halts
14+
// without folding that element, and no further elements are enumerated.
15+
//
16+
17+
module EmptySeq =
18+
[<Fact>]
19+
let ``Null source is invalid`` () =
20+
assertNullArg
21+
<| fun () -> TaskSeq.foldWhile (fun _ _ -> true) (fun _ item -> item + 1) 0 null
22+
23+
assertNullArg
24+
<| fun () -> TaskSeq.foldWhileAsync (fun _ _ -> Task.fromResult true) (fun _ item -> Task.fromResult (item + 1)) 0 null
25+
26+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
27+
let ``TaskSeq-foldWhile returns initial state when empty`` variant = task {
28+
let! result =
29+
Gen.getEmptyVariant variant
30+
|> TaskSeq.foldWhile (fun _ _ -> true) (fun _ item -> item + 1) -1
31+
32+
result |> should equal -1
33+
}
34+
35+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
36+
let ``TaskSeq-foldWhileAsync returns initial state when empty`` variant = task {
37+
let! result =
38+
Gen.getEmptyVariant variant
39+
|> TaskSeq.foldWhileAsync (fun _ _ -> Task.fromResult true) (fun _ item -> Task.fromResult (item + 1)) -1
40+
41+
result |> should equal -1
42+
}
43+
44+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
45+
let ``TaskSeq-foldWhile does not call predicate or folder when empty`` variant = task {
46+
let mutable predicateCalled = false
47+
let mutable folderCalled = false
48+
49+
let! _ =
50+
Gen.getEmptyVariant variant
51+
|> TaskSeq.foldWhile
52+
(fun _ _ ->
53+
predicateCalled <- true
54+
true)
55+
(fun state _ ->
56+
folderCalled <- true
57+
state)
58+
0
59+
60+
predicateCalled |> should be False
61+
folderCalled |> should be False
62+
}
63+
64+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
65+
let ``TaskSeq-foldWhileAsync does not call predicate or folder when empty`` variant = task {
66+
let mutable predicateCalled = false
67+
let mutable folderCalled = false
68+
69+
let! _ =
70+
Gen.getEmptyVariant variant
71+
|> TaskSeq.foldWhileAsync
72+
(fun _ _ -> task {
73+
predicateCalled <- true
74+
return true
75+
})
76+
(fun state _ -> task {
77+
folderCalled <- true
78+
return state
79+
})
80+
0
81+
82+
predicateCalled |> should be False
83+
folderCalled |> should be False
84+
}
85+
86+
module Functionality =
87+
[<Fact>]
88+
let ``TaskSeq-foldWhile with always-true predicate behaves like fold`` () = task {
89+
let! result =
90+
TaskSeq.ofList [ 1; 2; 3; 4; 5 ]
91+
|> TaskSeq.foldWhile (fun _ _ -> true) (fun acc item -> acc + item) 0
92+
93+
result |> should equal 15
94+
}
95+
96+
[<Fact>]
97+
let ``TaskSeq-foldWhileAsync with always-true predicate behaves like foldAsync`` () = task {
98+
let! result =
99+
TaskSeq.ofList [ 1; 2; 3; 4; 5 ]
100+
|> TaskSeq.foldWhileAsync (fun _ _ -> Task.fromResult true) (fun acc item -> Task.fromResult (acc + item)) 0
101+
102+
result |> should equal 15
103+
}
104+
105+
[<Fact>]
106+
let ``TaskSeq-foldWhile is left-associative like fold`` () = task {
107+
let! result =
108+
TaskSeq.ofList [ "b"; "c"; "d" ]
109+
|> TaskSeq.foldWhile (fun _ _ -> true) (fun acc item -> acc + item) "a"
110+
111+
result |> should equal "abcd"
112+
}
113+
114+
[<Fact>]
115+
let ``TaskSeq-foldWhileAsync is left-associative like foldAsync`` () = task {
116+
let! result =
117+
TaskSeq.ofList [ "b"; "c"; "d" ]
118+
|> TaskSeq.foldWhileAsync (fun _ _ -> Task.fromResult true) (fun acc item -> Task.fromResult (acc + item)) "a"
119+
120+
result |> should equal "abcd"
121+
}
122+
123+
module Halt =
124+
[<Fact>]
125+
let ``TaskSeq-foldWhile stops immediately when predicate is false on first element`` () = task {
126+
let mutable predicateCalls = 0
127+
let mutable folderCalls = 0
128+
129+
let! result =
130+
TaskSeq.ofList [ 1; 2; 3; 4; 5 ]
131+
|> TaskSeq.foldWhile
132+
(fun _ _ ->
133+
predicateCalls <- predicateCalls + 1
134+
false)
135+
(fun _ item ->
136+
folderCalls <- folderCalls + 1
137+
item)
138+
0
139+
140+
result |> should equal 0
141+
predicateCalls |> should equal 1
142+
folderCalls |> should equal 0
143+
}
144+
145+
[<Fact>]
146+
let ``TaskSeq-foldWhileAsync stops immediately when predicate is false on first element`` () = task {
147+
let mutable predicateCalls = 0
148+
let mutable folderCalls = 0
149+
150+
let! result =
151+
TaskSeq.ofList [ 1; 2; 3; 4; 5 ]
152+
|> TaskSeq.foldWhileAsync
153+
(fun _ _ -> task {
154+
predicateCalls <- predicateCalls + 1
155+
return false
156+
})
157+
(fun _ item -> task {
158+
folderCalls <- folderCalls + 1
159+
return item
160+
})
161+
0
162+
163+
result |> should equal 0
164+
predicateCalls |> should equal 1
165+
folderCalls |> should equal 0
166+
}
167+
168+
[<Fact>]
169+
let ``TaskSeq-foldWhile halts mid-sequence without folding the halting element`` () = task {
170+
// Sum while adding the next element would keep the total <= 5. Once adding
171+
// the element would overshoot, stop — that element is NOT folded in.
172+
let mutable predicateCalls = 0
173+
let mutable folderCalls = 0
174+
175+
let! result =
176+
TaskSeq.ofList [ 1; 2; 3; 4; 5 ]
177+
|> TaskSeq.foldWhile
178+
(fun acc item ->
179+
predicateCalls <- predicateCalls + 1
180+
acc + item <= 5)
181+
(fun acc item ->
182+
folderCalls <- folderCalls + 1
183+
acc + item)
184+
0
185+
186+
// 1 (ok, total 1), 2 (ok, total 3), 3 (would make 6 > 5, stop)
187+
result |> should equal 3
188+
predicateCalls |> should equal 3
189+
folderCalls |> should equal 2
190+
}
191+
192+
[<Fact>]
193+
let ``TaskSeq-foldWhileAsync halts mid-sequence without folding the halting element`` () = task {
194+
let mutable predicateCalls = 0
195+
let mutable folderCalls = 0
196+
197+
let! result =
198+
TaskSeq.ofList [ 1; 2; 3; 4; 5 ]
199+
|> TaskSeq.foldWhileAsync
200+
(fun acc item -> task {
201+
predicateCalls <- predicateCalls + 1
202+
return acc + item <= 5
203+
})
204+
(fun acc item -> task {
205+
folderCalls <- folderCalls + 1
206+
return acc + item
207+
})
208+
0
209+
210+
result |> should equal 3
211+
predicateCalls |> should equal 3
212+
folderCalls |> should equal 2
213+
}
214+
215+
[<Fact>]
216+
let ``TaskSeq-foldWhile does not enumerate past the halting element`` () = task {
217+
// Source has a side effect per pulled element; halt on the 3rd pull.
218+
let mutable pulled = 0
219+
220+
let source = taskSeq {
221+
for i in 1..5 do
222+
pulled <- pulled + 1
223+
yield i
224+
}
225+
226+
let! _ =
227+
source
228+
|> TaskSeq.foldWhile (fun _ item -> item < 3) (fun acc item -> acc + item) 0
229+
230+
// Pull 1 (ok), pull 2 (ok), pull 3 (predicate false, stop) — must not pull 4.
231+
pulled |> should equal 3
232+
}
233+
234+
[<Fact>]
235+
let ``TaskSeq-foldWhileAsync does not enumerate past the halting element`` () = task {
236+
let mutable pulled = 0
237+
238+
let source = taskSeq {
239+
for i in 1..5 do
240+
pulled <- pulled + 1
241+
yield i
242+
}
243+
244+
let! _ =
245+
source
246+
|> TaskSeq.foldWhileAsync (fun _ item -> Task.fromResult (item < 3)) (fun acc item -> Task.fromResult (acc + item)) 0
247+
248+
pulled |> should equal 3
249+
}
250+
251+
[<Fact>]
252+
let ``TaskSeq-foldWhile that never halts is equivalent to fold`` () = task {
253+
let! result =
254+
TaskSeq.ofList [ 1; 2; 3; 4; 5 ]
255+
|> TaskSeq.foldWhile (fun _ item -> item <= 10) (fun acc item -> acc + item) 0
256+
257+
result |> should equal 15
258+
}

src/FSharp.Control.TaskSeq/TaskSeq.fs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,10 @@ type TaskSeq private () =
569569
static member compareWithAsync comparer source1 source2 = Internal.compareWithAsync comparer source1 source2
570570
static member fold folder state source = Internal.fold (FolderAction folder) state source
571571
static member foldAsync folder state source = Internal.fold (AsyncFolderAction folder) state source
572+
static member foldWhile predicate folder state source = Internal.foldWhile predicate folder state source
573+
574+
static member foldWhileAsync predicate folder state source = Internal.foldWhileAsync predicate folder state source
575+
572576
static member scan folder state source = Internal.scan (FolderAction folder) state source
573577
static member scanAsync folder state source = Internal.scan (AsyncFolderAction folder) state source
574578
static member reduce folder source = Internal.reduce (FolderAction folder) source

src/FSharp.Control.TaskSeq/TaskSeq.fsi

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1891,6 +1891,51 @@ type TaskSeq =
18911891
static member foldAsync:
18921892
folder: ('State -> 'T -> #Task<'State>) -> state: 'State -> source: TaskSeq<'T> -> Task<'State>
18931893

1894+
/// <summary>
1895+
/// Applies the function <paramref name="folder" /> to each element in the task sequence, threading an
1896+
/// accumulator of type <paramref name="'State" /> through the computation, for as long as
1897+
/// <paramref name="predicate" /> returns <c>true</c>. The predicate is evaluated against the current
1898+
/// state and next element before that element is folded in; once it returns <c>false</c> the element
1899+
/// is not folded, iteration stops, and no further elements of the input are enumerated.
1900+
/// If either function is asynchronous, consider using <see cref="TaskSeq.foldWhileAsync" />.
1901+
/// </summary>
1902+
///
1903+
/// <param name="predicate">A function that, given the current state and next element, returns <c>true</c> to keep folding or <c>false</c> to stop.</param>
1904+
/// <param name="folder">A function that updates the state with each element from the sequence.</param>
1905+
/// <param name="state">The initial state.</param>
1906+
/// <param name="source">The input sequence.</param>
1907+
/// <returns>The state object after iteration halted, or after the whole sequence was consumed.</returns>
1908+
/// <exception cref="T:ArgumentNullException">Thrown when the input task sequence is null.</exception>
1909+
static member foldWhile:
1910+
predicate: ('State -> 'T -> bool) ->
1911+
folder: ('State -> 'T -> 'State) ->
1912+
state: 'State ->
1913+
source: TaskSeq<'T> ->
1914+
Task<'State>
1915+
1916+
/// <summary>
1917+
/// Applies the asynchronous function <paramref name="folder" /> to each element in the task sequence,
1918+
/// threading an accumulator of type <paramref name="'State" /> through the computation, for as long as
1919+
/// the asynchronous <paramref name="predicate" /> returns <c>true</c>. The predicate is evaluated
1920+
/// against the current state and next element before that element is folded in; once it returns
1921+
/// <c>false</c> the element is not folded, iteration stops, and no further elements of the input are
1922+
/// enumerated.
1923+
/// If both functions are synchronous, consider using <see cref="TaskSeq.foldWhile" />.
1924+
/// </summary>
1925+
///
1926+
/// <param name="predicate">An async function that, given the current state and next element, returns <c>true</c> to keep folding or <c>false</c> to stop.</param>
1927+
/// <param name="folder">An async function that updates the state with each element from the sequence.</param>
1928+
/// <param name="state">The initial state.</param>
1929+
/// <param name="source">The input sequence.</param>
1930+
/// <returns>The state object after iteration halted, or after the whole sequence was consumed.</returns>
1931+
/// <exception cref="T:ArgumentNullException">Thrown when the input task sequence is null.</exception>
1932+
static member foldWhileAsync:
1933+
predicate: ('State -> 'T -> #Task<bool>) ->
1934+
folder: ('State -> 'T -> #Task<'State>) ->
1935+
state: 'State ->
1936+
source: TaskSeq<'T> ->
1937+
Task<'State>
1938+
18941939
/// <summary>
18951940
/// Like <see cref="TaskSeq.fold" />, but returns the sequence of intermediate results and the final result.
18961941
/// The first element of the output sequence is always the initial state. If the input task sequence

src/FSharp.Control.TaskSeq/TaskSeqInternal.fs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,53 @@ module internal TaskSeqInternal =
409409
return result
410410
}
411411

412+
let foldWhile predicate folder initial (source: TaskSeq<_>) =
413+
checkNonNull (nameof source) source
414+
415+
task {
416+
use e = source.GetAsyncEnumerator CancellationToken.None
417+
let mutable result = initial
418+
let mutable running = true
419+
420+
while running do
421+
let! hasNext = e.MoveNextAsync()
422+
423+
if hasNext then
424+
if predicate result e.Current then
425+
result <- folder result e.Current
426+
else
427+
running <- false
428+
else
429+
running <- false
430+
431+
return result
432+
}
433+
434+
let foldWhileAsync predicate folder initial (source: TaskSeq<_>) =
435+
checkNonNull (nameof source) source
436+
437+
task {
438+
use e = source.GetAsyncEnumerator CancellationToken.None
439+
let mutable result = initial
440+
let mutable running = true
441+
442+
while running do
443+
let! hasNext = e.MoveNextAsync()
444+
445+
if hasNext then
446+
let! keepGoing = predicate result e.Current
447+
448+
if keepGoing then
449+
let! newState = folder result e.Current
450+
result <- newState
451+
else
452+
running <- false
453+
else
454+
running <- false
455+
456+
return result
457+
}
458+
412459
let scan folder initial (source: TaskSeq<_>) =
413460
checkNonNull (nameof source) source
414461

0 commit comments

Comments
 (0)