Skip to content

Commit fce98ec

Browse files
authored
AsyncEx (#24)
* add asyncex scaffold * more fixes * Lots of AsyncEx tests and docs * Lots of AsyncEx tests and docs
1 parent 31f536f commit fce98ec

18 files changed

+1689
-106
lines changed

.vscode/settings.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
{
2-
"FSharp.fsacRuntime":"netcore",
3-
"FSharp.enableAnalyzers": true,
2+
"FSharp.enableAnalyzers": false,
43
"FSharp.analyzersPath": [
54
"./packages/analyzers"
65
],
76
"editor.formatOnSave": true
8-
}
7+
}

README.md

Lines changed: 85 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# IcedTasks
22

3-
## What
3+
## What is IcedTasks?
44

55
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).
66

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

1515
- `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.
1616

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

18-
| 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> |
19-
|------------------------------------|---------------------|-----------------|----------------------|-----------------------------|-------------------------|-----------------------|-------------------------------------------|---------------------------------|--------------------------------------|
20-
| F# Async | FSharp.Core | netstandard2.0 | Cold | multiple | multiple | tailcalls | implicit | implicit | No |
21-
| F# ParallelAsync | IcedTasks | netstandard2.0 | Cold | multiple | multiple | tailcalls | implicit | implicit | Yes |
22-
| F# Task/C# Task | FSharp.Core | netstandard2.0 | Hot | multiple | once-start | no tailcalls | explicit | explicit | No |
23-
| F# ValueTask | IcedTasks | netstandard2.1 | Hot | once | once-start | no tailcalls | explicit | explicit | Yes |
24-
| F# ColdTask | IcedTasks | netstandard2.0 | Cold | multiple | multiple | no tailcalls | explicit | explicit | Yes |
25-
| F# CancellableTask | IcedTasks | netstandard2.0 | Cold | multiple | multiple | no tailcalls | implicit | implicit | Yes |
26-
| F# CancellableValueTask | IcedTasks | netstandard2.1 | Cold | one | multiple | no tailcalls | implicit | implicit | Yes |
19+
20+
### Differences at a glance
21+
22+
| 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> |
23+
|------------------------------------|---------------------|-----------------|----------------------|------------------------------|-------------------------|-----------------------|-------------------------------------------|---------------------------------|---------------------------------------|------------------------------------|
24+
| F# Async | FSharp.Core | netstandard2.0 | Cold | Multiple | multiple | tailcalls | implicit | implicit | No | No |
25+
| F# AsyncEx | IcedTasks | netstandard2.0 | Cold | Multiple | multiple | tailcalls | implicit | implicit | No | Yes |
26+
| F# ParallelAsync | IcedTasks | netstandard2.0 | Cold | Multiple | multiple | tailcalls | implicit | implicit | Yes | No |
27+
| F# Task/C# Task | FSharp.Core | netstandard2.0 | Hot | Multiple | once-start | no tailcalls | explicit | explicit | No | Yes |
28+
| F# ValueTask | IcedTasks | netstandard2.1 | Hot | Once | once-start | no tailcalls | explicit | explicit | Yes | Yes |
29+
| F# ColdTask | IcedTasks | netstandard2.0 | Cold | Multiple | multiple | no tailcalls | explicit | explicit | Yes | Yes |
30+
| F# CancellableTask | IcedTasks | netstandard2.0 | Cold | Multiple | multiple | no tailcalls | implicit | implicit | Yes | Yes |
31+
| F# CancellableValueTask | IcedTasks | netstandard2.1 | Cold | Once | multiple | no tailcalls | implicit | implicit | Yes | Yes |
2732

2833
- <sup>1</sup> - [Computation Expression](https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/computation-expressions)
2934
- <sup>2</sup> - Which [Nuget](https://www.nuget.org/) package do they come from
@@ -35,11 +40,71 @@ This library contains additional [computation expressions](https://docs.microsof
3540
- <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.
3641
- <sup>9</sup> - Cancellation will be checked before binds and runs.
3742
- <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.
43+
- <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.
44+
45+
## Why should I use this?
46+
47+
48+
### AsyncEx
49+
50+
AsyncEx is similar to Async except in the following ways:
51+
52+
1. Allows `use` for [IAsyncDisposable](https://docs.microsoft.com/en-us/dotnet/api/system.iasyncdisposable)
53+
54+
```fsharp
55+
open IcedTasks
56+
let fakeDisposable = { new IAsyncDisposable with member __.DisposeAsync() = ValueTask.CompletedTask }
57+
58+
let myAsyncEx = asyncEx {
59+
use! _ = fakeDisposable
60+
return 42
61+
}
62+
````
63+
2. Allows `let!/do!` against Tasks/ValueTasks/[any Awaitable](https://devblogs.microsoft.com/pfxteam/await-anything/)
3864
65+
```fsharp
66+
open IcedTasks
67+
let myAsyncEx = asyncEx {
68+
let! _ = task { return 42 } // Task<T>
69+
let! _ = valueTask { return 42 } // ValueTask<T>
70+
let! _ = Task.Yield() // YieldAwaitable
71+
return 42
72+
}
73+
```
74+
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)
75+
76+
77+
```fsharp
78+
let data = "lol"
79+
80+
let inner = asyncEx {
81+
do!
82+
task {
83+
do! Task.Yield()
84+
raise (ArgumentException "foo")
85+
return data
86+
}
87+
:> Task
88+
}
89+
90+
let outer = asyncEx {
91+
try
92+
do! inner
93+
return ()
94+
with
95+
| :? ArgumentException ->
96+
// Should be this exception and not AggregationException
97+
return ()
98+
| ex ->
99+
return raise (Exception("Should not throw this type of exception", ex))
100+
}
101+
```
102+
103+
104+
### For [ValueTasks](https://devblogs.microsoft.com/dotnet/understanding-the-whys-whats-and-whens-of-valuetask/)
39105
40-
## How
106+
- F# doesn't currently have a `valueTask` computation expression. [Until this PR is merged.](https://github.com/dotnet/fsharp/pull/14755)
41107
42-
### ValueTask
43108
44109
```fsharp
45110
open IcedTasks
@@ -50,6 +115,12 @@ let myValueTask = task {
50115
}
51116
```
52117

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

54125
### ColdTask
55126

@@ -125,6 +196,8 @@ let executeWriting = task {
125196

126197
### ParallelAsync
127198

199+
- When you want to execute multiple asyncs in parallel and wait for all of them to complete.
200+
128201
Short example:
129202

130203
```fsharp

docsSrc/index.md

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,184 @@ This library contains additional [computation expressions](https://docs.microsof
1313

1414
- `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.
1515

16+
- `AsyncEx<'T>` - Slight variation of F# async semantics described further below with examples.
17+
1618
## Why should I use IcedTasks?
1719

20+
### AsyncEx
21+
22+
AsyncEx is similar to Async except in the following ways:
23+
24+
1. Allows `use` for [IAsyncDisposable](https://docs.microsoft.com/en-us/dotnet/api/system.iasyncdisposable)
25+
26+
```fsharp
27+
open IcedTasks
28+
let fakeDisposable = { new IAsyncDisposable with member __.DisposeAsync() = ValueTask.CompletedTask }
29+
30+
let myAsyncEx = asyncEx {
31+
use! _ = fakeDisposable
32+
return 42
33+
}
34+
````
35+
2. Allows `let!/do!` against Tasks/ValueTasks/[any Awaitable](https://devblogs.microsoft.com/pfxteam/await-anything/)
36+
37+
```fsharp
38+
open IcedTasks
39+
let myAsyncEx = asyncEx {
40+
let! _ = task { return 42 } // Task<T>
41+
let! _ = valueTask { return 42 } // ValueTask<T>
42+
let! _ = Task.Yield() // YieldAwaitable
43+
return 42
44+
}
45+
```
46+
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)
47+
48+
49+
```fsharp
50+
let data = "lol"
51+
52+
let inner = asyncEx {
53+
do!
54+
task {
55+
do! Task.Yield()
56+
raise (ArgumentException "foo")
57+
return data
58+
}
59+
:> Task
60+
}
61+
62+
let outer = asyncEx {
63+
try
64+
do! inner
65+
return ()
66+
with
67+
| :? ArgumentException ->
68+
// Should be this exception and not AggregationException
69+
return ()
70+
| ex ->
71+
return raise (Exception("Should not throw this type of exception", ex))
72+
}
73+
```
74+
75+
1876
### For [ValueTasks](https://devblogs.microsoft.com/dotnet/understanding-the-whys-whats-and-whens-of-valuetask/)
1977
2078
- F# doesn't currently have a `valueTask` computation expression. [Until this PR is merged.](https://github.com/dotnet/fsharp/pull/14755)
2179
80+
81+
```fsharp
82+
open IcedTasks
83+
84+
let myValueTask = task {
85+
let! theAnswer = valueTask { return 42 }
86+
return theAnswer
87+
}
88+
```
89+
2290
### For Cold & CancellableTasks
2391
- You want control over when your tasks are started
2492
- You want to be able to re-run these executable tasks
2593
- You don't want to pollute your methods/functions with extra CancellationToken parameters
2694
- You want the computation to handle checking cancellation before every bind.
2795

2896

97+
### ColdTask
98+
99+
Short example:
100+
101+
```fsharp
102+
open IcedTasks
103+
104+
let coldTask_dont_start_immediately = task {
105+
let mutable someValue = null
106+
let fooColdTask = coldTask { someValue <- 42 }
107+
do! Async.Sleep(100)
108+
// ColdTasks will not execute until they are called, similar to how Async works
109+
Expect.equal someValue null ""
110+
// Calling fooColdTask will start to execute it
111+
do! fooColdTask ()
112+
Expect.equal someValue 42 ""
113+
}
114+
115+
```
116+
117+
### CancellableTask & CancellableValueTask
118+
119+
The examples show `cancellableTask` but `cancellableValueTask` can be swapped in.
120+
121+
Accessing the context's CancellationToken:
122+
123+
1. Binding against `CancellationToken -> Task<_>`
124+
125+
```fsharp
126+
let writeJunkToFile =
127+
let path = Path.GetTempFileName()
128+
129+
cancellableTask {
130+
let junk = Array.zeroCreate bufferSize
131+
use file = File.Create(path)
132+
133+
for i = 1 to manyIterations do
134+
// You can do! directly against a function with the signature of `CancellationToken -> Task<_>` to access the context's `CancellationToken`. This is slightly more performant.
135+
do! fun ct -> file.WriteAsync(junk, 0, junk.Length, ct)
136+
}
137+
```
138+
139+
2. Binding against `CancellableTask.getCancellationToken`
140+
141+
```fsharp
142+
let writeJunkToFile =
143+
let path = Path.GetTempFileName()
144+
145+
cancellableTask {
146+
let junk = Array.zeroCreate bufferSize
147+
use file = File.Create(path)
148+
// You can bind against `CancellableTask.getCancellationToken` to get the current context's `CancellationToken`.
149+
let! ct = CancellableTask.getCancellationToken ()
150+
for i = 1 to manyIterations do
151+
do! file.WriteAsync(junk, 0, junk.Length, ct)
152+
}
153+
```
154+
155+
Short example:
156+
157+
```fsharp
158+
let executeWriting = task {
159+
// CancellableTask is an alias for `CancellationToken -> Task<_>` so we'll need to pass in a `CancellationToken`.
160+
// For this example we'll use a `CancellationTokenSource` but if you were using something like ASP.NET, passing in `httpContext.RequestAborted` would be appropriate.
161+
use cts = new CancellationTokenSource()
162+
// call writeJunkToFile from our previous example
163+
do! writeJunkToFile cts.Token
164+
}
165+
166+
167+
```
168+
169+
### ParallelAsync
170+
171+
- When you want to execute multiple asyncs in parallel and wait for all of them to complete.
172+
173+
Short example:
174+
175+
```fsharp
176+
open IcedTasks
177+
178+
let exampleHttpCall url = async {
179+
// Pretend we're executing an HttpClient call
180+
return 42
181+
}
182+
183+
let getDataFromAFewSites = parallelAsync {
184+
let! result1 = exampleHttpCall "howManyPlantsDoIOwn"
185+
and! result2 = exampleHttpCall "whatsTheTemperature"
186+
and! result3 = exampleHttpCall "whereIsMyPhone"
187+
188+
// Do something meaningful with results
189+
return ()
190+
}
191+
192+
```
193+
29194
## How do I get started
30195

31196
dotnet add nuget IcedTasks

0 commit comments

Comments
 (0)