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