rspeele / TaskBuilder.fs

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

Is unitTask still required? #2

Closed 0x53A closed 7 years ago

0x53A commented 7 years ago

Hi,

thank you for this great builder, I've been using it in a few projects.

Do you remember which code constructs caused the type inference issues and made you add unitTask?

The reason I ask is that there was some work recently to improve the type inference in the compiler, so maybe this is already solved. If not, I would like to extract a repro and add an issue at https://github.com/Microsoft/visualfsharp.

In a simple test, everything seemed to work, but I do know that the solver sometimes breaks down when code gets more complex:

module Program

open System.Threading.Tasks
open FSharp.Control.Tasks.ContextSensitive

type X = { A : int ; B : int }

[<EntryPoint>]
let main argv =
    let tX = Task.FromResult({A=1;B=2})
    let t = task {
        do! Task.Delay(100)
        let! x = tX
        return x.B;
    }

    printfn "%i" t.Result

    0

^^^ this works as expected for me, using TaskBuilder.fs from 6aa7ef6e47caf9f0af56413c4bb6d256763ffbaf

rspeele commented 7 years ago

Hi,

Sadly, yes. Here is the ugly case.

type X() =
    member this.Num = 0

let xTask : Task<X> = task { return X() }

let badTask =
    task {
        let! x = xTask
        return x.Num + 1
        //      ^ FS0072: Lookup on object of indeterminate type based on
        //        information prior to this program point.
    }

I think that your example works because of the special inference on record properties (i.e. same reason you could write let f x = x.A + x.B).

I would be really glad to get rid of unitTask if I could.

0x53A commented 7 years ago

Thanks! I created https://github.com/Microsoft/visualfsharp/issues/3281, let's see what happens.

rspeele commented 7 years ago

I looked at the approach in your pull request and in the Query.fsi code linked by Don Syme, and at first I thought it only worked to discriminate between Generic<SpecificType> and Generic<'a>, so it wouldn't help with the Task situation, where we have ParentType and ChildType<'a>.

But I was also interested in playing around with using inline constraints more to support arbitrary awaitables, like the ValueTask type. I found that I could use a fancier version of the old bindGenericAwaitable to support Bind on any task-like using very similar rules to the C# compiler. With Task<'a> handled by the builder itself, and this new very generic inline await in an extension member, it seems to work. I tested with the existing test suite, a couple new compilation tests, and with my projects Rezoom and Rezoom.SQL that use this builder.

I also had to do something kind of weird where the inline bind is inside a static class with a generic parameter, otherwise the inference gave up on figuring out the return type in some cases (where you are using the inline Bind in the context of a non-inline generic function).

This is really abusing the compiler and is probably dependent on unspecified implementation details, but it works too well to avoid. Now, not only is unitTask obsolete, but we can let! bind arbitrary task-likes just like we can await them in C#!

Thanks again for posting about this problem on the FSharp github and finding out about the extension member trick. I had never encountered that technique before.

0x53A commented 7 years ago

Nice!