Skip to content

Commit

Permalink
Lots of AsyncEx tests and docs
Browse files Browse the repository at this point in the history
  • Loading branch information
TheAngryByrd committed Jul 4, 2023
1 parent 1bf5076 commit f842907
Show file tree
Hide file tree
Showing 9 changed files with 528 additions and 15 deletions.
97 changes: 85 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# IcedTasks

## What
## What is IcedTasks?

This library contains additional [computation expressions](https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/computation-expressions) for the [task CE](https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/task-expressions) utilizing the [Resumable Code](https://github.com/fsharp/fslang-design/blob/main/FSharp-6.0/FS-1087-resumable-code.md) introduced [in F# 6.0](https://devblogs.microsoft.com/dotnet/whats-new-in-fsharp-6/#making-f-faster-and-more-interopable-with-task).

Expand All @@ -14,16 +14,21 @@ This library contains additional [computation expressions](https://docs.microsof

- `ParallelAsync<'T>` - Utilizes the [applicative syntax](https://docs.microsoft.com/en-us/dotnet/fsharp/whats-new/fsharp-50#applicative-computation-expressions) to allow parallel execution of [Async<'T> expressions](https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/async-expressions). See [this discussion](https://github.com/dotnet/fsharp/discussions/11043) as to why this is a separate computation expression.

- `AsyncEx<'T>` - Slight variation of F# async semantics described further below with examples.

| Computation Expression<sup>1</sup> | Library<sup>2</sup> | TFM<sup>3</sup> | Hot/Cold<sup>4</sup> | Multiple-Awaits<sup>5</sup> | Multi-start<sup>6</sup> | Tailcalls<sup>7</sup> | CancellationToken propagation<sup>8</sup> | Cancellation checks<sup>9</sup> | Parallel when using and!<sup>10</sup> |
|------------------------------------|---------------------|-----------------|----------------------|-----------------------------|-------------------------|-----------------------|-------------------------------------------|---------------------------------|--------------------------------------|
| F# Async | FSharp.Core | netstandard2.0 | Cold | multiple | multiple | tailcalls | implicit | implicit | No |
| F# ParallelAsync | IcedTasks | netstandard2.0 | Cold | multiple | multiple | tailcalls | implicit | implicit | Yes |
| F# Task/C# Task | FSharp.Core | netstandard2.0 | Hot | multiple | once-start | no tailcalls | explicit | explicit | No |
| F# ValueTask | IcedTasks | netstandard2.1 | Hot | once | once-start | no tailcalls | explicit | explicit | Yes |
| F# ColdTask | IcedTasks | netstandard2.0 | Cold | multiple | multiple | no tailcalls | explicit | explicit | Yes |
| F# CancellableTask | IcedTasks | netstandard2.0 | Cold | multiple | multiple | no tailcalls | implicit | implicit | Yes |
| F# CancellableValueTask | IcedTasks | netstandard2.1 | Cold | one | multiple | no tailcalls | implicit | implicit | Yes |

### Differences at a glance

| Computation Expression<sup>1</sup> | Library<sup>2</sup> | TFM<sup>3</sup> | Hot/Cold<sup>4</sup> | Multiple Awaits <sup>5</sup> | Multi-start<sup>6</sup> | Tailcalls<sup>7</sup> | CancellationToken propagation<sup>8</sup> | Cancellation checks<sup>9</sup> | Parallel when using and!<sup>10</sup> | use IAsyncDisposable <sup>11</sup> |
|------------------------------------|---------------------|-----------------|----------------------|------------------------------|-------------------------|-----------------------|-------------------------------------------|---------------------------------|---------------------------------------|------------------------------------|
| F# Async | FSharp.Core | netstandard2.0 | Cold | Multiple | multiple | tailcalls | implicit | implicit | No | No |
| F# AsyncEx | IcedTasks | netstandard2.0 | Cold | Multiple | multiple | tailcalls | implicit | implicit | No | Yes |
| F# ParallelAsync | IcedTasks | netstandard2.0 | Cold | Multiple | multiple | tailcalls | implicit | implicit | Yes | No |
| F# Task/C# Task | FSharp.Core | netstandard2.0 | Hot | Multiple | once-start | no tailcalls | explicit | explicit | No | Yes |
| F# ValueTask | IcedTasks | netstandard2.1 | Hot | Once | once-start | no tailcalls | explicit | explicit | Yes | Yes |
| F# ColdTask | IcedTasks | netstandard2.0 | Cold | Multiple | multiple | no tailcalls | explicit | explicit | Yes | Yes |
| F# CancellableTask | IcedTasks | netstandard2.0 | Cold | Multiple | multiple | no tailcalls | implicit | implicit | Yes | Yes |
| F# CancellableValueTask | IcedTasks | netstandard2.1 | Cold | Once | multiple | no tailcalls | implicit | implicit | Yes | Yes |

- <sup>1</sup> - [Computation Expression](https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/computation-expressions)
- <sup>2</sup> - Which [Nuget](https://www.nuget.org/) package do they come from
Expand All @@ -35,11 +40,71 @@ This library contains additional [computation expressions](https://docs.microsof
- <sup>8</sup> - `CancellationToken` is propagated to all types the support implicit `CancellatationToken` passing. Calling `cancellableTask { ... }` nested inside `async { ... }` (or any of those combinations) will use the `CancellationToken` from when the code was started.
- <sup>9</sup> - Cancellation will be checked before binds and runs.
- <sup>10</sup> - Allows parallel execution of the asynchronous code using the [Applicative Syntax](https://docs.microsoft.com/en-us/dotnet/fsharp/whats-new/fsharp-50#applicative-computation-expressions) in computation expressions.
- <sup>11</sup> - Allows `use` of `IAsyncDisposable` with the computation expression. See [IAsyncDisposable](https://docs.microsoft.com/en-us/dotnet/api/system.iasyncdisposable) for more info.

## Why should I use this?


### AsyncEx

AsyncEx is similar to Async except in the following ways:

1. Allows `use` for `[IAsyncDisposable](https://docs.microsoft.com/en-us/dotnet/api/system.iasyncdisposable)`

```fsharp
open IcedTasks
let fakeDisposable = { new IAsyncDisposable with member __.DisposeAsync() = ValueTask.CompletedTask }
let myAsyncEx = asyncEx {
use! _ = fakeDisposable
return 42
}
````
2. Allows `let!/do!` against Tasks/ValueTasks/[any Awaitable](https://devblogs.microsoft.com/pfxteam/await-anything/)
```fsharp
open IcedTasks
let myAsyncEx = asyncEx {
let! _ = task { return 42 } // Task<T>
let! _ = valueTask { return 42 } // ValueTask<T>
let! _ = Task.Yield() // YieldAwaitable
return 42
}
```
3. When Tasks throw exceptions they will use the behavior described in [Async.Await overload (esp. AwaitTask without throwing AggregateException](https://github.com/fsharp/fslang-suggestions/issues/840)
```fsharp
let data = "lol"
let inner = asyncEx {
do!
task {
do! Task.Yield()
raise (ArgumentException "foo")
return data
}
:> Task
}
let outer = asyncEx {
try
do! inner
return ()
with
| :? ArgumentException ->
// Should be this exception and not AggregationException
return ()
| ex ->
return raise (Exception("Should not throw this type of exception", ex))
}
```
### For [ValueTasks](https://devblogs.microsoft.com/dotnet/understanding-the-whys-whats-and-whens-of-valuetask/)
## How
- F# doesn't currently have a `valueTask` computation expression. [Until this PR is merged.](https://github.com/dotnet/fsharp/pull/14755)
### ValueTask
```fsharp
open IcedTasks
Expand All @@ -50,6 +115,12 @@ let myValueTask = task {
}
```

### For Cold & CancellableTasks
- You want control over when your tasks are started
- You want to be able to re-run these executable tasks
- You don't want to pollute your methods/functions with extra CancellationToken parameters
- You want the computation to handle checking cancellation before every bind.


### ColdTask

Expand Down Expand Up @@ -125,6 +196,8 @@ let executeWriting = task {

### ParallelAsync

- When you want to execute multiple asyncs in parallel and wait for all of them to complete.

Short example:

```fsharp
Expand Down
165 changes: 165 additions & 0 deletions docsSrc/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,184 @@ This library contains additional [computation expressions](https://docs.microsof

- `ParallelAsync<'T>` - Utilizes the [applicative syntax](https://docs.microsoft.com/en-us/dotnet/fsharp/whats-new/fsharp-50#applicative-computation-expressions) to allow parallel execution of [Async<'T> expressions](https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/async-expressions). See [this discussion](https://github.com/dotnet/fsharp/discussions/11043) as to why this is a separate computation expression.

- `AsyncEx<'T>` - Slight variation of F# async semantics described further below with examples.

## Why should I use IcedTasks?

### AsyncEx

AsyncEx is similar to Async except in the following ways:

1. Allows `use` for `[IAsyncDisposable](https://docs.microsoft.com/en-us/dotnet/api/system.iasyncdisposable)`

```fsharp
open IcedTasks
let fakeDisposable = { new IAsyncDisposable with member __.DisposeAsync() = ValueTask.CompletedTask }
let myAsyncEx = asyncEx {
use! _ = fakeDisposable
return 42
}
````
2. Allows `let!/do!` against Tasks/ValueTasks/[any Awaitable](https://devblogs.microsoft.com/pfxteam/await-anything/)
```fsharp
open IcedTasks
let myAsyncEx = asyncEx {
let! _ = task { return 42 } // Task<T>
let! _ = valueTask { return 42 } // ValueTask<T>
let! _ = Task.Yield() // YieldAwaitable
return 42
}
```
3. When Tasks throw exceptions they will use the behavior described in [Async.Await overload (esp. AwaitTask without throwing AggregateException](https://github.com/fsharp/fslang-suggestions/issues/840)
```fsharp
let data = "lol"
let inner = asyncEx {
do!
task {
do! Task.Yield()
raise (ArgumentException "foo")
return data
}
:> Task
}
let outer = asyncEx {
try
do! inner
return ()
with
| :? ArgumentException ->
// Should be this exception and not AggregationException
return ()
| ex ->
return raise (Exception("Should not throw this type of exception", ex))
}
```
### For [ValueTasks](https://devblogs.microsoft.com/dotnet/understanding-the-whys-whats-and-whens-of-valuetask/)
- F# doesn't currently have a `valueTask` computation expression. [Until this PR is merged.](https://github.com/dotnet/fsharp/pull/14755)
```fsharp
open IcedTasks
let myValueTask = task {
let! theAnswer = valueTask { return 42 }
return theAnswer
}
```

### For Cold & CancellableTasks
- You want control over when your tasks are started
- You want to be able to re-run these executable tasks
- You don't want to pollute your methods/functions with extra CancellationToken parameters
- You want the computation to handle checking cancellation before every bind.


### ColdTask

Short example:

```fsharp
open IcedTasks
let coldTask_dont_start_immediately = task {
let mutable someValue = null
let fooColdTask = coldTask { someValue <- 42 }
do! Async.Sleep(100)
// ColdTasks will not execute until they are called, similar to how Async works
Expect.equal someValue null ""
// Calling fooColdTask will start to execute it
do! fooColdTask ()
Expect.equal someValue 42 ""
}
```

### CancellableTask & CancellableValueTask

The examples show `cancellableTask` but `cancellableValueTask` can be swapped in.

Accessing the context's CancellationToken:

1. Binding against `CancellationToken -> Task<_>`

```fsharp
let writeJunkToFile =
let path = Path.GetTempFileName()
cancellableTask {
let junk = Array.zeroCreate bufferSize
use file = File.Create(path)
for i = 1 to manyIterations do
// You can do! directly against a function with the signature of `CancellationToken -> Task<_>` to access the context's `CancellationToken`. This is slightly more performant.
do! fun ct -> file.WriteAsync(junk, 0, junk.Length, ct)
}
```
2. Binding against `CancellableTask.getCancellationToken`
```fsharp
let writeJunkToFile =
let path = Path.GetTempFileName()
cancellableTask {
let junk = Array.zeroCreate bufferSize
use file = File.Create(path)
// You can bind against `CancellableTask.getCancellationToken` to get the current context's `CancellationToken`.
let! ct = CancellableTask.getCancellationToken ()
for i = 1 to manyIterations do
do! file.WriteAsync(junk, 0, junk.Length, ct)
}
```
Short example:
```fsharp
let executeWriting = task {
// CancellableTask is an alias for `CancellationToken -> Task<_>` so we'll need to pass in a `CancellationToken`.
// For this example we'll use a `CancellationTokenSource` but if you were using something like ASP.NET, passing in `httpContext.RequestAborted` would be appropriate.
use cts = new CancellationTokenSource()
// call writeJunkToFile from our previous example
do! writeJunkToFile cts.Token
}
```

### ParallelAsync

- When you want to execute multiple asyncs in parallel and wait for all of them to complete.

Short example:

```fsharp
open IcedTasks
let exampleHttpCall url = async {
// Pretend we're executing an HttpClient call
return 42
}
let getDataFromAFewSites = parallelAsync {
let! result1 = exampleHttpCall "howManyPlantsDoIOwn"
and! result2 = exampleHttpCall "whatsTheTemperature"
and! result3 = exampleHttpCall "whereIsMyPhone"
// Do something meaningful with results
return ()
}
```

## How do I get started

dotnet add nuget IcedTasks
Expand Down
3 changes: 3 additions & 0 deletions src/IcedTasks/AsyncEx.fs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ type private Async =
static member inline map f x =
async.Bind(x, (fun v -> async.Return(f v)))

/// <summary>
/// This contains many functions that implement Task throwing semantics differently than the current FSharp.Core. See <see href="https://github.com/fsharp/fslang-suggestions/issues/840">Async.Await overload (esp. AwaitTask without throwing AggregateException)</see>
/// </summary>
type AsyncEx =

/// <summary>
Expand Down
42 changes: 41 additions & 1 deletion src/IcedTasks/CancellableTask.fs
Original file line number Diff line number Diff line change
Expand Up @@ -766,6 +766,37 @@ module CancellableTasks =
/// <exclude />
[<AutoOpen>]
module HighPriority =

type AsyncEx with

/// <summary>Return an asynchronous computation that will wait for the given task to complete and return
/// its result.</summary>
///
/// <remarks>
/// This is based on <see href="https://github.com/fsharp/fslang-suggestions/issues/840">Async.Await overload (esp. AwaitTask without throwing AggregateException)</see>
/// </remarks>
static member inline AwaitCancellableTask(t: CancellableTask<'T>) = async {
let! ct = Async.CancellationToken

return!
t ct
|> AsyncEx.AwaitTask
}

/// <summary>Return an asynchronous computation that will wait for the given task to complete and return
/// its result.</summary>
///
/// <remarks>
/// This is based on <see href="https://github.com/fsharp/fslang-suggestions/issues/840">Async.Await overload (esp. AwaitTask without throwing AggregateException)</see>
/// </remarks>
static member inline AwaitCancellableTask(t: CancellableTask) = async {
let! ct = Async.CancellationToken

return!
t ct
|> AsyncEx.AwaitTask
}

type Microsoft.FSharp.Control.Async with

/// <summary>Return an asynchronous computation that will wait for the given task to complete and return
Expand Down Expand Up @@ -846,7 +877,16 @@ module CancellableTasks =
/// A set of extension methods making it possible to bind against <see cref='T:IcedTasks.CancellableTasks.CancellableTask`1'/> in async computations.
/// </summary>
[<AutoOpen>]
module AsyncExtenions =
module AsyncExtensions =

type AsyncExBuilder with

member inline this.Source(t: CancellableTask<'T>) : Async<'T> =
AsyncEx.AwaitCancellableTask t

member inline this.Source(t: CancellableTask) : Async<unit> =
AsyncEx.AwaitCancellableTask t

type Microsoft.FSharp.Control.AsyncBuilder with

member inline this.Bind(t: CancellableTask<'T>, binder: ('T -> Async<'U>)) : Async<'U> =
Expand Down
Loading

0 comments on commit f842907

Please sign in to comment.