From b4f9aaf9f4a5301138c77f3eb0cf653377e92208 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 01:30:24 +0000 Subject: [PATCH 1/2] perf: replace ValueOption tracking with while! in pairwise and distinctUntilChanged family pairwise, distinctUntilChanged, distinctUntilChangedWith, and distinctUntilChangedWithAsync previously used a ValueOption mutable and a for-in loop over the source to track the 'previous' element. used in other optimized functions (except, exceptOfSeq, skipWhile, etc.). This avoids the per-element struct match (ValueNone vs ValueSome branch) and is structurally consistent with the rest of the library. All 5251 tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- release-notes.txt | 1 + src/FSharp.Control.TaskSeq/TaskSeqInternal.fs | 79 ++++++++++--------- 2 files changed, 41 insertions(+), 39 deletions(-) diff --git a/release-notes.txt b/release-notes.txt index 72c765f9..aeb6f07b 100644 --- a/release-notes.txt +++ b/release-notes.txt @@ -2,6 +2,7 @@ Release notes: Unreleased + - perf: pairwise, distinctUntilChanged, distinctUntilChangedWith, distinctUntilChangedWithAsync now use explicit enumerator + while! instead of ValueOption tracking + for-in loop, eliminating per-element struct match overhead - test: add SideEffects module and ImmTaskSeq variant tests to TaskSeq.ChunkBy.Tests.fs, improving coverage for chunkBy and chunkByAsync - 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 - refactor: simplify splitAt 'rest' taskSeq to use while!, removing redundant go2 mutable and manual MoveNextAsync pre-advance diff --git a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs index 7e73fb4e..d95200af 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs @@ -1488,73 +1488,74 @@ module internal TaskSeqInternal = checkNonNull (nameof source) source taskSeq { - let mutable maybePrevious = ValueNone + use e = source.GetAsyncEnumerator CancellationToken.None + let! hasFirst = e.MoveNextAsync() - for current in source do - match maybePrevious with - | ValueNone -> - yield current - maybePrevious <- ValueSome current - | ValueSome previous -> - if previous = current then - () // skip - else + if hasFirst then + let mutable previous = e.Current + yield previous + + while! e.MoveNextAsync() do + let current = e.Current + + if current <> previous then yield current - maybePrevious <- ValueSome current + previous <- current } let distinctUntilChangedWith (comparer: 'T -> 'T -> bool) (source: TaskSeq<_>) = checkNonNull (nameof source) source taskSeq { - let mutable maybePrevious = ValueNone + use e = source.GetAsyncEnumerator CancellationToken.None + let! hasFirst = e.MoveNextAsync() - for current in source do - match maybePrevious with - | ValueNone -> - yield current - maybePrevious <- ValueSome current - | ValueSome previous -> - if comparer previous current then - () // skip - else + if hasFirst then + let mutable previous = e.Current + yield previous + + while! e.MoveNextAsync() do + let current = e.Current + + if not (comparer previous current) then yield current - maybePrevious <- ValueSome current + previous <- current } let distinctUntilChangedWithAsync (comparer: 'T -> 'T -> #Task) (source: TaskSeq<_>) = checkNonNull (nameof source) source taskSeq { - let mutable maybePrevious = ValueNone + use e = source.GetAsyncEnumerator CancellationToken.None + let! hasFirst = e.MoveNextAsync() - for current in source do - match maybePrevious with - | ValueNone -> - yield current - maybePrevious <- ValueSome current - | ValueSome previous -> + if hasFirst then + let mutable previous = e.Current + yield previous + + while! e.MoveNextAsync() do + let current = e.Current let! areEqual = comparer previous current - if areEqual then - () // skip - else + if not areEqual then yield current - maybePrevious <- ValueSome current + previous <- current } let pairwise (source: TaskSeq<_>) = checkNonNull (nameof source) source taskSeq { - let mutable maybePrevious = ValueNone + use e = source.GetAsyncEnumerator CancellationToken.None + let! hasFirst = e.MoveNextAsync() - for current in source do - match maybePrevious with - | ValueNone -> maybePrevious <- ValueSome current - | ValueSome previous -> + if hasFirst then + let mutable previous = e.Current + + while! e.MoveNextAsync() do + let current = e.Current yield previous, current - maybePrevious <- ValueSome current + previous <- current } let groupBy (projector: ProjectorAction<'T, 'Key, _>) (source: TaskSeq<_>) = From d5e102bbfabb587c67c1a4741bc1b200b13b67ff Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 27 Apr 2026 01:30:26 +0000 Subject: [PATCH 2/2] ci: trigger checks