JordanMarr / SqlHydra

SqlHydra is a suite of NuGet packages for working with databases in F# including code generation tools and query expressions.
MIT License
223 stars 22 forks source link

Conversion from Task to Async to Task #8

Closed stoft closed 2 years ago

stoft commented 2 years ago

Hello! I noticed that there is a conversion from Task to Async to Task here: https://github.com/JordanMarr/SqlHydra/blob/7d24b1d1b8266786bb3e03a17f3e6b214e128a59/src/SqlHydra.Query/QueryContext.fs#L127

and here: https://github.com/JordanMarr/SqlHydra/blob/7d24b1d1b8266786bb3e03a17f3e6b214e128a59/src/SqlHydra.Query/QueryContext.fs#L136

Just for my understanding, is this double conversion really necessary? And possibly correlated, why choose to return a Task and not an Async?

Cheers!

JordanMarr commented 2 years ago

Hi!

That is a great question. I chose to work with tasks because

All of other the QueryContext data methods use tasks. The method you referenced happen to be the few places where I actually needed to await a result, therefore it was necessary to call Async.AwaitTask (see the F# Async Programming docs).
For those cases, I also had to call StartImmediateAsTask; otherwise, they would be the only methods that return async instead of task.

One other point of interest is that I initially used a third party TaskBuilder library, but then decided that there weren't enough calls to justify forcing that dependency.

stoft commented 2 years ago

Is this different from accessing Task.Result? "If the Result property is accessed before the computation finishes, the property blocks the calling thread until the value is available."

JordanMarr commented 2 years ago

Yes, this approach is actually asynchronous, whereas Task.Result would not be because it would block (wait) for the database response before returning the result.

stoft commented 2 years ago

And thus it's up to the caller of InsertAsync to block or not? For lack of a Task.map or similar (which a task builder lib or v6 would provide). Makes sense. 😄

JordanMarr commented 2 years ago

I almost always add my own Task.fs with Task.map to my data projects since it’s only a few lines.

Alternatively, there is also the non async overload of Insert if you don’t care about async which blocks for you and automatically unwraps the task.

Also, you are still free to use TaskBuilder in your project -- I just removed the dependency so that you are not required to use it (since some people are already using F# 6.0, which would make TaskBuilder redundant for them).

JordanMarr commented 2 years ago

For your convenience, this is the one I created for my current project. I considered adding some of this stuff to the library, but wanted to wait and see what is included in F# 6.0 first.

/// Utility functions for working with tasks.
module Task

open FSharp.Control.Tasks.V2
open System.Threading.Tasks

let bind (f : 'a -> Task<'b>) (x : Task<'a>) = 
    task {
        let! x = x
        return! f x
    }

/// Maps the result of Task<'a> to Task<'b>
let map<'a, 'b> (f : 'a -> 'b) (tsk: Task<'a>) =
    task {
        let! taskValue = tsk
        return f taskValue
    }

let mapSeq<'a, 'b> (f : 'a -> 'b) (itemsTask: Task<'a list>) =
    task {
        let! items = itemsTask
        return items |> Seq.map f
    }

let mapArray<'a, 'b> (f : 'a -> 'b) (itemsTask: Task<'a seq>) =
    task {
        let! items = itemsTask
        return items |> Seq.map f |> Seq.toArray
    }

let awaitIgnore (tsk: Task<'T>) =
    task {
        let! result = tsk
        result |> ignore
    }
    :> Task

let fromResult value = 
    System.Threading.Tasks.Task.FromResult value
stoft commented 2 years ago

Thank you for the insights, very helpful for my understanding. 🙂