diff --git a/docs/fsharp-cheatsheet.md b/docs/fsharp-cheatsheet.md
index 0675735..f42c0de 100644
--- a/docs/fsharp-cheatsheet.md
+++ b/docs/fsharp-cheatsheet.md
@@ -437,7 +437,7 @@ See [Records (MS Learn)](https://learn.microsoft.com/en-us/dotnet/fsharp/languag
// Create
let anonRecord1 = {| Name = "Don Syme"; Language = "F#"; Age = 999 |}
-
+
// Copy and Update
let anonRecord2 = {| anonRecord1 with Name = "Mads Torgersen"; Language = "C#" |}
@@ -553,7 +553,7 @@ The `let..match..with` statement can be simplified using just the `function` sta
match num with
| 1 | 2 | 3 -> printfn "Found 1, 2, or 3!"
| a -> printfn "%d" a
-
+
let filterNumbers' = // the paramater and `match num with` are combined
function | 1 | 2 | 3 -> printfn "Found 1, 2, or 3!"
| a -> printfn "%d" a
@@ -746,113 +746,167 @@ A *Partial active pattern* must return an `Option<'T>`.
| DivisibleBy 5 -> "Buzz"
| i -> string i
-*Partial active patterns* share the syntax of parameterized patterns but their active recognizers accept only one argument.
-
## Asynchronous Programming
-F# asynchronous programming is centered around two core concepts: async computations and tasks.
+F# asynchronous programming support consists of two complementary mechanisms::
+- .NET's Tasks (via `task { }` expressions). This provides semantics very close to that of C#'s `async`/`await` mechanism, requiring explicit direct management of `CancellationToken`s.
+- F# native `Async` computations (via `async { }` expressions). Predates `Task`. Provides intrinsic `CancellationToken` propagation.
- async {
- do! Async.Sleep (waitInSeconds * 1000)
- let! asyncResult = asyncComputation
- use! disposableResult = iDisposableAsyncComputation
+### .NET Tasks
+
+In F#, .NET Tasks can be constructed using the `task { }` computational expression.
+.NET Tasks are "hot" - they immediately start running. At the first `let!` or `do!`, the `Task<'T>` is returned and execution continues on the ThreadPool.
+
+ open System
+ open System.Threading
+ open System.Threading.Tasks
+ open System.IO
+
+ let readFile filename ct = task {
+ printfn "Started Reading Task"
+ do! Task.Delay((TimeSpan.FromSeconds 5), cancellationToken = ct) // use do! when awaiting a Task
+ let! text = File.ReadAllTextAsync(filename, ct) // use let! when awaiting a Task<'T>, and unwrap 'T from Task<'T>.
+ return text
}
-### Async vs Tasks
-
-An async computation is a unit of work, and a task is a promise of a result. A subtle but important distinction.
-Async computations are composable and are not started until explicitly requested; Tasks (when created using the `task` expression) are _hot_:
-
- let runAsync waitInSeconds =
- async {
- printfn "Created Async"
- do! Async.Sleep (waitInSeconds * 1000)
- printfn $"Completed Async"
- }
-
- let runTask waitInSeconds =
- task {
- printfn "Started Task"
- do! System.Threading.Tasks.Task.Delay (waitInSeconds * 1000)
- printfn $"Completed Task"
- }
-
- let asyncComputation = runAsync 5 // returns Async and does not print anything
- let newTask = runTask 3 // returns System.Threading.Tasks.Task and outputs: "Started Task" <3 second delay> "Completed Task"
-
- asyncComputation |> Async.RunSynchronously // this now runs the async computation
- newTask.Wait() // this will have already completed by this point
+ let readFileTask: Task = readFile "myfile.txt" CancellationToken.None // (before return) Output: Started Reading Task
- // Output:
- // Started Task
- // Created Async
- // Completed Task
- // Completed Async
+ // (readFileTask continues execution on the ThreadPool)
-### Async and Task Interop
+ let fileContent = readFileTask.Result // Blocks thread and waits for content. (1)
+ let fileContent' = readFileTask.Result // Task is already completed, returns same value immediately; no output
-As F# sits in .NET, and a lot of the codebase uses the C# async/await, the majority of actions are going to be executed and tracked using `System.Threading.Tasks.Task<'T>`s.
+(1) `.Result` used for demonstration only. Read about [async/await Best Practices](https://learn.microsoft.com/en-us/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming#async-all-the-way)
-#### Async.AwaitTask
+### Async Computations
-This converts a Task into an async computation. It has the [signature](#functions-signatures): `Task<'T>` -> `Async<'T>`
+Async computations were invented before .NET Tasks existed, which is why F# has two core methods for asynchronous programming. However, async computations did not become obsolete. They offer another, but different, approach: dataflow.
+Async computations are constructed using `async { }` expressions, and the [`Async` module](https://fsharp.github.io/fsharp-core-docs/reference/fsharp-control-fsharpasync.html#section3) is used to compose and execute them.
+In contrast to .NET Tasks, async expressions are "cold" (need to be explicitly started) and every execution [propagates a CancellationToken implicitly](#asynchronous-programming-cancellation-async).
- async {
- let! bytes = File.ReadAllBytesAsync(path) |> Async.AwaitTask
- let fileName = Path.GetFileName(path)
- printfn $"File {fileName} has %d{bytes.Length} bytes"
+ open System
+ open System.Threading
+ open System.IO
+
+ let readFile filename = async {
+ do! Async.Sleep(TimeSpan.FromSeconds 5) // use do! when awaiting an Async
+ let! text = File.ReadAllTextAsync(filename) |> Async.AwaitTask // (1)
+ printfn "Finished Reading File"
+ return text
}
-#### Async.StartAsTask
-
-This converts an async computation into a Task. It has the [signature](#functions-signatures): `Async<'T>` -> `Task<'T>`
-
- async {
- do! Async.Sleep 5000
- } |> Async.StartAsTask
-
-### Async and the ThreadPool
-
-Below is a demonstration of the different ways to start an async computation and where it ends up in the dotnet runtime.
-In the comments; `M`, `X`, `Y`, and `Z` are used to represent differences in values.
-
- #r "nuget:Nito.AsyncEx, 5.1.2"
-
- let asyncTask from =
- async {
- printfn $"{from} Thread Id = {System.Threading.Thread.CurrentThread.ManagedThreadId}"
- }
-
- let run () =
- printfn $"run() Thread Id = {System.Threading.Thread.CurrentThread.ManagedThreadId}"
- asyncTask "StartImmediate" |> Async.StartImmediate // run and wait on same thread
- asyncTask "Start" |> Async.Start // queue in ThreadPool and do not wait
- asyncTask "RunSynchronously" |> Async.RunSynchronously // run and wait; depends
- printfn ""
-
- run ()
- // run() Thread Id = M - Main, non-ThreadPool thread
- // StartImmediate Thread Id = M - started, waited, and completed on run() thread
- // Start Thread Id = X - queued and completed in ThreadPool
- // RunSynchronously Thread Id = Y - started, waited, and completed in ThreadPool
- // Important: As `Start` is queued in the ThreadPool, it might finish before `RunSynchronously` and they will share the same number
-
- // Run in ThreadPool
- async { run () } |> Async.RunSynchronously
- // run() Thread Id = X - ThreadPool thread
- // StartImmediate Thread Id = X - started, waited, and completed on run() thread
- // Start Thread Id = Y - queued and completed in new ThreadPool thread
- // RunSynchronously Thread Id = X - started, waited, and completed on run() thread
- // Important: `RunSynchronously` behaves like `StartImmediate` when on a ThreadPool thread without a SynchronizationContext
-
- // Run in ThreadPool with a SynchronizationContext
- async { Nito.AsyncEx.AsyncContext.Run run } |> Async.RunSynchronously
- // run() Thread Id = X - ThreadPool thread
- // StartImmediate Thread Id = X - started, waited, and completed on run() thread
- // Start Thread Id = Y - queued and completed in new ThreadPool thread
- // RunSynchronously Thread Id = Z - started, waited, and completed in new ThreadPool thread
+ // compose a new async computation from exising async computations
+ let readFiles = [ readFile "A"; readFile "B" ] |> Async.Parallel
+
+ // execute async computation
+ let textOfFiles: string[] = readFiles |> Async.RunSynchronously
+ // Out: Finished Reading File
+ // Out: Finished Reading File
+
+ // re-execute async computation again
+ let textOfFiles': string[] = readFiles |> Async.RunSynchronously
+ // Out: Finished Reading File
+ // Out: Finished Reading File
+
+(1) As .NET Tasks became the central component of task-based asynchronous programming after F# Async were introduced, F#'s Async has `Async.AwaitTask` to map from `Task<'T>` to `Async<'T>`. Note that cancellation and exception handling require [special considerations](https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/141).
+
+#### Creation / Composition
+
+The `Async` module has a number of functions to compose and start computations. The full list with explanations can be found in the [Async Type Reference](https://fsharp.github.io/fsharp-core-docs/reference/fsharp-control-fsharpasync.html#section0).
+
+| Function | Description |
+|-------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| Async.Ignore | Creates an `Async` computation from an `Async<'T>` |
+| Async.Parallel | Composes a new computation from multiple computations, `Async<'T> seq`, and runs them in parallel; it returns all the results in an array `Async<'T[]>` |
+| Async.Sequential | Composes a new computation from multiple computations, `Async<'T> seq`, and runs them in series; it returns all the results in an array `Async<'T[]>` |
+| Async.Choice | Composes a new computation from multiple computations, `Async<'T option> seq`, and returns the first where `'T'` is `Some value` (all others running are canceled). If all computations return `None` then the result is `None` |
+
+For all functions that compose a new computation from children, if any child computations raise an exception, then the overall computation will trigger an exception. The `CancellationToken` passed to the child computations will be triggered, and execution continues when all running children have cancelled execution.
+
+#### Executing
+
+| Function | Description |
+|------------------------------|------------------------------------------------------------------------------------------------------------------------------|
+| Async.RunSynchronously | Runs an async computation and awaits its result. |
+| Async.StartAsTask | Runs an async computation on the ThreadPool and wraps the result in a `Task<'T>`. |
+| Async.StartImmediateAsTask | Runs an async computation, starting immediately on the current operating system thread, and wraps the result in a `Task<'T>` |
+| Async.Start | Runs an `Async` computation on the ThreadPool (without observing any exceptions). |
+| Async.StartImmediate | Runs a computation, starting immediately on the current thread and continuations completing in the ThreadPool. |
+
+### Cancellation
+
+#### .NET Tasks
+
+.NET Tasks do not have any intrinsic handling of `CancellationToken`s; you are responsible for passing `CancellationToken`s down the call hierarchy to all sub-Tasks.
+
+ open System
+ open System.Threading
+ open System.Threading.Tasks
+
+ let loop (token: CancellationToken) = task {
+ for cnt in [ 0 .. 9 ] do
+ printf $"{cnt}: And..."
+ do! Task.Delay((TimeSpan.FromSeconds 2), token) // token is required for Task.Delay to be interruptible
+ printfn "Done"
+ }
+
+ let cts = new CancellationTokenSource (TimeSpan.FromSeconds 5)
+ let runningLoop = loop cts.Token
+ try
+ runningLoop.GetAwaiter().GetResult() // (1)
+ with :? OperationCanceledException -> printfn "Canceled"
+
+Output:
+
+ 0: And...Done
+ 1: And...Done
+ 2: And...Canceled
+
+(1) `.GetAwaiter().GetResult()` used for demonstration only. Read about [async/await Best Practices](https://learn.microsoft.com/en-us/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming#async-all-the-way)
+
+
+
+#### Async
+
+Asynchronous computations have the benefit of implicit `CancellationToken` passing and checking.
+
+ open System
+ open System.Threading
+ open System.Threading.Tasks
+
+ let loop = async {
+ for cnt in [ 0 .. 9 ] do
+ printf $"{cnt}: And..."
+ do! Async.Sleep(TimeSpan.FromSeconds 1) // Async.Sleep implicitly receives and checks `cts.Token`
+
+ let! ct = Async.CancellationToken // when interoperating with Tasks, cancellationTokens need to be passed explicitly
+ do! Task.Delay((TimeSpan.FromSeconds 1), cancellationToken = ct) |> Async.AwaitTask
+
+ printfn "Done"
+ }
+
+ let cts = new CancellationTokenSource(TimeSpan.FromSeconds 5)
+ try
+ Async.RunSynchronously (loop, Timeout.Infinite, cts.Token)
+ with :? OperationCanceledException -> printfn "Canceled"
+
+Output:
+
+ 0: And...Done
+ 1: And...Done
+ 2: And...Canceled
+
+All methods for cancellation can be found in the [Core Library Documentation](https://fsharp.github.io/fsharp-core-docs/reference/fsharp-control-fsharpasync.html#section3)
+
+### More to Explore
+
+Asynchronous programming is a vast topic. Here are some other resources worth exploring:
+
+- [Asynchronous Programming in F#](https://learn.microsoft.com/en-us/dotnet/fsharp/tutorials/async) - Microsoft's tutorial guide. Recommended as it is up-to-date and expands on some of the topics here.
+- [Iced Tasks](https://github.com/TheAngryByrd/IcedTasks?tab=readme-ov-file#icedtasks) - .NET Tasks start immediately. The IcedTasks library provide additional [computational expressions](https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/computation-expressions) such as `cancellableTask`, which combines the benefits of .NET Tasks (natural interoperation with Task APIs and the performance benefits of the `task`'s State-Machine based implementation) with asynchronous expressions (composability, implicit `CancellationToken` passing, and the fact that you can invoke (or retry) a given computation multiple times).
+- [Asynchronous Programming Best Practices](https://github.com/davidfowl/AspNetCoreDiagnosticScenarios/blob/master/AsyncGuidance.md#table-of-contents) by David Fowler - offers a fantastic list of good practices for .NET Task usage.