From 7bfbdc9e7e2e91f500c54e82b641e778025510b2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 6 Mar 2026 17:15:48 +0000 Subject: [PATCH 1/2] =?UTF-8?q?Add=20AsyncSeq.removeAt,=20updateAt,=20inse?= =?UTF-8?q?rtAt=20=E2=80=94=20mirror=20Seq=20combinators?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These three F# 6.0 Seq-mirroring combinators enable in-place mutation-style operations on async sequences: - removeAt: skip the element at the given index - updateAt: replace the element at the given index with a new value - insertAt: insert a value before the element at the given index (or append when index == length) All three raise ArgumentException for negative indices. insertAt also raises ArgumentException when index exceeds the sequence length. 14 new tests added; 301/301 pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- RELEASE_NOTES.md | 6 + src/FSharp.Control.AsyncSeq/AsyncSeq.fs | 26 ++++ src/FSharp.Control.AsyncSeq/AsyncSeq.fsi | 13 ++ .../AsyncSeqTests.fs | 132 ++++++++++++++++++ version.props | 2 +- 5 files changed, 178 insertions(+), 1 deletion(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 5b078ca..1544f75 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,3 +1,9 @@ +### 4.7.0 + +* Added `AsyncSeq.removeAt` — returns a new sequence with the element at the specified index removed, mirroring `Seq.removeAt`. +* Added `AsyncSeq.updateAt` — returns a new sequence with the element at the specified index replaced by a given value, mirroring `Seq.updateAt`. +* Added `AsyncSeq.insertAt` — returns a new sequence with a value inserted before the element at the specified index (or appended if the index equals the sequence length), mirroring `Seq.insertAt`. + ### 4.6.0 * Added `AsyncSeq.isEmpty` — returns `true` if the sequence contains no elements; short-circuits after the first element, mirroring `Seq.isEmpty`. diff --git a/src/FSharp.Control.AsyncSeq/AsyncSeq.fs b/src/FSharp.Control.AsyncSeq/AsyncSeq.fs index c15e802..4ce71be 100644 --- a/src/FSharp.Control.AsyncSeq/AsyncSeq.fs +++ b/src/FSharp.Control.AsyncSeq/AsyncSeq.fs @@ -1432,6 +1432,32 @@ module AsyncSeq = let s = System.Collections.Generic.HashSet(excluded) source |> filter (fun x -> not (s.Contains(x))) + let removeAt (index : int) (source : AsyncSeq<'T>) : AsyncSeq<'T> = asyncSeq { + if index < 0 then invalidArg "index" "must be non-negative" + let i = ref 0 + for x in source do + if i.Value <> index then yield x + i := i.Value + 1 } + + let updateAt (index : int) (value : 'T) (source : AsyncSeq<'T>) : AsyncSeq<'T> = asyncSeq { + if index < 0 then invalidArg "index" "must be non-negative" + let i = ref 0 + for x in source do + if i.Value = index then yield value + else yield x + i := i.Value + 1 } + + let insertAt (index : int) (value : 'T) (source : AsyncSeq<'T>) : AsyncSeq<'T> = asyncSeq { + if index < 0 then invalidArg "index" "must be non-negative" + let i = ref 0 + for x in source do + if i.Value = index then yield value + yield x + i := i.Value + 1 + if i.Value = index then yield value + elif i.Value < index then + invalidArg "index" "The index is outside the range of elements in the collection." } + #if !FABLE_COMPILER let iterAsyncParallel (f:'a -> Async) (s:AsyncSeq<'a>) : Async = async { use mb = MailboxProcessor.Start (ignore >> async.Return) diff --git a/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi b/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi index 7be6bf8..914b9c8 100644 --- a/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi +++ b/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi @@ -408,6 +408,19 @@ module AsyncSeq = /// in the given excluded collection. Uses a HashSet for O(1) lookup. Mirrors Seq.except. val except : excluded:seq<'T> -> source:AsyncSeq<'T> -> AsyncSeq<'T> when 'T : equality + /// Returns a new asynchronous sequence with the element at the specified index removed. + /// Raises ArgumentException if index is negative. Mirrors Seq.removeAt. + val removeAt : index:int -> source:AsyncSeq<'T> -> AsyncSeq<'T> + + /// Returns a new asynchronous sequence with the element at the specified index replaced by the given value. + /// Raises ArgumentException if index is negative. Mirrors Seq.updateAt. + val updateAt : index:int -> value:'T -> source:AsyncSeq<'T> -> AsyncSeq<'T> + + /// Returns a new asynchronous sequence with the given value inserted before the element at the specified index. + /// An index equal to the length of the sequence appends the value at the end. + /// Raises ArgumentException if index is negative or greater than the sequence length. Mirrors Seq.insertAt. + val insertAt : index:int -> value:'T -> source:AsyncSeq<'T> -> AsyncSeq<'T> + /// Creates an asynchronous sequence that lazily takes element from an /// input synchronous sequence and returns them one-by-one. val ofSeq : source:seq<'T> -> AsyncSeq<'T> diff --git a/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs b/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs index 74c43c7..7e163c4 100644 --- a/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs +++ b/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs @@ -3408,3 +3408,135 @@ let ``AsyncSeq.sortWith sorts descending with negated comparer`` () = let ``AsyncSeq.sortWith returns empty array for empty sequence`` () = let result = AsyncSeq.sortWith compare AsyncSeq.empty Assert.AreEqual([||], result) + +// ===== removeAt ===== + +[] +let ``AsyncSeq.removeAt removes the element at the specified index`` () = + let result = + AsyncSeq.ofSeq [ 1; 2; 3; 4; 5 ] + |> AsyncSeq.removeAt 2 + |> AsyncSeq.toArrayAsync + |> Async.RunSynchronously + Assert.AreEqual([| 1; 2; 4; 5 |], result) + +[] +let ``AsyncSeq.removeAt removes the first element (index 0)`` () = + let result = + AsyncSeq.ofSeq [ 10; 20; 30 ] + |> AsyncSeq.removeAt 0 + |> AsyncSeq.toArrayAsync + |> Async.RunSynchronously + Assert.AreEqual([| 20; 30 |], result) + +[] +let ``AsyncSeq.removeAt removes the last element`` () = + let result = + AsyncSeq.ofSeq [ 1; 2; 3 ] + |> AsyncSeq.removeAt 2 + |> AsyncSeq.toArrayAsync + |> Async.RunSynchronously + Assert.AreEqual([| 1; 2 |], result) + +[] +let ``AsyncSeq.removeAt raises ArgumentException for negative index`` () = + Assert.Throws(fun () -> + AsyncSeq.ofSeq [ 1; 2; 3 ] + |> AsyncSeq.removeAt -1 + |> AsyncSeq.toArrayAsync + |> Async.RunSynchronously |> ignore) + |> ignore + +// ===== updateAt ===== + +[] +let ``AsyncSeq.updateAt replaces element at specified index`` () = + let result = + AsyncSeq.ofSeq [ 1; 2; 3; 4 ] + |> AsyncSeq.updateAt 1 99 + |> AsyncSeq.toArrayAsync + |> Async.RunSynchronously + Assert.AreEqual([| 1; 99; 3; 4 |], result) + +[] +let ``AsyncSeq.updateAt replaces first element`` () = + let result = + AsyncSeq.ofSeq [ 1; 2; 3 ] + |> AsyncSeq.updateAt 0 99 + |> AsyncSeq.toArrayAsync + |> Async.RunSynchronously + Assert.AreEqual([| 99; 2; 3 |], result) + +[] +let ``AsyncSeq.updateAt replaces last element`` () = + let result = + AsyncSeq.ofSeq [ 1; 2; 3 ] + |> AsyncSeq.updateAt 2 99 + |> AsyncSeq.toArrayAsync + |> Async.RunSynchronously + Assert.AreEqual([| 1; 2; 99 |], result) + +[] +let ``AsyncSeq.updateAt raises ArgumentException for negative index`` () = + Assert.Throws(fun () -> + AsyncSeq.ofSeq [ 1; 2; 3 ] + |> AsyncSeq.updateAt -1 0 + |> AsyncSeq.toArrayAsync + |> Async.RunSynchronously |> ignore) + |> ignore + +// ===== insertAt ===== + +[] +let ``AsyncSeq.insertAt inserts element at specified index`` () = + let result = + AsyncSeq.ofSeq [ 1; 2; 4; 5 ] + |> AsyncSeq.insertAt 2 3 + |> AsyncSeq.toArrayAsync + |> Async.RunSynchronously + Assert.AreEqual([| 1; 2; 3; 4; 5 |], result) + +[] +let ``AsyncSeq.insertAt inserts at index 0 (prepend)`` () = + let result = + AsyncSeq.ofSeq [ 2; 3 ] + |> AsyncSeq.insertAt 0 1 + |> AsyncSeq.toArrayAsync + |> Async.RunSynchronously + Assert.AreEqual([| 1; 2; 3 |], result) + +[] +let ``AsyncSeq.insertAt appends when index equals sequence length`` () = + let result = + AsyncSeq.ofSeq [ 1; 2; 3 ] + |> AsyncSeq.insertAt 3 4 + |> AsyncSeq.toArrayAsync + |> Async.RunSynchronously + Assert.AreEqual([| 1; 2; 3; 4 |], result) + +[] +let ``AsyncSeq.insertAt inserts into empty sequence at index 0`` () = + let result = + AsyncSeq.empty + |> AsyncSeq.insertAt 0 42 + |> AsyncSeq.toArrayAsync + |> Async.RunSynchronously + Assert.AreEqual([| 42 |], result) + +[] +let ``AsyncSeq.insertAt raises ArgumentException for negative index`` () = + Assert.Throws(fun () -> + AsyncSeq.ofSeq [ 1; 2; 3 ] + |> AsyncSeq.insertAt -1 0 + |> AsyncSeq.toArrayAsync + |> Async.RunSynchronously |> ignore) + |> ignore + +[] +let ``AsyncSeq.insertAt raises ArgumentException when index exceeds length`` () = + Assert.Throws(fun () -> + AsyncSeq.ofSeq [ 1; 2; 3 ] + |> AsyncSeq.insertAt 5 0 + |> AsyncSeq.toArrayAsync + |> Async.RunSynchronously |> ignore) + |> ignore diff --git a/version.props b/version.props index 3dedc1d..0f4359a 100644 --- a/version.props +++ b/version.props @@ -1,5 +1,5 @@ - 4.6.0 + 4.7.0 From 845b999cff0c2c205fcd60de35ddd998dbd4bb49 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 6 Mar 2026 17:18:42 +0000 Subject: [PATCH 2/2] ci: trigger checks