4

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>?
knocte
  • 16,941
  • 11
  • 79
  • 125

1 Answers1

6

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.

knocte
  • 16,941
  • 11
  • 79
  • 125
rmunn
  • 34,942
  • 10
  • 74
  • 105
  • 1
    ahh, now I can see why `Async` is more flexible than `Task`, and then it seems now a bit ugly to me that the BCL always returns started asynchronous tasks (and this is why initially I thought I should wrap it this way), right? – knocte Mar 24 '18 at 11:29
  • 2
    Interop with C# functions that expect Tasks is why [`Async.StartAsTask`](https://msdn.microsoft.com/en-us/visualfsharpdocs/conceptual/async.startastask%5B't%5D-method-%5Bfsharp%5D) exists, which returns a `Task<'T>` (an already-started one) which is what C# code will generally expect. – rmunn Mar 24 '18 at 11:32
  • Wouldn't the already-started version not actually be already started? The only way for a "running" `Async `to exist is with `Async.StartChild`, but it's only pointing to a running `Async` after calling `let! child = ...`. Since you're immediately calling return! on the value, the returned value is simply an unstarted computation that needlessly wraps a computation in a child and immediately waits for it to finish. The only way I can see to do something equivalent to running a started async is to pass in a "beforeWaiting" function that runs after `StartChild` and before `return! child` – Stroniax Jan 29 '22 at 16:54