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())