Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I create an F# async from a C# method with a callback?

Suppose I have some C# code that takes a callback:

void DoSomething(Action<string> callback);

Now, I want to use this in F#, but wrap it in an async. How would I go about this?

// Not real code
let doSomething = async {
  let mutable result = null
  new Action(fun x -> result <- x) |> Tasks.DoSomething
  // Wait for result to be assigned
  return result
}

For example, suppose DoSomething looks like this:

module Tasks

  let DoSomething callback = 
    callback "Hello"
    ()

Then the output of the following should be "Hello":

let wrappedDoSomething = async {
   // Call DoSomething somehow
}

[<EntryPoint>]
let main argv =
  async {
    let! resultOfDoSomething = wrappedDoSomething
    Console.WriteLine resultOfDoSomething
    return ()
  } |> Async.RunSynchronously
  0
like image 992
sdgfsdh Avatar asked Jun 10 '18 10:06

sdgfsdh


2 Answers

The function Async.FromContinuations is, so to say, the "lowest level" of Async. All other async combinators can be expressed in terms of it.

It is the lowest level in the sense that it directly encodes the very nature of async computations - the knowledge of what to do in the three possible cases: (1) a successful completion of the previous computation step, (2) a crash of the previous computation step, and (3) cancellation from outside. These possible cases are expressed as the three function-typed arguments of the function that you pass to Async.FromContinuations. For example:

let returnFive = 
    Async.FromContinuations( fun (succ, err, cancl) ->
        succ 5
    )

async {
    let! res = returnFive
    printfn "%A" res  // Prints "5"
} 
|> Async.RunSynchronously

Here, my function fun (succ, err, cancl) -> succ 5 has decided that it has completed successfully, and calls the succ continuation to pass its computation result to the next step.

In your case, the function DoSomething expresses only one of the three cases - i.e. "what to do on successful completion". Once you're inside the callback, it means that whatever DoSomething was doing, has completed successfully. That's when you need to call the succ continuation:

let doSometingAsync = 
    Async.FromContinuations( fun (succ, err, cancl) -> 
        Tasks.DoSomething( fun res -> succ res ) 
    )

Of course, you can avoid a nested lambda-expression fun res -> succ res by passing succ directly into DoSomething as callback. Unfortunately, you'll have to explicitly specify which type of Action to use for wrapping it, which negates the advantage:

let doSometingAsync = 
    Async.FromContinuations( fun (succ, err, cancl) -> 
        Tasks.DoSomething( System.Action<string> succ ) 
    )

As an aside, note that this immediately uncovered a hole in the DoSomething's API: it ignores the error case. What happens if DoSomething fails to do whatever it was meant to do? There is no way you'd know about it, and the whole async workflow will just hang. Or, even worse: the process will exit immediately (depending on how the crash happens).

If you have any control over DoSomething, I suggest you address this issue.

like image 102
Fyodor Soikin Avatar answered Oct 17 '22 15:10

Fyodor Soikin


You can try something like:

let doSomething callback = async {
    Tasks.DoSomething(callback)
}

If your goal is to define the callback in the method you could do something like:

let doSomething () = async {
    let callback = new Action<string>(fun result -> printfn "%A" result )
    Tasks.DoSomething(callback)
}

If your goal is to have the result of the async method be used in the DoSomething callback you could do something like:

let doSomething =

         Async.StartWithContinuations(
         async {
            return result
            },
         (fun result -> Tasks.DoSomething(result)),
         (fun _ -> printfn "Deal with exception."),
         (fun _ -> printfn "Deal with cancellation."))
like image 39
Ringil Avatar answered Oct 17 '22 15:10

Ringil