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 2fafcf30..611663b8 100644
--- a/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj
+++ b/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj
@@ -48,6 +48,8 @@
+
+
diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Do.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Do.Tests.fs
new file mode 100644
index 00000000..d979f6fa
--- /dev/null
+++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Do.Tests.fs
@@ -0,0 +1,58 @@
+module TaskSeq.Tests.Do
+
+open System
+open System.Threading.Tasks
+open FsUnit
+open Xunit
+
+open FSharp.Control
+
+[]
+let ``CE taskSeq: use 'do'`` () =
+ let mutable value = 0
+
+ taskSeq { do value <- value + 1 } |> verifyEmpty
+
+[]
+let ``CE taskSeq: use 'do!' with a task`` () =
+ let mutable value = 0
+
+ taskSeq { do! task { do value <- value + 1 } }
+ |> verifyEmpty
+ |> Task.map (fun _ -> value |> should equal 1)
+
+[]
+let ``CE taskSeq: use 'do!' with a valuetask`` () =
+ let mutable value = 0
+
+ taskSeq { do! ValueTask.ofTask (task { do value <- value + 1 }) }
+ |> verifyEmpty
+ |> Task.map (fun _ -> value |> should equal 1)
+
+[]
+let ``CE taskSeq: use 'do!' with a non-generic valuetask`` () =
+ let mutable value = 0
+
+ taskSeq { do! ValueTask(task { do value <- value + 1 }) }
+ |> verifyEmpty
+ |> Task.map (fun _ -> value |> should equal 1)
+
+[]
+let ``CE taskSeq: use 'do!' with a non-generic task`` () =
+ let mutable value = 0
+
+ taskSeq { do! (task { do value <- value + 1 }) |> Task.ignore }
+ |> verifyEmpty
+ |> Task.map (fun _ -> value |> should equal 1)
+
+[]
+let ``CE taskSeq: use 'do!' with a task-delay`` () =
+ let mutable value = 0
+
+ taskSeq {
+ do value <- value + 1
+ do! Task.Delay 50
+ do value <- value + 1
+ }
+ |> verifyEmpty
+ |> Task.map (fun _ -> value |> should equal 2)
diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Let.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Let.Tests.fs
new file mode 100644
index 00000000..a4b9b66d
--- /dev/null
+++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Let.Tests.fs
@@ -0,0 +1,82 @@
+module TaskSeq.Tests.Let
+
+open System
+open System.Threading.Tasks
+open FsUnit
+open Xunit
+
+open FSharp.Control
+
+[]
+let ``CE taskSeq: use 'let'`` () =
+ let mutable value = 0
+
+ taskSeq {
+ let value1 = value + 1
+ let value2 = value1 + 1
+ yield value2
+ }
+ |> TaskSeq.exactlyOne
+ |> Task.map (should equal 2)
+
+[]
+let ``CE taskSeq: use 'let!' with a task`` () =
+ let mutable value = 0
+
+ taskSeq {
+ let! unit' = task { do value <- value + 1 }
+ do unit'
+ }
+ |> verifyEmpty
+ |> Task.map (fun _ -> value |> should equal 1)
+
+[]
+let ``CE taskSeq: use 'let!' with a task`` () =
+ taskSeq {
+ let! test = task { return "test" }
+ yield test
+ }
+ |> TaskSeq.exactlyOne
+ |> Task.map (should equal "test")
+
+[]
+let ``CE taskSeq: use 'let!' with a valuetask`` () =
+ let mutable value = 0
+
+ taskSeq {
+ let! unit' = ValueTask.ofTask (task { do value <- value + 1 })
+ do unit'
+ }
+ |> verifyEmpty
+ |> Task.map (fun _ -> value |> should equal 1)
+
+[]
+let ``CE taskSeq: use 'let!' with a valuetask`` () =
+ taskSeq {
+ let! test = ValueTask.ofTask (task { return "test" })
+ yield test
+ }
+ |> TaskSeq.exactlyOne
+ |> Task.map (should equal "test")
+
+[]
+let ``CE taskSeq: use 'let!' with a non-generic valuetask`` () =
+ let mutable value = 0
+
+ taskSeq {
+ let! unit' = ValueTask(task { do value <- value + 1 })
+ do unit'
+ }
+ |> verifyEmpty
+ |> Task.map (fun _ -> value |> should equal 1)
+
+[]
+let ``CE taskSeq: use 'let!' with a non-generic task`` () =
+ let mutable value = 0
+
+ taskSeq {
+ let! unit' = (task { do value <- value + 1 }) |> Task.ignore
+ do unit'
+ }
+ |> verifyEmpty
+ |> Task.map (fun _ -> value |> should equal 1)
diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Using.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Using.Tests.fs
index 07c7c247..8ef92f5d 100644
--- a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Using.Tests.fs
+++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Using.Tests.fs
@@ -1,4 +1,4 @@
-module FSharp.Control.TaskSeq.Test
+module TaskSeq.Test.Using
open System
open System.Threading.Tasks
@@ -34,7 +34,7 @@ type private MultiDispose(disposed: int ref) =
let private check = TaskSeq.length >> Task.map (should equal 1)
[]
-let ``CE task: Using when type implements IDisposable`` () =
+let ``CE taskSeq: Using when type implements IDisposable`` () =
let disposed = ref false
let ts = taskSeq {
@@ -46,7 +46,7 @@ let ``CE task: Using when type implements IDisposable`` () =
|> Task.map (fun _ -> disposed.Value |> should be True)
[]
-let ``CE task: Using when type implements IAsyncDisposable`` () =
+let ``CE taskSeq: Using when type implements IAsyncDisposable`` () =
let disposed = ref false
let ts = taskSeq {
@@ -58,7 +58,7 @@ let ``CE task: Using when type implements IAsyncDisposable`` () =
|> Task.map (fun _ -> disposed.Value |> should be True)
[]
-let ``CE task: Using when type implements IDisposable and IAsyncDisposable`` () =
+let ``CE taskSeq: Using when type implements IDisposable and IAsyncDisposable`` () =
let disposed = ref 0
let ts = taskSeq {
@@ -70,7 +70,7 @@ let ``CE task: Using when type implements IDisposable and IAsyncDisposable`` ()
|> Task.map (fun _ -> disposed.Value |> should equal -1) // should prefer IAsyncDisposable, which returns -1
[]
-let ``CE task: Using! when type implements IDisposable`` () =
+let ``CE taskSeq: Using! when type implements IDisposable`` () =
let disposed = ref false
let ts = taskSeq {
@@ -82,7 +82,7 @@ let ``CE task: Using! when type implements IDisposable`` () =
|> Task.map (fun _ -> disposed.Value |> should be True)
[]
-let ``CE task: Using! when type implements IAsyncDisposable`` () =
+let ``CE taskSeq: Using! when type implements IAsyncDisposable`` () =
let disposed = ref false
let ts = taskSeq {
@@ -94,7 +94,7 @@ let ``CE task: Using! when type implements IAsyncDisposable`` () =
|> Task.map (fun _ -> disposed.Value |> should be True)
[]
-let ``CE task: Using! when type implements IDisposable and IAsyncDisposable`` () =
+let ``CE taskSeq: Using! when type implements IDisposable and IAsyncDisposable`` () =
let disposed = ref 0
let ts = taskSeq {
diff --git a/src/FSharp.Control.TaskSeq.Test/TestUtils.fs b/src/FSharp.Control.TaskSeq.Test/TestUtils.fs
index 28b36031..36244708 100644
--- a/src/FSharp.Control.TaskSeq.Test/TestUtils.fs
+++ b/src/FSharp.Control.TaskSeq.Test/TestUtils.fs
@@ -136,11 +136,13 @@ type DummyTaskFactory(µsecMin: int64<µs>, µsecMax: int64<µs>) =
[]
module TestUtils =
+ /// Verifies that a task sequence is empty by converting to an array and checking emptiness.
let verifyEmpty ts =
ts
|> TaskSeq.toArrayAsync
|> Task.map (Array.isEmpty >> should be True)
+ /// Verifies that a task sequence contains integers 1-10, by converting to an array and comparing.
let verify1To10 ts =
ts
|> TaskSeq.toArrayAsync
diff --git a/src/FSharp.Control.TaskSeq/TaskSeqBuilder.fs b/src/FSharp.Control.TaskSeq/TaskSeqBuilder.fs
index a372b14f..ea154804 100644
--- a/src/FSharp.Control.TaskSeq/TaskSeqBuilder.fs
+++ b/src/FSharp.Control.TaskSeq/TaskSeqBuilder.fs
@@ -505,56 +505,6 @@ type TaskSeqBuilder() =
sm.Data.awaiter <- null
__stack_fin)
- member inline _.Bind(task: Task<'TResult1>, continuation: ('TResult1 -> TaskSeqCode<'T>)) : TaskSeqCode<'T> =
- TaskSeqCode<'T>(fun sm ->
- let mutable awaiter = task.GetAwaiter()
- let mutable __stack_fin = true
-
- Debug.logInfo "at Bind"
-
- if not awaiter.IsCompleted then
- // This will yield with __stack_fin2 = false
- // This will resume with __stack_fin2 = true
- let __stack_fin2 = ResumableCode.Yield().Invoke(&sm)
- __stack_fin <- __stack_fin2
-
- Debug.logInfo ("at Bind: with __stack_fin = ", __stack_fin)
- Debug.logInfo ("at Bind: this.completed = ", sm.Data.completed)
-
- if __stack_fin then
- let result = awaiter.GetResult()
- (continuation result).Invoke(&sm)
-
- else
- Debug.logInfo "at Bind: calling AwaitUnsafeOnCompleted"
-
- sm.Data.awaiter <- awaiter
- sm.Data.current <- ValueNone
- false)
-
- member inline _.Bind(task: ValueTask<'TResult1>, continuation: ('TResult1 -> TaskSeqCode<'T>)) : TaskSeqCode<'T> =
- TaskSeqCode<'T>(fun sm ->
- let mutable awaiter = task.GetAwaiter()
- let mutable __stack_fin = true
-
- Debug.logInfo "at BindV"
-
- if not awaiter.IsCompleted then
- // This will yield with __stack_fin2 = false
- // This will resume with __stack_fin2 = true
- let __stack_fin2 = ResumableCode.Yield().Invoke(&sm)
- __stack_fin <- __stack_fin2
-
- if __stack_fin then
- let result = awaiter.GetResult()
- (continuation result).Invoke(&sm)
- else
- Debug.logInfo "at BindV: calling AwaitUnsafeOnCompleted"
-
- sm.Data.awaiter <- awaiter
- sm.Data.current <- ValueNone
- false)
-
//
// These "modules of priority" allow for an indecisive F# to resolve
// the proper overload if a single type implements more than one
@@ -567,6 +517,66 @@ type TaskSeqBuilder() =
// (like For depending on Using etc).
//
+[]
+module LowPriority =
+ type TaskSeqBuilder with
+
+ //
+ // Note: we cannot place _.Bind directly on the type, as the NoEagerXXX attribute
+ // has no effect, and each use of `do!` will give an overload error (because the
+ // `TaskLike` type and the `Task<_>` type are partially interchangeable, see notes there).
+ //
+ // However, we cannot unify these two methods, because Task<_> inherits from Task (non-generic)
+ // and we need a way to distinguish these two methods.
+ //
+ // Types handled:
+ // - ValueTask (non-generic, because it implements GetResult() -> unit)
+ // - ValueTask<'T> (because it implements GetResult() -> 'TResult)
+ // - Task (non-generic, because it implements GetResult() -> unit)
+ // - any other type that implements GetAwaiter()
+ //
+ // Not handled:
+ // - Task<'T> (because it only implements GetResult() -> unit, not GetResult() -> 'TResult)
+
+ []
+ member inline _.Bind< ^TaskLike, 'TResult1, 'TResult2, ^Awaiter, 'TOverall
+ when ^TaskLike: (member GetAwaiter: unit -> ^Awaiter)
+ and ^Awaiter :> ICriticalNotifyCompletion
+ and ^Awaiter: (member get_IsCompleted: unit -> bool)
+ and ^Awaiter: (member GetResult: unit -> 'TResult1)>
+ (
+ task: ^TaskLike,
+ continuation: ('TResult1 -> TaskSeqCode<'TResult2>)
+ ) : TaskSeqCode<'TResult2> =
+
+ TaskSeqCode<'TResult2>(fun sm ->
+ let mutable awaiter = (^TaskLike: (member GetAwaiter: unit -> ^Awaiter) (task))
+ let mutable __stack_fin = true
+
+ Debug.logInfo "at TaskLike bind"
+
+ if not (^Awaiter: (member get_IsCompleted: unit -> bool) (awaiter)) then
+ // This will yield with __stack_fin2 = false
+ // This will resume with __stack_fin2 = true
+ let __stack_fin2 = ResumableCode.Yield().Invoke(&sm)
+ __stack_fin <- __stack_fin2
+
+ Debug.logInfo ("at TaskLike bind: with __stack_fin = ", __stack_fin)
+ Debug.logInfo ("at TaskLike bind: this.completed = ", sm.Data.completed)
+
+ if __stack_fin then
+ Debug.logInfo "at TaskLike bind!: finished awaiting, calling continuation"
+ let result = (^Awaiter: (member GetResult: unit -> 'TResult1) (awaiter))
+ (continuation result).Invoke(&sm)
+
+ else
+ Debug.logInfo "at TaskLike bind: await further"
+
+ sm.Data.awaiter <- awaiter
+ sm.Data.current <- ValueNone
+ false)
+
+
[]
module MediumPriority =
type TaskSeqBuilder with
@@ -608,3 +618,45 @@ module MediumPriority =
member inline this.YieldFrom(source: IAsyncEnumerable<'T>) : TaskSeqCode<'T> =
this.For(source, (fun v -> this.Yield(v)))
+
+[]
+module HighPriority =
+ type TaskSeqBuilder with
+
+ //
+ // Notes Task:
+ // - Task<_> implements GetAwaiter(), but TaskAwaiter does not implement GetResult() -> TResult
+ // - Instead, it has GetResult() -> unit, which is not '^TaskLike'
+ // - Conclusion: we need an extra high-prio overload to allow support for Task<_>
+ //
+ // Notes ValueTask:
+ // - In contrast, ValueTask<_> *does have* GetResult() -> 'TResult
+ // - Conclusion: we do not need an extra overload anymore for ValueTask
+ //
+ member inline _.Bind(task: Task<'TResult1>, continuation: ('TResult1 -> TaskSeqCode<'T>)) : TaskSeqCode<'T> =
+ TaskSeqCode<'T>(fun sm ->
+ let mutable awaiter = task.GetAwaiter()
+ let mutable __stack_fin = true
+
+ Debug.logInfo "at Bind"
+
+ if not awaiter.IsCompleted then
+ // This will yield with __stack_fin2 = false
+ // This will resume with __stack_fin2 = true
+ let __stack_fin2 = ResumableCode.Yield().Invoke(&sm)
+ __stack_fin <- __stack_fin2
+
+ Debug.logInfo ("at Bind: with __stack_fin = ", __stack_fin)
+ Debug.logInfo ("at Bind: this.completed = ", sm.Data.completed)
+
+ if __stack_fin then
+ Debug.logInfo "at Bind: finished awaiting, calling continuation"
+ let result = awaiter.GetResult()
+ (continuation result).Invoke(&sm)
+
+ else
+ Debug.logInfo "at Bind: await further"
+
+ sm.Data.awaiter <- awaiter
+ sm.Data.current <- ValueNone
+ false)
diff --git a/src/FSharp.Control.TaskSeq/Utils.fs b/src/FSharp.Control.TaskSeq/Utils.fs
index 15fce9b2..3cd2d350 100644
--- a/src/FSharp.Control.TaskSeq/Utils.fs
+++ b/src/FSharp.Control.TaskSeq/Utils.fs
@@ -28,6 +28,16 @@ module ValueTask =
/// Creates a ValueTask with an IValueTaskSource representing the operation
let inline ofIValueTaskSource taskSource version = ValueTask(taskSource, version)
+ /// Creates a ValueTask form a Task<'T>
+ let inline ofTask (task: Task<'T>) = ValueTask<'T>(task)
+
+ /// Ignore a ValueTask<'T>, returns a non-generic ValueTask.
+ let inline ignore (vtask: ValueTask<'T>) =
+ if vtask.IsCompleted then
+ ValueTask()
+ else
+ ValueTask(vtask.AsTask())
+
module Task =
/// Convert an Async<'T> into a Task<'T>
let inline ofAsync (async: Async<'T>) = task { return! async }
@@ -41,22 +51,16 @@ module Task =
/// Convert a Task<'T> into an Async<'T>
let inline toAsync (task: Task<'T>) = Async.AwaitTask task
- /// Convert a Task into a Task
- let inline toTask (task: Task) = task :> Task
-
/// Convert a Task<'T> into a ValueTask<'T>
let inline toValueTask (task: Task<'T>) = ValueTask<'T> task
- /// Convert a Task into a non-generic ValueTask
- let inline toIgnoreValueTask (task: Task) = ValueTask(task :> Task)
-
///
/// Convert a ValueTask<'T> to a Task<'T>. To use a non-generic ValueTask,
/// consider using: .
///
let inline ofValueTask (valueTask: ValueTask<'T>) = task { return! valueTask }
- /// Convert a Task<'T> into a Task, ignoring the result
+ /// Convert a Task<'T> into a non-generic Task, ignoring the result
let inline ignore (task: Task<'T>) =
TaskBuilder.task {
let! _ = task