Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/aw/actions-lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@
"version": "v9.0.0",
"sha": "3a2844b7e9c422d3c10d287c895573f7108da1b3"
},
"github/gh-aw-actions/setup@v0.86.2": {
"github/gh-aw-actions/setup@v0.88.7": {
"repo": "github/gh-aw-actions/setup",
"version": "v0.86.2",
"sha": "6aab9e5b5c91c615506061f09bedd81a23babe3c"
"version": "v0.88.7",
"sha": "5e508589e03a7757a7e05b26e834292f5445bfb6"
}
},
"containers": {
Expand Down
776 changes: 508 additions & 268 deletions .github/workflows/repo-assist.lock.yml

Large diffs are not rendered by default.

172 changes: 138 additions & 34 deletions .github/workflows/repo-assist.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

* Test coverage: Added tests for previously-untested public API functions `AsyncSeq.tryFirst`, `AsyncSeq.firstOrDefault`, `AsyncSeq.zipWithParallel`, `AsyncSeq.combineLatestWithAsync`, and `AsyncSeq.toObservable`. No functional changes.
* Fixed Fable CI build: `Microsoft.Bcl.AsyncInterfaces` was pinned to a specific version (`10.0.7`) that was older than the version resolved transitively via `System.Threading.Channels`, causing a `NU1605` package downgrade error that made Fable's project cracker fail during `dotnet fable`. The reference now uses `Version="*"` (matching `System.Threading.Channels`) so both resolve consistently. (#334)
* Tests: Added comprehensive tests for `AsyncSeq.zapp`, `AsyncSeq.zappAsync`, and `AsyncSeq.compareWithAsync`, which previously had no dedicated test coverage.
* Performance: Optimised `AsyncSeq.distinctUntilChangedWithAsync` (and thus `distinctUntilChangedWith` / `distinctUntilChanged`) to track the previous element with a `hasPrev` flag and a direct `mutable` field instead of wrapping it in a `'T option`. Previously each iteration heap-allocated a new `Some` box; the new implementation eliminates that allocation, matching the pattern already used by `pairwise`.

### 4.17.0

Expand Down
14 changes: 8 additions & 6 deletions src/FSharp.Control.AsyncSeq/AsyncSeq.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2740,16 +2740,18 @@ module AsyncSeq =
use ie = source.GetEnumerator()
let! move = ie.MoveNext()
let mutable b = move
let mutable prev = None
// Use a flag + mutable field instead of Option to avoid per-element heap allocation
let mutable hasPrev = false
let mutable prev = Unchecked.defaultof<'T>
while b.IsSome do
let v = b.Value
match prev with
| None ->
if not hasPrev then
yield v
| Some p ->
let! changed = f p v
else
let! changed = f prev v
if not changed then yield v
prev <- Some v
hasPrev <- true
prev <- v
let! moven = ie.MoveNext()
b <- moven }

Expand Down
99 changes: 99 additions & 0 deletions tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -5178,3 +5178,102 @@ let ``AsyncSeq.toObservable on empty sequence emits nothing`` () =
use _sub = (AsyncSeq.toObservable (AsyncSeq.empty<int>)).Subscribe(observer)
Assert.IsTrue(completedEvent.Wait(2000))
Assert.AreEqual([||], received.ToArray())

// ===== zapp / zappAsync =====

[<Test>]
let ``AsyncSeq.zapp applies functions to corresponding elements`` () =
let fs = asyncSeq { yield (fun x -> x + 10); yield (fun x -> x * 2); yield (fun x -> x - 1) }
let vs = asyncSeq { yield 1; yield 2; yield 3 }
let result = AsyncSeq.zapp fs vs |> AsyncSeq.toArrayAsync |> Async.RunSynchronously
Assert.AreEqual([| 11; 4; 2 |], result)

[<Test>]
let ``AsyncSeq.zapp stops when functions run out`` () =
let fs = asyncSeq { yield (fun x -> x + 1); yield (fun x -> x + 2) }
let vs = asyncSeq { yield 10; yield 20; yield 30 }
let result = AsyncSeq.zapp fs vs |> AsyncSeq.toArrayAsync |> Async.RunSynchronously
Assert.AreEqual([| 11; 22 |], result)

[<Test>]
let ``AsyncSeq.zapp stops when values run out`` () =
let fs = asyncSeq { yield (fun x -> x + 1); yield (fun x -> x + 2); yield (fun x -> x + 3) }
let vs = asyncSeq { yield 5 }
let result = AsyncSeq.zapp fs vs |> AsyncSeq.toArrayAsync |> Async.RunSynchronously
Assert.AreEqual([| 6 |], result)

[<Test>]
let ``AsyncSeq.zapp on empty functions returns empty`` () =
let fs = AsyncSeq.empty<int -> int>
let vs = asyncSeq { yield 1; yield 2; yield 3 }
let result = AsyncSeq.zapp fs vs |> AsyncSeq.toArrayAsync |> Async.RunSynchronously
Assert.AreEqual([||], result)

[<Test>]
let ``AsyncSeq.zappAsync applies async functions to corresponding elements`` () =
let fs = asyncSeq {
yield (fun x -> async { return x + 10 })
yield (fun x -> async { return x * 3 })
}
let vs = asyncSeq { yield 5; yield 4 }
let result = AsyncSeq.zappAsync fs vs |> AsyncSeq.toArrayAsync |> Async.RunSynchronously
Assert.AreEqual([| 15; 12 |], result)

[<Test>]
let ``AsyncSeq.zappAsync on empty source returns empty`` () =
let fs = asyncSeq { yield (fun x -> async { return x + 1 }) }
let vs = AsyncSeq.empty<int>
let result = AsyncSeq.zappAsync fs vs |> AsyncSeq.toArrayAsync |> Async.RunSynchronously
Assert.AreEqual([||], result)

// ===== compareWithAsync =====

[<Test>]
let ``AsyncSeq.compareWithAsync equal sequences returns 0`` () =
let result =
AsyncSeq.compareWithAsync
(fun a b -> async { return compare a b })
(AsyncSeq.ofSeq [1;2;3])
(AsyncSeq.ofSeq [1;2;3])
|> Async.RunSynchronously
Assert.AreEqual(0, result)

[<Test>]
let ``AsyncSeq.compareWithAsync shorter is less than longer`` () =
let result =
AsyncSeq.compareWithAsync
(fun a b -> async { return compare a b })
(AsyncSeq.ofSeq [1;2])
(AsyncSeq.ofSeq [1;2;3])
|> Async.RunSynchronously
Assert.IsTrue(result < 0)

[<Test>]
let ``AsyncSeq.compareWithAsync longer is greater than shorter`` () =
let result =
AsyncSeq.compareWithAsync
(fun a b -> async { return compare a b })
(AsyncSeq.ofSeq [1;2;3])
(AsyncSeq.ofSeq [1;2])
|> Async.RunSynchronously
Assert.IsTrue(result > 0)

[<Test>]
let ``AsyncSeq.compareWithAsync lexicographic difference`` () =
let result =
AsyncSeq.compareWithAsync
(fun a b -> async { return compare a b })
(AsyncSeq.ofSeq [1;3])
(AsyncSeq.ofSeq [1;2])
|> Async.RunSynchronously
Assert.IsTrue(result > 0)

[<Test>]
let ``AsyncSeq.compareWithAsync empty sequences returns 0`` () =
let result =
AsyncSeq.compareWithAsync
(fun a b -> async { return compare a b })
AsyncSeq.empty<int>
AsyncSeq.empty<int>
|> Async.RunSynchronously
Assert.AreEqual(0, result)