Search code examples
asynchronousf#c#-to-f#computation-expression

Return started Async<T> in F# function from calling a non-async Func<T> from C#?


Let's say I want to call, from F#, this C# function:

public static class Foo {
    public Bar Baz()
    {
       ...
    }
}

The problem is this function is CPU intensive and I don't want to block on it. Unfortunately the C# library doesn't have an Task<Bar> BazAsync() overload.

Then I want to provide the asynchronous version myself, by creating an F# function that calls it and returns an (already started) Async<Bar>. That is, I don't want to use System.Threading.Task<Bar>.

I guess what I'm looking for is the equivalent of Task<T>.Run() in the F# Async way of doing things.

I've already looked at the following options:

  • Async.StartAsTask -> deals with C#ish Task type.
  • Async.Start and Async.StartImmediately -> receive Async<unit> not Async<T>

Is Async.StartChild what I'm looking for? If yes, it would be:

let BazWrapper() =
    let asyncTask: Async<Bar> = async {
        return Foo.Bar()
    }
    Async.StartChild asyncTask

However, if this above is the solution:

  1. Why most documentation around F# async workflows doesn't mention StartChild?
  2. Why BazWrapper returns Async<Async<Bar>> instead of Async<Bar>?

Solution

  • BazWrapper returns Async<Async<Bar>> because that's what StartChild does: it takes an Async<'T> and returns Async<Async<'T>>. The intent is to use it within an async computation expression, so that you can launch multiple "child" asyncs. Example from the Async.Start vs Async.StartChild sample code:

    async {
        //(...async stuff...)
        for msg in msgs do 
            let! child = asyncSendMsg msg |> Async.StartChild
            ()
        //(...more async stuff...)
    }
    

    When you're inside an async computation expression, the let! keyword will "unwrap" an Async<'Whatever> leaving you with a value of type 'Whatever. In the case of calling Async.StartChild, the 'Whatever type is specifically Async<'T>.

    Therefore, if you want to return an already-started async via Async.StartChild, the way to do it would be:

    let BazWrapper() =
        let asyncTask: Async<Bar> = async {
            return Foo.Bar()
        }
        async {
            let! child = Async.StartChild asyncTask
            return! child
        }
    

    However, I suspect that you'll find that an already-started async isn't as useful to you as a "cold" async (one that isn't started yet), because a "cold" async can still be composed with other async tasks before starting it. (This can be useful for, for example, wrapping logging around your asyncs.) So if I were writing the code for your situation, I'd probably simply do this:

    let BazWrapper() =
        async {
            return Foo.Bar()
        }
    

    And now BazWrapper() returns an Async that has not been started yet, and you can either start that with Async.RunSynchronously if you want the value right away, or use it in other async computation expressions.