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..a916b800 --- /dev/null +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Singleton.Tests.fs @@ -0,0 +1,96 @@ +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 + } + + [] + 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 + } 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)) } 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. diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fs b/src/FSharp.Control.TaskSeq/TaskSeq.fs index 8e4ae80c..fb45a1ae 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeq.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeq.fs @@ -21,6 +21,8 @@ module TaskSeq = } } + let singleton (source: 'T) = Internal.singleton source + let isEmpty source = Internal.isEmpty source // @@ -37,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()) diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fsi b/src/FSharp.Control.TaskSeq/TaskSeq.fsi index aeae538d..0d9fe0c9 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeq.fsi +++ b/src/FSharp.Control.TaskSeq/TaskSeq.fsi @@ -8,6 +8,11 @@ module TaskSeq = /// Initialize an empty taskSeq. val empty<'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..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) @@ -61,6 +67,31 @@ module internal TaskSeqInternal = return not step } + let singleton (source: 'T) = + { new IAsyncEnumerable<'T> with + member _.GetAsyncEnumerator(_) = + let mutable status = BeforeAll + + { new IAsyncEnumerator<'T> with + member _.MoveNextAsync() = + 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 _.DisposeAsync() = ValueTask.CompletedTask + } + } + /// Returns length unconditionally, or based on a predicate let lengthBy predicate (source: taskSeq<_>) = task { use e = source.GetAsyncEnumerator(CancellationToken())