feat: add TaskSeq.foldWhile and foldWhileAsync#401
feat: add TaskSeq.foldWhile and foldWhileAsync#401OnurGumus wants to merge 4 commits intofsprojects:mainfrom
Conversation
Fold over a task sequence with early termination. The folder returns
`Continue newState` to keep consuming, or `Halt newState` to stop
immediately; in either case the state is updated. No elements past the
one that caused Halt are enumerated from the input.
Motivation: reaching for `fold`/`foldAsync` often ends with a mutable
flag or a `match state with Decided -> state | _ -> ...` guard at the
top of the folder when the caller wants to short-circuit on some
condition discovered mid-stream. `tryPickAsync` handles the "find and
halt" subcase but cannot also thread an accumulator. `foldUntil` fills
the gap and removes the impedance mismatch.
Includes:
- FoldStep<'State> DU (Continue / Halt) in public API
- Internal FoldUntilAction DU unifying sync/async folder dispatch
- Public TaskSeq.foldUntil and TaskSeq.foldUntilAsync
- XML docs on both overloads mirroring fold/foldAsync style
- Release-notes entry
- 22 tests covering: null source, empty sequence (no folder call,
initial state preserved), all-Continue (equals fold), Halt-on-first
(1 folder call, no further pulls), Halt mid-sequence (correct count,
source not pulled past Halt), Halt-on-last (equals fold)
|
@OnurGumus I'm wondering about the new type. Why not a boolean flag to indicate when to stop? |
Reworks the early-termination fold added in 7bde226 to match the conventions of FSharp.Control.TaskSeq: bool-returning predicate + plain folder, and no new public DU. Every other early-termination API in the library (takeWhile, skipWhile, tryFind, forall, exists) uses this shape; every DU in the source is internal. FoldStep<'State> would have been the only public DU, which is the inconsistency @dsyme flagged on the PR. API: - TaskSeq.foldWhile : ('State -> 'T -> bool) -> ('State -> 'T -> 'State) -> 'State -> TaskSeq<'T> -> Task<'State> - TaskSeq.foldWhileAsync : ('State -> 'T -> #Task<bool>) -> ('State -> 'T -> #Task<'State>) -> 'State -> TaskSeq<'T> -> Task<'State> Semantics (match takeWhile, exclusive): the predicate is evaluated against (currentState, nextElement) before that element is folded. If false, iteration halts without folding that element and the source is not enumerated further. Removes: - public FoldStep<'State> DU - internal FoldUntilAction DU - TaskSeq.foldUntil / foldUntilAsync Tests: rewritten for exclusive semantics (halting element not folded, predicate-call / folder-call counts asserted separately). Full test suite green (5277 passed).
5c79074 to
bbf9125
Compare
Hi @dsyme , I kinda favor types over bools, but you are right. Looking at other signatures using bool would be more consistent. I refactored accordingly: foldWhile : ('State -> 'T -> bool) -> ('State -> 'T -> 'State) -> 'State -> TaskSeq<'T> -> Task<'State> |
|
🤖 This is an automated response from Repo Assist. Nice implementation — the semantics are correct and the test suite is thorough. One small code-quality note: In both // foldWhile
if predicate result e.Current then
result <- folder result e.Current // e.Current accessed again
// foldWhileAsync
let! keepGoing = predicate result e.Current
if keepGoing then
let! newState = folder result e.Current // e.Current accessed againWhile // suggested
let item = e.Current
if predicate result item then
result <- folder result itemThis is minor and does not affect correctness. Everything else looks clean.
|
Summary
Adds
TaskSeq.foldWhileandTaskSeq.foldWhileAsync— a fold variant with early termination. A boolean predicate decides, against the current state and next element, whether that element should be folded in. Once the predicate returnsfalse, iteration halts without folding that element and no further elements are pulled from the input (matchestakeWhilesemantics exactly).Motivation
Reaching for
fold/foldAsyncoften ends with a mutable flag or amatch state with Decided -> state | _ -> ...guard at the top of the folder when the caller wants to short-circuit on some condition discovered mid-stream.tryPickAsynchandles the "find and halt" subcase but cannot also thread an accumulator.foldWhilefills the gap.Why not existing operators
tryPickAsync— short-circuits but returns'U option, no accumulator.fold+ mutable flag — works but requires mutation at the call site and drains the whole sequence even after deciding.takeWhileInclusive+toListAsync— nice shape but allocates a list and requires a second pass to inspect.scan+tryFind— loses the final state when no element halts.foldWhileis the shape the language naturally wants for stateful early-exit folds, and matches the existingtakeWhile/skipWhile/tryFindconvention: plainboolpredicate separated from the data function, no new public types.What's in the PR
TaskSeq.foldWhileandTaskSeq.foldWhileAsyncinTaskSeq.fs+ signatures and XML docs inTaskSeq.fsiUnreleasedTaskSeq.FoldWhile.Tests.fs:ArgumentNullExceptionTestEmptyVariants)fold(including left-associativity)foldTest plan
dotnet test ...— full suite: 5277 passed, 0 failed, 2 skippeddotnet fantomas . --check— cleannetstandard2.1Originally proposed as
foldUntilwith aFoldStep<'State>DU (Continue/Halt). Per @dsyme's review, refactored to thebool-predicate form above to match the library's existing early-termination API style.