Grokking F# async vs. System.Threading.Task

Viewed 189

Let's consider I've function loadCustomerProjection:

let loadCustomerProjection id =
    use session = store.OpenSession()

    let result =
      session.Load<CustomerReadModel>(id)

    match result with
    | NotNull -> Some result
    | _ -> None

session.Load is synchronous and returns a CustomerReadModel

session also provides a LoadAsync method which I'm been using like this:

let loadCustomerProjection id =
    use session = store.OpenSession()

    let y = async {
      let! x =  session.LoadAsync<CustomerReadModel>(id) |> Async.AwaitTask
      return x
    }
    let x = y |> Async.RunSynchronously
    match x with
      | NotNull -> Some x
      | _ -> None

What I'm trying to understand:

Does the second version even make sense and does it add any value in terms of non blocking behavior as both have the same signature: Guid -> CustomerReadModel option?

Would it make more sense to have this signature Guid -> Async<CustomerReadModel> if loadCustomerProjection is called from with an Giraffe HttpHandler?

Or considering the Giraffe context, would be even better to have the signature Guid -> Task<CustomerReadModel>?

In my Giraffe handler what I want to do at least is to handle a null result via pattern matching as 404 - so at some point I need to have an |> Async.AwaitTask call anyway.

3 Answers

In this particular case there is no difference between your first and second version, except that you go through all the hoops in the second version to call the async function and then immediately wait for it. But the second version blocks exactly like the first one does.

As for the hot vs cold comments in the other answers; Because you wrapped the call to LoadAsync in an async computation expression, it stays cold (because async expression only executes when it is run). If on the other hand you would write

let y =
    session.LoadAsync<CustomerReadModel>(id)
    |> Async.AwaitTask

Then the LoadAsync would start executing immediately.

If you want to support async operations then it would indeed make sense make the entire function async:

let loadCustomerProjection id =
    async {
        use session = store.OpenSession()
  
        let! x =  session.LoadAsync<CustomerReadModel>(id) |> Async.AwaitTask

        match x with
        | NotNull -> return Some x
        | _ -> return None
    }

Whether you would use Task or Async is up to you. Personally I prefer Async because it's native to F#. But in your case you might want to stick with Giraffe's decision to stick with Task to avoid conversions.

Since the main difference between a Task and an Async is that when you have a Task it is always Hot whereas the Async is Cold (until you decide to run it) it makes sense to work with Async in your backend code and then when it is time for Giraffe to use it, convert it into a Task or run it.

In your case you are just doing one thing. I imagine it is more useful to go full async if you have multiple async steps that you want to compose in some way.

See https://docs.microsoft.com/en-us/dotnet/fsharp/tutorials/asynchronous-and-concurrent-programming/async#combine-asynchronous-computations for example.

As mentioned in the other answer the main difference between Task and Async is that Tasks are started immediately whereas Asyncs must be started explicitly.

In the simple example above it wouldn't make much difference in returning the 'T, Async<'T>, or Task<'T>. My preference would probably be Task<'T> as that is what you are receiving in the function and it makes sense to continue propagating the Task<_> until the final point of use.

Note that when you are using Giraffe you should have access to TaskBuilder.fs which gives you a task { } computation expression in the FSharp.Control.Tasks.V2 module.

Related