rspeele / TaskBuilder.fs

F# computation expression builder for System.Threading.Tasks
Creative Commons Zero v1.0 Universal
235 stars 27 forks source link

Easy way to get result synchronously? #28

Closed isaacabraham closed 5 years ago

isaacabraham commented 5 years ago

Is there an equivalent of Async.RunSynchronously aside from the .Result? I'm thinking as a function that the task can be piped to.

rspeele commented 5 years ago

Nope. Even if the builder handled this case specially, you could await any sort of Task in your task {...} block and end up subject to the same problems as you'll find with .Result. Therefore this cannot be supported any better than just using .Result.

isaacabraham commented 5 years ago

@rspeele this is to stop getting spurious compiler errors like this:

[<EntryPoint>]
let main argv =
    task {
        return 0
    }.Result

which gives a compiler error: "This value is not a function and cannot be applied". The "fix" in this case is to either separate out the creation of the task and the Result member access, or wrap the task { } block in parentheses, neither of which are idiomatic.

I'd suggest adding something like Task.RunSynchronously to both (a) provide a way to avoid the error above, and (b) to provide some equivalence for people coming from the async { } world to Async.RunSynchronously.

isaacabraham commented 5 years ago

You could create an type extension on top of Task quite easily to achieve the above:

[<Extension>]
type Task() =
    static member RunSynchronously(task:_ Task) = task.Result

[<EntryPoint>]
let main argv =
    task {
        return 0
    } |> Task.RunSynchronously
rspeele commented 5 years ago

Sorry, I'm not in favor of it. Four reasons:

  1. Blocking the thread on a Task via .Result or .Wait() is not only wasteful, but depending on the synchronization context, can cause deadlocks. There should usually not be a reason to do this other than at the very top level of an app, e.g. just once within main(argv). I think including a function to make this more convenient would make it look like it is a recommended practice to use it frequently.

  2. Naming it RunSynchronously would match Async.RunSynchronously, but shouldn't, because the two are very different in this regard. Async is more like a Func<Task> and can be run many times. The task{} block is already running and invoking .Result will only block for its result, not run it again.

  3. Having an alternate way to do the same thing as .Result is confusing. Supposing I've never seen the library docs but am reading a block of code that uses this library, I would not know at a glance that this was just another way to write .Result. I might mistake it for something like Task.Run. If this was something that had to be written very frequently, the convenience of being able to use F#-idiomatic syntax might outweigh this disadvantage, but as mentioned in (1) that is not the case here.

  4. In the long term, TaskBuilder.fs will hopefully be obsoleted by inclusion in FSharp.Core. The smaller we keep our API, the less work it will be for downstream users to port their code to the compiler-optimized built in Task CE of the future.

toburger commented 5 years ago

A place where you need the synchronous result often is in F# scripting code or at the REPL. But I think a simple code snippet to facilitate the usage at the beginning of the script is enough for this scenario. That said I share your opinion of not providing such a helper out of the box.

Maybe a "universal" helper that awaits every kind of asynchronous operation for those scenario would be nice (thinking of the @ operator used in clojure, which awaits every peculiarity of asynchronous computations).