Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@
<Compile Include="TaskSeq.Realworld.fs" />
<Compile Include="TaskSeq.AsyncExtensions.Tests.fs" />
<Compile Include="TaskSeq.TaskExtensions.Tests.fs" />
<Compile Include="TaskSeq.Do.Tests.fs" />
<Compile Include="TaskSeq.Let.Tests.fs" />
<Compile Include="TaskSeq.Using.Tests.fs" />
<Compile Include="Program.fs" />
</ItemGroup>
Expand Down
58 changes: 58 additions & 0 deletions src/FSharp.Control.TaskSeq.Test/TaskSeq.Do.Tests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
module TaskSeq.Tests.Do

open System
open System.Threading.Tasks
open FsUnit
open Xunit

open FSharp.Control

[<Fact>]
let ``CE taskSeq: use 'do'`` () =
let mutable value = 0

taskSeq { do value <- value + 1 } |> verifyEmpty

[<Fact>]
let ``CE taskSeq: use 'do!' with a task<unit>`` () =
let mutable value = 0

taskSeq { do! task { do value <- value + 1 } }
|> verifyEmpty
|> Task.map (fun _ -> value |> should equal 1)

[<Fact>]
let ``CE taskSeq: use 'do!' with a valuetask<unit>`` () =
let mutable value = 0

taskSeq { do! ValueTask.ofTask (task { do value <- value + 1 }) }
|> verifyEmpty
|> Task.map (fun _ -> value |> should equal 1)

[<Fact>]
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)

[<Fact>]
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)

[<Fact>]
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)
82 changes: 82 additions & 0 deletions src/FSharp.Control.TaskSeq.Test/TaskSeq.Let.Tests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
module TaskSeq.Tests.Let

open System
open System.Threading.Tasks
open FsUnit
open Xunit

open FSharp.Control

[<Fact>]
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)

[<Fact>]
let ``CE taskSeq: use 'let!' with a task<unit>`` () =
let mutable value = 0

taskSeq {
let! unit' = task { do value <- value + 1 }
do unit'
}
|> verifyEmpty
|> Task.map (fun _ -> value |> should equal 1)

[<Fact>]
let ``CE taskSeq: use 'let!' with a task<string>`` () =
taskSeq {
let! test = task { return "test" }
yield test
}
|> TaskSeq.exactlyOne
|> Task.map (should equal "test")

[<Fact>]
let ``CE taskSeq: use 'let!' with a valuetask<unit>`` () =
let mutable value = 0

taskSeq {
let! unit' = ValueTask.ofTask (task { do value <- value + 1 })
do unit'
}
|> verifyEmpty
|> Task.map (fun _ -> value |> should equal 1)

[<Fact>]
let ``CE taskSeq: use 'let!' with a valuetask<string>`` () =
taskSeq {
let! test = ValueTask.ofTask (task { return "test" })
yield test
}
|> TaskSeq.exactlyOne
|> Task.map (should equal "test")

[<Fact>]
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)

[<Fact>]
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)
14 changes: 7 additions & 7 deletions src/FSharp.Control.TaskSeq.Test/TaskSeq.Using.Tests.fs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module FSharp.Control.TaskSeq.Test
module TaskSeq.Test.Using

open System
open System.Threading.Tasks
Expand Down Expand Up @@ -34,7 +34,7 @@ type private MultiDispose(disposed: int ref) =
let private check = TaskSeq.length >> Task.map (should equal 1)

[<Fact>]
let ``CE task: Using when type implements IDisposable`` () =
let ``CE taskSeq: Using when type implements IDisposable`` () =
let disposed = ref false

let ts = taskSeq {
Expand All @@ -46,7 +46,7 @@ let ``CE task: Using when type implements IDisposable`` () =
|> Task.map (fun _ -> disposed.Value |> should be True)

[<Fact>]
let ``CE task: Using when type implements IAsyncDisposable`` () =
let ``CE taskSeq: Using when type implements IAsyncDisposable`` () =
let disposed = ref false

let ts = taskSeq {
Expand All @@ -58,7 +58,7 @@ let ``CE task: Using when type implements IAsyncDisposable`` () =
|> Task.map (fun _ -> disposed.Value |> should be True)

[<Fact>]
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 {
Expand All @@ -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

[<Fact>]
let ``CE task: Using! when type implements IDisposable`` () =
let ``CE taskSeq: Using! when type implements IDisposable`` () =
let disposed = ref false

let ts = taskSeq {
Expand All @@ -82,7 +82,7 @@ let ``CE task: Using! when type implements IDisposable`` () =
|> Task.map (fun _ -> disposed.Value |> should be True)

[<Fact>]
let ``CE task: Using! when type implements IAsyncDisposable`` () =
let ``CE taskSeq: Using! when type implements IAsyncDisposable`` () =
let disposed = ref false

let ts = taskSeq {
Expand All @@ -94,7 +94,7 @@ let ``CE task: Using! when type implements IAsyncDisposable`` () =
|> Task.map (fun _ -> disposed.Value |> should be True)

[<Fact>]
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 {
Expand Down
2 changes: 2 additions & 0 deletions src/FSharp.Control.TaskSeq.Test/TestUtils.fs
Original file line number Diff line number Diff line change
Expand Up @@ -136,11 +136,13 @@ type DummyTaskFactory(µsecMin: int64<µs>, µsecMax: int64<µs>) =

[<AutoOpen>]
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
Expand Down
152 changes: 102 additions & 50 deletions src/FSharp.Control.TaskSeq/TaskSeqBuilder.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -567,6 +517,66 @@ type TaskSeqBuilder() =
// (like For depending on Using etc).
//

[<AutoOpen>]
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)

[<NoEagerConstraintApplication>]
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)


[<AutoOpen>]
module MediumPriority =
type TaskSeqBuilder with
Expand Down Expand Up @@ -608,3 +618,45 @@ module MediumPriority =

member inline this.YieldFrom(source: IAsyncEnumerable<'T>) : TaskSeqCode<'T> =
this.For(source, (fun v -> this.Yield(v)))

[<AutoOpen>]
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)
Loading