https://github.com/rspeele/fsharpasyncmethodbuilder
Write F# Async<T> code in C# with async/await
https://github.com/rspeele/fsharpasyncmethodbuilder
Last synced: 8 months ago
JSON representation
Write F# Async<T> code in C# with async/await
- Host: GitHub
- URL: https://github.com/rspeele/fsharpasyncmethodbuilder
- Owner: rspeele
- License: cc0-1.0
- Created: 2017-07-07T00:17:34.000Z (over 8 years ago)
- Default Branch: master
- Last Pushed: 2017-07-15T03:42:54.000Z (over 8 years ago)
- Last Synced: 2025-02-10T05:42:35.083Z (10 months ago)
- Language: F#
- Size: 13.7 KB
- Stars: 2
- Watchers: 3
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: COPYING
Awesome Lists containing this project
README
# Use F#'s `Async` type in C#
This is a proof-of-concept implementing a method builder compatible with C#
7.0's "arbitrary async returns" feature, for F#'s `Async<_>` type (aka
`FSharpAsync` when used from C#).
That is, it lets you write F# async expressions directly in C# using async/await
syntax, instead of writing `Task`s and using `FSharpAsync.AwaitTask` to
convert them.
# Example
```csharp
using System;
using System.Threading.Tasks;
using Microsoft.FSharp.Control;
using FAsync;
public static class ExampleClass
{
// FAsync is a simple wrapper around FSharpAsync.
// It only exists to point to the compiler to the async method builder to use.
// You can get out the wrapped FSharpAsync easily.
public static FAsync DelayedFactorial(int x)
{
// You can wait Tasks
await Task.Delay(1);
// ... and F# Asyncs
await FSharpAsync.Sleep(1);
// ... and other FAsyncs
if (x > 1)
return x * await DelayedFactorial(x - 1);
else
return x;
}
public static void ExampleUsage()
{
// FAsync has an implicit conversion to FSharpAsync.
FSharpAsync fac = DelayedFactorial(1);
// Like in F#, none of the code in the body of an FAsync method runs until it's started.
var result = FSharpAsync.RunSynchronously(fac, null, null);
Console.WriteLine(result);
}
}
```
See [Tests/Program.cs](Tests/Program.cs) for more examples.
To use the code yourself, you can copy
[FAsync.fs](FSharpAsyncMethodBuilder/FAsync.fs) into an F# project, add a
reference to NuGet package System.Threading.Tasks.Extensions, then reference
that F# project from your C# projects.
# Motivation
This would make sense to use if you have a lot of F# code currently using `async
{ ... }` builders, and are sprinkling in some C# code that interops with it.
It saves some of the overhead of constantly converting between the two task types.
I suspect in the real world more systems are the other way around, and would
benefit more from my other project,
[TaskBuilder.fs](https://github.com/rspeele/TaskBuilder.fs), which lets you
write `Task`s from F# code.
For me, this was just an exercise in learning how to use the new arbitrary async
return feature in C#, which isn't well documented yet. It may serve as a useful
reference for others trying to convert F# computation expressions to C# async
method builders.
Unfortunately, there aren't that many more applications for this technique,
because the async/await feature in C# is not as general-purpose as the
computation expression feature in F#. Any async method builder must support
awaiting _any_ type that implements that awaitable pattern.
This means that you can't reasonably write a builder for a monad that has
nothing to do with asynchronous programming, say, `Option` or `Result`, since users would be allowed to await TPL tasks, `ValueTask`, etc.
within your builder. What's more, your `MyMonad` type would appear awaitable
to every other builder, which probably won't be able to handle it the way you
intend.
It might be handy for some other async-ish things like
[Hopac](https://github.com/Hopac/Hopac), though I haven't looked into that.
# Caveat
There is one subtle but important difference between these async methods and the
F# equivalents. Each `Async<_>` logically represents a "recipe" for a task --
something like a `unit -> Task<_>`. When you have an `Async<_>` in F#, you can
run it as many times as you want with `Async.StartAsTask`, including in parallel
with itself. It'll work fine as long as you don't have them sharing mutable
state created outside the `async` block.
With the C# async/await implementation, calling the method creates a single
instance of a state machine which encapsulates the local variables of the
method. If you call the method once and start the resulting `FSharpAsync`
multiple times concurrently, each running `Task` will use the same state
machine, and they'll trample over each others' local variables with disastrous
results. So don't do that!