From 6ff0e4ae7a1aeb9b88b3479b799b012bb1ede4b4 Mon Sep 17 00:00:00 2001 From: Gustavo Leon <1261319+gusty@users.noreply.github.com> Date: Sun, 13 Nov 2022 09:56:39 +0100 Subject: [PATCH 1/8] + singleton --- src/FSharp.Control.TaskSeq/TaskSeq.fs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fs b/src/FSharp.Control.TaskSeq/TaskSeq.fs index 8e4ae80c..87831249 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeq.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeq.fs @@ -21,6 +21,20 @@ module TaskSeq = } } + let singleton (source: 'T) = + { new IAsyncEnumerable<'T> with + member _.GetAsyncEnumerator(_) = + let mutable started = false + { new IAsyncEnumerator<'T> with + member _.MoveNextAsync () = + let r = ValueTask.FromResult (not started) + started <- true + r + member _.get_Current () : 'T = if started then source else invalidOp "Enumeration has not started. Call MoveNextAsync." + member _.DisposeAsync () = ValueTask.CompletedTask + } + } + let isEmpty source = Internal.isEmpty source // From 51cdad9b78e2a3c88cf3935b1ab9ec82e7053168 Mon Sep 17 00:00:00 2001 From: Gustavo Leon <1261319+gusty@users.noreply.github.com> Date: Sun, 13 Nov 2022 10:00:19 +0100 Subject: [PATCH 2/8] + fsi signature --- src/FSharp.Control.TaskSeq/TaskSeq.fsi | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fsi b/src/FSharp.Control.TaskSeq/TaskSeq.fsi index aeae538d..054d3609 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeq.fsi +++ b/src/FSharp.Control.TaskSeq/TaskSeq.fsi @@ -7,6 +7,9 @@ module TaskSeq = /// Initialize an empty taskSeq. val empty<'T> : taskSeq<'T> + + /// Creates a taskSeq sequence that generates a single element and then ends. + val singleton : source: 'T -> taskSeq<'T> /// /// Returns if the task sequence contains no elements, otherwise. From 12de7f75b312d77f5345d09be10923e14997f7bf Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Thu, 24 Nov 2022 19:45:01 +0100 Subject: [PATCH 3/8] Move implementation of singleton to 'internals' file, update doc comment and remove exception --- src/FSharp.Control.TaskSeq/TaskSeq.fs | 14 +------------- src/FSharp.Control.TaskSeq/TaskSeq.fsi | 8 +++++--- src/FSharp.Control.TaskSeq/TaskSeqInternal.fs | 16 ++++++++++++++++ 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fs b/src/FSharp.Control.TaskSeq/TaskSeq.fs index 87831249..0dbc0c00 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeq.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeq.fs @@ -21,19 +21,7 @@ module TaskSeq = } } - let singleton (source: 'T) = - { new IAsyncEnumerable<'T> with - member _.GetAsyncEnumerator(_) = - let mutable started = false - { new IAsyncEnumerator<'T> with - member _.MoveNextAsync () = - let r = ValueTask.FromResult (not started) - started <- true - r - member _.get_Current () : 'T = if started then source else invalidOp "Enumeration has not started. Call MoveNextAsync." - member _.DisposeAsync () = ValueTask.CompletedTask - } - } + let singleton (source: 'T) = Internal.singleton source let isEmpty source = Internal.isEmpty source diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fsi b/src/FSharp.Control.TaskSeq/TaskSeq.fsi index 054d3609..0d9fe0c9 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeq.fsi +++ b/src/FSharp.Control.TaskSeq/TaskSeq.fsi @@ -7,9 +7,11 @@ module TaskSeq = /// Initialize an empty taskSeq. val empty<'T> : taskSeq<'T> - - /// Creates a taskSeq sequence that generates a single element and then ends. - val singleton : source: 'T -> taskSeq<'T> + + /// + /// Creates a sequence from that generates a single element and then ends. + /// + val singleton: source: 'T -> taskSeq<'T> /// /// Returns if the task sequence contains no elements, otherwise. diff --git a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs index a6499178..636be049 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs @@ -61,6 +61,22 @@ module internal TaskSeqInternal = return not step } + let singleton (source: 'T) = + { new IAsyncEnumerable<'T> with + member _.GetAsyncEnumerator(_) = + let mutable ended = false + + { new IAsyncEnumerator<'T> with + member _.MoveNextAsync() = + let vt = ValueTask.FromResult(not ended) + ended <- true + vt + + member _.Current: 'T = if ended then Unchecked.defaultof<'T> else source + member _.DisposeAsync() = ValueTask.CompletedTask + } + } + /// Returns length unconditionally, or based on a predicate let lengthBy predicate (source: taskSeq<_>) = task { use e = source.GetAsyncEnumerator(CancellationToken()) From a8fd75a124dd1dd2212c74a64c2dbef953a821b0 Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Thu, 24 Nov 2022 19:46:47 +0100 Subject: [PATCH 4/8] Update release notes --- src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj b/src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj index 7c223944..ffe66c2f 100644 --- a/src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj +++ b/src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj @@ -23,8 +23,9 @@ Generates optimized IL code through the new resumable state machines, and comes nuget-package-readme.md Release notes: - 0.2.3 - - improve TaskSeq.empty by not relying on resumable state, #89 + 0.2.3 (unreleased) + - add TaskSeq.singleton, #90 (by @gusty) + - improve TaskSeq.empty by not relying on resumable state, #89 (by @gusty) - do not throw exception for unequal lengths in TaskSeq.zip, fixes #32 0.2.2 - removes TaskSeq.toSeqCachedAsync, which was incorrectly named. Use toSeq or toListAsync instead. From 8ede61bc18f4a58328c706f2478abe90f3d44955 Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Thu, 24 Nov 2022 19:47:58 +0100 Subject: [PATCH 5/8] Remove redundant code --- src/FSharp.Control.TaskSeq/TaskSeq.fs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fs b/src/FSharp.Control.TaskSeq/TaskSeq.fs index 0dbc0c00..fb45a1ae 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeq.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeq.fs @@ -39,10 +39,6 @@ module TaskSeq = e.DisposeAsync().AsTask().Wait() ] - let format x = string x - let f () = format 42 - - let toArray (source: taskSeq<'T>) = [| let e = source.GetAsyncEnumerator(CancellationToken()) From d5c5bbf6d573aab95c448b90fffc02d67e66bd56 Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Thu, 24 Nov 2022 22:28:27 +0100 Subject: [PATCH 6/8] Add tests for `TaskSeq.singleton` --- .../FSharp.Control.TaskSeq.Test.fsproj | 1 + .../TaskSeq.Singleton.Tests.fs | 80 +++++++++++++++++++ src/FSharp.Control.TaskSeq.Test/TestUtils.fs | 5 ++ 3 files changed, 86 insertions(+) create mode 100644 src/FSharp.Control.TaskSeq.Test/TaskSeq.Singleton.Tests.fs diff --git a/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj b/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj index 7eb542e8..fdeb70de 100644 --- a/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj +++ b/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj @@ -39,6 +39,7 @@ + diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Singleton.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Singleton.Tests.fs new file mode 100644 index 00000000..ea2d2368 --- /dev/null +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Singleton.Tests.fs @@ -0,0 +1,80 @@ +module TaskSeq.Tests.Singleton + +open System.Threading.Tasks +open Xunit +open FsUnit.Xunit +open FsToolkit.ErrorHandling + +open FSharp.Control + +module EmptySeq = + + [)>] + let ``TaskSeq-singleton with empty has length one`` variant = + taskSeq { + yield! TaskSeq.singleton 10 + yield! Gen.getEmptyVariant variant + } + |> TaskSeq.exactlyOne + |> Task.map (should equal 10) + +module Other = + [] + let ``TaskSeq-singleton creates a sequence of one`` () = + TaskSeq.singleton 42 + |> TaskSeq.exactlyOne + |> Task.map (should equal 42) + + [] + let ``TaskSeq-singleton can be yielded multiple times`` () = + let singleton = TaskSeq.singleton 42 + + taskSeq { + yield! singleton + yield! singleton + yield! singleton + yield! singleton + } + |> TaskSeq.toList + |> should equal [ 42; 42; 42; 42 ] + + [] + let ``TaskSeq-singleton with isEmpty`` () = + TaskSeq.singleton 42 + |> TaskSeq.isEmpty + |> Task.map (should be False) + + [] + let ``TaskSeq-singleton with append`` () = + TaskSeq.singleton 42 + |> TaskSeq.append (TaskSeq.singleton 42) + |> TaskSeq.toList + |> should equal [ 42; 42 ] + + [)>] + let ``TaskSeq-singleton with collect`` variant = + Gen.getSeqImmutable variant + |> TaskSeq.collect TaskSeq.singleton + |> verify1To10 + + [] + let ``TaskSeq-singleton does not throw when getting Current before MoveNext`` () = task { + let enumerator = (TaskSeq.singleton 42).GetAsyncEnumerator() + let defaultValue = enumerator.Current // should return the default value for int + defaultValue |> should equal 0 + } + + [] + let ``TaskSeq-singleton does not throw when getting Current after last MoveNext`` () = task { + let enumerator = (TaskSeq.singleton 42).GetAsyncEnumerator() + let! isNext = enumerator.MoveNextAsync() + isNext |> should be True + let value = enumerator.Current // the first and only value + value |> should equal 42 + + // move past the end + let! isNext = enumerator.MoveNextAsync() + isNext |> should be False + let defaultValue = enumerator.Current // should return the default value for int + defaultValue |> should equal 0 + } diff --git a/src/FSharp.Control.TaskSeq.Test/TestUtils.fs b/src/FSharp.Control.TaskSeq.Test/TestUtils.fs index 91baf383..dd4873b3 100644 --- a/src/FSharp.Control.TaskSeq.Test/TestUtils.fs +++ b/src/FSharp.Control.TaskSeq.Test/TestUtils.fs @@ -141,6 +141,11 @@ module TestUtils = |> TaskSeq.toArrayAsync |> Task.map (Array.isEmpty >> should be True) + let verify1To10 ts = + ts + |> TaskSeq.toArrayAsync + |> Task.map (should equal [| 1..10 |]) + /// Delays (no spin-wait!) between 20 and 70ms, assuming a 15.6ms resolution clock let longDelay () = task { do! Task.Delay(Random().Next(20, 70)) } From a5cb9207ad6c171eca4d31cf1a875eb1fe4cc20e Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Fri, 25 Nov 2022 00:46:14 +0100 Subject: [PATCH 7/8] Fix code for proper results before/after enumerating singleton --- src/FSharp.Control.TaskSeq/TaskSeqInternal.fs | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs index 636be049..e2f2293a 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs @@ -11,6 +11,12 @@ module ExtraTaskSeqOperators = /// A TaskSeq workflow for IAsyncEnumerable<'T> types. let taskSeq = TaskSeqBuilder() +[] +type AsyncEnumStatus = + | BeforeAll + | WithCurrent + | AfterAll + [] type Action<'T, 'U, 'TaskU when 'TaskU :> Task<'U>> = | CountableAction of countable_action: (int -> 'T -> 'U) @@ -64,15 +70,24 @@ module internal TaskSeqInternal = let singleton (source: 'T) = { new IAsyncEnumerable<'T> with member _.GetAsyncEnumerator(_) = - let mutable ended = false + let mutable status = BeforeAll { new IAsyncEnumerator<'T> with member _.MoveNextAsync() = - let vt = ValueTask.FromResult(not ended) - ended <- true - vt + match status with + | BeforeAll -> + status <- WithCurrent + ValueTask.True + | WithCurrent -> + status <- AfterAll + ValueTask.False + | AfterAll -> ValueTask.False + + member _.Current: 'T = + match status with + | WithCurrent -> source + | _ -> Unchecked.defaultof<'T> - member _.Current: 'T = if ended then Unchecked.defaultof<'T> else source member _.DisposeAsync() = ValueTask.CompletedTask } } From fa6b6c607bb76ad7218996c0cd25e51d612e807d Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Fri, 25 Nov 2022 00:46:32 +0100 Subject: [PATCH 8/8] Add a test for cornercase of multiple MoveNext after the end --- .../TaskSeq.Singleton.Tests.fs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Singleton.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Singleton.Tests.fs index ea2d2368..a916b800 100644 --- a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Singleton.Tests.fs +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Singleton.Tests.fs @@ -78,3 +78,19 @@ module Other = let defaultValue = enumerator.Current // should return the default value for int defaultValue |> should equal 0 } + + [] + let ``TaskSeq-singleton multiple MoveNext is fine`` () = task { + let enumerator = (TaskSeq.singleton 42).GetAsyncEnumerator() + let! isNext = enumerator.MoveNextAsync() + isNext |> should be True + let! _ = enumerator.MoveNextAsync() + let! _ = enumerator.MoveNextAsync() + let! _ = enumerator.MoveNextAsync() + let! isNext = enumerator.MoveNextAsync() + isNext |> should be False + + // should return the default value for int after moving past the end + let defaultValue = enumerator.Current + defaultValue |> should equal 0 + }