Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AsyncEx #24

Merged
merged 4 commits into from
Jul 4, 2023
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
5 changes: 2 additions & 3 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
{
"FSharp.fsacRuntime":"netcore",
"FSharp.enableAnalyzers": true,
"FSharp.enableAnalyzers": false,
"FSharp.analyzersPath": [
"./packages/analyzers"
],
"editor.formatOnSave": true
}
}
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
Loading