From 9985de0256a2955f904e0ed662d0b3965036a0b3 Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Sun, 27 Nov 2022 04:26:02 +0100 Subject: [PATCH 1/5] Add tests for `do` and `do!` and cleanup some helper functions --- .../FSharp.Control.TaskSeq.Test.fsproj | 1 + .../TaskSeq.Do.Tests.fs | 48 +++++++++++++++++++ .../TaskSeq.Using.Tests.fs | 14 +++--- src/FSharp.Control.TaskSeq.Test/TestUtils.fs | 2 + src/FSharp.Control.TaskSeq/Utils.fs | 18 ++++--- 5 files changed, 69 insertions(+), 14 deletions(-) create mode 100644 src/FSharp.Control.TaskSeq.Test/TaskSeq.Do.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 2fafcf30..c1fad98c 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,7 @@ + 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..3806fdeb --- /dev/null +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Do.Tests.fs @@ -0,0 +1,48 @@ +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 + +//[] +//let ``CE taskSeq: use 'do!' with a valuetask`` () = +// let mutable value = 0 + +// taskSeq { do! ValueTask.ofIValueTaskSource (task { do value <- value + 1 }) } + +// |> verifyEmpty + +//[] +//let ``CE taskSeq: use 'do!' with a non-generic valuetask`` () = +// let mutable value = 0 + +// taskSeq { do! ValueTask(task { do value <- value + 1 }) } + +// |> verifyEmpty + +//[] +//let ``CE taskSeq: use 'do!' with a non-generic task`` () = +// let mutable value = 0 + +// taskSeq { do! (task { do value <- value + 1 }) |> Task.ignore } + +// |> verifyEmpty 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/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 From 08bb7a7e5e3b831c36b1c26fa554bd2e886dd7ed Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Sun, 27 Nov 2022 05:20:53 +0100 Subject: [PATCH 2/5] Implement `^TaskLike` version of `Bind` to allow more flexible types with `do!`, like `ValueTask` and non-generic `Task` --- .../TaskSeq.Do.Tests.fs | 42 +++-- src/FSharp.Control.TaskSeq/TaskSeqBuilder.fs | 161 ++++++++++++------ 2 files changed, 131 insertions(+), 72 deletions(-) diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Do.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Do.Tests.fs index 3806fdeb..70400689 100644 --- a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Do.Tests.fs +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Do.Tests.fs @@ -11,38 +11,36 @@ open FSharp.Control let ``CE taskSeq: use 'do'`` () = let mutable value = 0 - taskSeq { do value <- value + 1 } - - |> verifyEmpty + 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.ofIValueTaskSource (task { do value <- value + 1 }) } - -// |> verifyEmpty - -//[] -//let ``CE taskSeq: use 'do!' with a non-generic valuetask`` () = -// let mutable value = 0 +[] +let ``CE taskSeq: use 'do!' with a valuetask`` () = + let mutable value = 0 -// taskSeq { do! ValueTask(task { do value <- value + 1 }) } + taskSeq { do! ValueTask.ofTask (task { do value <- value + 1 }) } + |> verifyEmpty + |> Task.map (fun _ -> value |> should equal 1) -// |> verifyEmpty +[] +let ``CE taskSeq: use 'do!' with a non-generic valuetask`` () = + let mutable value = 0 -//[] -//let ``CE taskSeq: use 'do!' with a non-generic task`` () = -// let mutable value = 0 + taskSeq { do! ValueTask(task { do value <- value + 1 }) } + |> verifyEmpty + |> Task.map (fun _ -> value |> should equal 1) -// taskSeq { do! (task { do value <- value + 1 }) |> Task.ignore } +[] +let ``CE taskSeq: use 'do!' with a non-generic task`` () = + let mutable value = 0 -// |> verifyEmpty + taskSeq { do! (task { do value <- value + 1 }) |> Task.ignore } + |> verifyEmpty + |> Task.map (fun _ -> value |> should equal 1) diff --git a/src/FSharp.Control.TaskSeq/TaskSeqBuilder.fs b/src/FSharp.Control.TaskSeq/TaskSeqBuilder.fs index a372b14f..688d7d9d 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,58 @@ 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 interchangeable). + // + // However, we cannot unify these two methods, because Task<_> inherits from Task (non-generic) + // and we need a way to distinguish these two methods. + // + + [] + 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 +610,62 @@ module MediumPriority = member inline this.YieldFrom(source: IAsyncEnumerable<'T>) : TaskSeqCode<'T> = this.For(source, (fun v -> this.Yield(v))) + +[] +module HighPriority = + type TaskSeqBuilder with + + 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) + + 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) From 5647472a8fb00bfd00f610af1e13eb8020c41740 Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Sun, 27 Nov 2022 17:21:31 +0100 Subject: [PATCH 3/5] Remove `ValueTask`, now handled by `^TaskLike`, see comments in code --- .../FSharp.Control.TaskSeq.Test.fsproj | 1 + src/FSharp.Control.TaskSeq/TaskSeqBuilder.fs | 57 ++++++++----------- 2 files changed, 25 insertions(+), 33 deletions(-) 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 c1fad98c..611663b8 100644 --- a/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj +++ b/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj @@ -49,6 +49,7 @@ + diff --git a/src/FSharp.Control.TaskSeq/TaskSeqBuilder.fs b/src/FSharp.Control.TaskSeq/TaskSeqBuilder.fs index 688d7d9d..2fc1a426 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeqBuilder.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeqBuilder.fs @@ -524,11 +524,19 @@ module LowPriority = // // 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 interchangeable). + // `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 @@ -545,7 +553,7 @@ module LowPriority = let mutable awaiter = (^TaskLike: (member GetAwaiter: unit -> ^Awaiter) (task)) let mutable __stack_fin = true - Debug.logInfo "at TaskLike bind!" + Debug.logInfo "at TaskLike bind" if not (^Awaiter: (member get_IsCompleted: unit -> bool) (awaiter)) then // This will yield with __stack_fin2 = false @@ -553,8 +561,8 @@ module LowPriority = 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) + 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" @@ -562,7 +570,7 @@ module LowPriority = (continuation result).Invoke(&sm) else - Debug.logInfo "at TaskLike bind!: await further" + Debug.logInfo "at TaskLike bind: await further" sm.Data.awaiter <- awaiter sm.Data.current <- ValueNone @@ -615,9 +623,19 @@ module MediumPriority = 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 awaiter: TaskAwaiter<'TResult1> = task.GetAwaiter() let mutable __stack_fin = true Debug.logInfo "at Bind" @@ -642,30 +660,3 @@ module HighPriority = 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) From 88f018eae9e6904888f4381e7e98525cd434079c Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Sun, 27 Nov 2022 17:22:07 +0100 Subject: [PATCH 4/5] Add let-bang tests for corner cases --- .../TaskSeq.Let.Tests.fs | 82 +++++++++++++++++++ src/FSharp.Control.TaskSeq/TaskSeqBuilder.fs | 2 +- 2 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 src/FSharp.Control.TaskSeq.Test/TaskSeq.Let.Tests.fs 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/TaskSeqBuilder.fs b/src/FSharp.Control.TaskSeq/TaskSeqBuilder.fs index 2fc1a426..ea154804 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeqBuilder.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeqBuilder.fs @@ -635,7 +635,7 @@ module HighPriority = // member inline _.Bind(task: Task<'TResult1>, continuation: ('TResult1 -> TaskSeqCode<'T>)) : TaskSeqCode<'T> = TaskSeqCode<'T>(fun sm -> - let mutable awaiter: TaskAwaiter<'TResult1> = task.GetAwaiter() + let mutable awaiter = task.GetAwaiter() let mutable __stack_fin = true Debug.logInfo "at Bind" From 7e427a46e82fa5e2c8c95d7aa3e85cd771ad7c04 Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Sun, 27 Nov 2022 17:29:41 +0100 Subject: [PATCH 5/5] Add a do-bang test with `Task.Delay` from the issue description --- src/FSharp.Control.TaskSeq.Test/TaskSeq.Do.Tests.fs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Do.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Do.Tests.fs index 70400689..d979f6fa 100644 --- a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Do.Tests.fs +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Do.Tests.fs @@ -44,3 +44,15 @@ let ``CE taskSeq: use 'do!' with a non-generic task`` () = 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)