FsToolkit.ErrorHandling icon indicating copy to clipboard operation
FsToolkit.ErrorHandling copied to clipboard

How to unwrap an async

Open JordanMarr opened this issue 3 years ago • 11 comments

I just upgraded from Cvdm.ErrorHandling to this. In Cvdm.ErrorHandling, I could unwrap an Async<Result<User, string>> by using let! operator without piping into any of the AsyncResult or Result module conversion functions.

// tryGetUser = int -> Async<Result<User, string>>
let! myResult = tryGetUser(1) // returns a Result<User, string>

Now this fails because it's trying to look at the result type. But in this case, I don't want to fully unwrap it to be just a User -- I actually want it to be a Result<User, string> so that I can do some custom logic for the error case.

Is there a way to do this? I'm looking through the AsyncResult module, but I don't see anything relevant.

JordanMarr avatar Oct 08 '20 17:10 JordanMarr

Yeah this is a known problem with how the binding overloads work in F#. 😞 This is discussed in https://github.com/demystifyfp/FsToolkit.ErrorHandling/issues/88

Short answer, use the async CE instead of asyncResult or rework so you don't need to do this.

TheAngryByrd avatar Oct 08 '20 18:10 TheAngryByrd

For a more concrete example, you can an async CE inline and do something like this:

let doTheThing () = asyncResult {
    let! user = async {
        let! myResult = tryGetUser(1)
        match myResult with
        | Ok user -> user
        | Error msg -> //custom errorHandling
    }
}

TheAngryByrd avatar Oct 08 '20 18:10 TheAngryByrd

Shoot. So then my options are to revert back to Cvdm version, or rework with async CE. I suppose async CE is the better choice since Cvdm is no longer maintained.

JordanMarr avatar Oct 08 '20 18:10 JordanMarr

Yeah I did this in my codebase in many places as well. I should probably find the F# RFC for helping fix this issue so people 👍 it.

TheAngryByrd avatar Oct 08 '20 18:10 TheAngryByrd

:.(

JordanMarr avatar Oct 08 '20 18:10 JordanMarr

Would you consider adding this helper function to the AsyncResult module?

module AsyncResult =
    /// Awaits a result type without resolving it.
    let awaitResult (f: Async<Result<_, _>>) =
        f |> Async.map Ok

// Usage
// tryGetUser = int -> Async<Result<User, string>>
let! myResult = tryGetUser(1) |> AsyncResult.awaitResult // returns a Result<User, string>
match myResult with
| Ok user -> printfn "User: %A" user
| Error err -> printfn "Error: %s" err

JordanMarr avatar Oct 08 '20 21:10 JordanMarr

I'm glad you found a solution that works for you but I don't think I would accept it. Here are some of my reasons:

  1. I'd like for the real fix to be in the F# compiler itself and I'm shy to add work arounds to the library for it.
  2. This creates a Result in a Result so Async<Result<Result<'a,'err>,'err>> which doesn't seem obviously useful.
  3. awaitResult seems like a misnomer, it's not really awaiting anything, just nesting it.
  4. Explaining why this is useful in a doc-comment can be very tricky.

TheAngryByrd avatar Oct 09 '20 15:10 TheAngryByrd

I believe https://github.com/fsharp/fslang-suggestions/issues/905 is the correct suggestion to fix this in the F# compiler. I think I need to add to that issue since our particular case is because use the Source transformation to help cut down on the need for combinatorially explosive extension methods (reduction shown in https://github.com/demystifyfp/FsToolkit.ErrorHandling/pull/83).

TheAngryByrd avatar Oct 09 '20 15:10 TheAngryByrd

  1. It's meant to be more of a shim / adapter, so it's not the same 'err in the outer result.
    It would be more like this: Async<Result<Result<'a, 'originalError>, 'adaptedError>>

So the use case being that you are using a custom error DU within your asyncAwait CE, and then you want to await an async result function that doesn't use your custom error DU, and you don't want to unwrap the result.

  1. The name could be better. (I was loosely thinking of Async.AwaitTask which wraps a task)
  2. Yes, it is somewhat of an edge case -- although it didn't take very long for this case to come up for me.

I hope you'll change your mind. But if not, I guess it's an easy enough function to create locally.

JordanMarr avatar Oct 09 '20 15:10 JordanMarr

Looking again at #88, I think this is slightly different.
I'm not getting the "lookup of indeterminate object type" error.

The issue is that I'm using a custom error DU in my awaitResult -- in my case, DrawingLogError -- and I'm trying to await a function that returns a result with a different error type: ProjectData.getBim360ProjectInfo // Async<Result<ProjectInfo, string>>.

    // Custom error DU
    type DrawingLogError = 
        | ForgeNeedsAuthorization
        | GeneralError of string
    asyncResult {
        let! token = AuthController.tryGetBim360Token3 ctx |> Result.requireSome ForgeNeedsAuthorization

        // has return type of Async<Result<ProjectInfo, string>>
        // I want to await Result<ProjectInfo, string> without unwrapping the result.
        // fails with "Type mismatch. Expecting a Async<Result<ProjectInfo, DrawingLogError>> but given a Async<Result<ProjectInfo, string>>".
        let! bim360ProjectInfo = ProjectData.getBim360ProjecInfo dbCtx projectId
        
        match bim360ProjectInfo with
        | Ok projInfo -> // do stuff
        | Error msg -> // do alternate stuff
    }

So the shim function wraps the Async<Result<ProjectInfo, string>> within the matching error type Async<Result<Result<ProjectInfo, string>, DrawingLogError>>.

(Which is a very ugly type signature that should never be gazed upon with human eyes!)

JordanMarr avatar Oct 09 '20 16:10 JordanMarr

Also stumbled on this recently and thought of a potential solution. Please excuse the names, they are just placeholders!

// Marker DU, not used directly
type AsyncResultBindAsResult<'T, 'TError> = AsyncResultBindAsResult of Async<Result<'T, 'TError>>



// Wrapper function
module AsyncResult = 
  
  let bindAsResult (x : Async<Result<'T, 'TError>>) = 
    AsyncResultBindAsResult x



// Overload added to AsyncResultCE

        member inline __.Bind
            (
                bindAsResult: AsyncResultBindAsResult<'T, 'TError>,
                binder: Result<'T, 'TError> -> Async<Result<'U, 'UError>>
            ) : Async<Result<'U, 'UError>> =
            async {
                let (AsyncResultBindAsResult asyncResult) = bindAsResult
                let! result = asyncResult

                return! binder result
            }



// Usage
// tryGetUser = int -> Async<Result<User, string>>
let! myResult = tryGetUser(1) |> AsyncResult.bindAsResult // returns a Result<User, string>

Not actually tested so there may be some hurdles I'm missing.

Although advantages are pretty slight over the "double-result" solution above.

njlr avatar Nov 15 '21 10:11 njlr

AsyncResult

How would I go about defining this ? I get an error in Rider for the member: Unexpected Member in definition. Sorry if the question is weird, Im still new to F# Or is there a better fix available by now?

SebastianAtWork avatar Sep 04 '23 08:09 SebastianAtWork