From 8a72218f753a3903fc3ccd383f570ee0fb3e2979 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 01:18:45 +0000 Subject: [PATCH] perf: use while! in groupBy, countBy, partition, except, exceptOfSeq Replaces the manual go-flag + initial-MoveNext pre-advance pattern with idiomatic while! in five functions: groupBy, countBy, partition, except, and exceptOfSeq. The pattern being replaced (groupBy example): let! step = e.MoveNextAsync() let mutable go = step while go do // body let! step = e.MoveNextAsync() go <- step After: while! e.MoveNextAsync() do // body For except/exceptOfSeq the first-element check is also cleaned up: let! hasFirst = e.MoveNextAsync() if hasFirst then if hashSet.Add e.Current then yield e.Current while! e.MoveNextAsync() do ... All changes are purely internal - no public API or behaviour change. Verified: 5093 tests passed, 0 failures, 0 warnings, formatting OK. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- release-notes.txt | 1 + src/FSharp.Control.TaskSeq/TaskSeqInternal.fs | 73 ++++++------------- 2 files changed, 24 insertions(+), 50 deletions(-) diff --git a/release-notes.txt b/release-notes.txt index d07e4fc..d7e3fcc 100644 --- a/release-notes.txt +++ b/release-notes.txt @@ -2,6 +2,7 @@ Release notes: 1.0.0 + - perf: use while! in groupBy, countBy, partition, except, exceptOfSeq to eliminate redundant mutable 'go' variables and initial MoveNextAsync calls - adds taskSeqDynamic computation expression and TaskSeqDynamic/TaskSeqDynamicInfo types for dynamic (FSI-compatible) resumable code, fixing issue where taskSeq would raise NotImplementedException in F# Interactive, #246 - perf: TaskSeq.chunkBy and chunkByAsync reuse the ResizeArray buffer between chunks, reducing allocations on sequences with many chunk boundaries - fixes: TaskSeq.insertAt, insertManyAt, removeAt, removeManyAt, updateAt now raise ArgumentNullException (not NullReferenceException) when given a null source; insertManyAt also validates the values argument diff --git a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs index 261dbba..0baf290 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs @@ -1462,35 +1462,29 @@ module internal TaskSeqInternal = taskSeq { use e = source.GetAsyncEnumerator CancellationToken.None - let mutable go = true - let! step = e.MoveNextAsync() - go <- step + let! hasFirst = e.MoveNextAsync() - if step then + if hasFirst then // only create hashset by the time we actually start iterating; // taskSeq enumerates sequentially, so a plain HashSet suffices — no locking needed. let hashSet = HashSet<_>(HashIdentity.Structural) use excl = itemsToExclude.GetAsyncEnumerator CancellationToken.None - let! exclStep = excl.MoveNextAsync() - let mutable exclGo = exclStep - while exclGo do + while! excl.MoveNextAsync() do hashSet.Add excl.Current |> ignore - let! exclStep = excl.MoveNextAsync() - exclGo <- exclStep - while go do + // if true, it was added, and therefore unique, so we return it + // if false, it existed, and therefore a duplicate, and we skip + if hashSet.Add e.Current then + yield e.Current + + while! e.MoveNextAsync() do let current = e.Current - // if true, it was added, and therefore unique, so we return it - // if false, it existed, and therefore a duplicate, and we skip if hashSet.Add current then yield current - let! step = e.MoveNextAsync() - go <- step - } let exceptOfSeq itemsToExclude (source: TaskSeq<_>) = @@ -1499,26 +1493,24 @@ module internal TaskSeqInternal = taskSeq { use e = source.GetAsyncEnumerator CancellationToken.None - let mutable go = true - let! step = e.MoveNextAsync() - go <- step + let! hasFirst = e.MoveNextAsync() - if step then + if hasFirst then // only create hashset by the time we actually start iterating; // initialize directly from the seq — taskSeq is sequential so no locking needed. let hashSet = HashSet<_>(itemsToExclude, HashIdentity.Structural) - while go do + // if true, it was added, and therefore unique, so we return it + // if false, it existed, and therefore a duplicate, and we skip + if hashSet.Add e.Current then + yield e.Current + + while! e.MoveNextAsync() do let current = e.Current - // if true, it was added, and therefore unique, so we return it - // if false, it existed, and therefore a duplicate, and we skip if hashSet.Add current then yield current - let! step = e.MoveNextAsync() - go <- step - } let distinctUntilChanged (source: TaskSeq<_>) = @@ -1601,12 +1593,10 @@ module internal TaskSeqInternal = use e = source.GetAsyncEnumerator CancellationToken.None let groups = Dictionary<'Key, ResizeArray<'T>>(HashIdentity.Structural) let order = ResizeArray<'Key>() - let! step = e.MoveNextAsync() - let mutable go = step match projector with | ProjectorAction proj -> - while go do + while! e.MoveNextAsync() do let key = proj e.Current let mutable ra = Unchecked.defaultof<_> @@ -1616,11 +1606,9 @@ module internal TaskSeqInternal = order.Add key ra.Add e.Current - let! step = e.MoveNextAsync() - go <- step | AsyncProjectorAction proj -> - while go do + while! e.MoveNextAsync() do let! key = proj e.Current let mutable ra = Unchecked.defaultof<_> @@ -1630,8 +1618,6 @@ module internal TaskSeqInternal = order.Add key ra.Add e.Current - let! step = e.MoveNextAsync() - go <- step return Array.init order.Count (fun i -> @@ -1646,12 +1632,10 @@ module internal TaskSeqInternal = use e = source.GetAsyncEnumerator CancellationToken.None let counts = Dictionary<'Key, int>(HashIdentity.Structural) let order = ResizeArray<'Key>() - let! step = e.MoveNextAsync() - let mutable go = step match projector with | ProjectorAction proj -> - while go do + while! e.MoveNextAsync() do let key = proj e.Current let mutable count = 0 @@ -1659,11 +1643,9 @@ module internal TaskSeqInternal = order.Add key counts[key] <- count + 1 - let! step = e.MoveNextAsync() - go <- step | AsyncProjectorAction proj -> - while go do + while! e.MoveNextAsync() do let! key = proj e.Current let mutable count = 0 @@ -1671,8 +1653,6 @@ module internal TaskSeqInternal = order.Add key counts[key] <- count + 1 - let! step = e.MoveNextAsync() - go <- step return Array.init order.Count (fun i -> let k = order[i] in k, counts[k]) } @@ -1684,12 +1664,10 @@ module internal TaskSeqInternal = use e = source.GetAsyncEnumerator CancellationToken.None let trueItems = ResizeArray<'T>() let falseItems = ResizeArray<'T>() - let! step = e.MoveNextAsync() - let mutable go = step match predicate with | Predicate pred -> - while go do + while! e.MoveNextAsync() do let item = e.Current if pred item then @@ -1697,16 +1675,11 @@ module internal TaskSeqInternal = else falseItems.Add item - let! step = e.MoveNextAsync() - go <- step - | PredicateAsync pred -> - while go do + while! e.MoveNextAsync() do let item = e.Current let! result = pred item if result then trueItems.Add item else falseItems.Add item - let! step = e.MoveNextAsync() - go <- step return trueItems.ToArray(), falseItems.ToArray() }