Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Return async value from Fable.Remoting

Here is a client side Fable.Remoting example that prints the result of an async function.

    // Client code (Compiled to Javascript using Fable)
    // ============
    open Fable.Remoting.Client

    let server = Proxy.create<IServer>

    async {
     let! length = server.getLength “hello”
     do printfn “%d” length // 5
    }
    |> Async.StartImmediate

How do I get the length value?

like image 536
Brett Rowberry Avatar asked Aug 23 '19 04:08

Brett Rowberry


3 Answers

I see you've tagged your question with elmish, so I'm going to assume you have a Msg type defined. Don't use Async.StartImmediate or Async.RunSynchronously; in Elmish, you should use Cmd.OfAsync to schedule a message to be dispatched once the async block returns a value. There are four functions in Cmd.OfAsync (and the same four appear in Cmd.OfPromise as well): either, perform, attempt, and result. I'll break them down for you since their documentation isn't quite up to snuff yet:

  • either: takes four parameters, task, arg, ofSuccess, and ofError. task is the async function you want to call (of type 'a -> Async<'b>). arg is the parameter of type 'a that you want to pass to the task function. ofSuccess is a function of type 'b -> 'Msg: it will receive the result of the async function and is supposed to create a message, presumably one that incorporates the 'b result. Finally, ofError is a function of type exn -> 'Msg: if the task function throws an exception, then ofError will be called instead of ofSuccess, and is supposed to turn that exception into an Elmish message that your code can handle (presumably one that will log an error to the Javascript console or pop up a notification with Thoth.Toast or something like that).
  • perform: like either but there's no ofError parameter. Use this if your async command cannot fail (which is never the case with remote API calls, as it's always possible the network is down or your server is unresponsive), or if you just don't care about exceptions and don't mind an unhandled exception getting thrown.
  • attempt: like either but there's no ofSuccess parameter, so the task function's result will be ignored if it succeeds.
  • result: this one is completely different. It just takes a single parameter of type Async<'Msg>, i.e. you pass it an async block that is already going to produce a message.

With the code you've written, you would use Cmd.OfAsync.result if you wanted to make minimal changes to your code, but I would suggest using Cmd.OfAsync.perform instead (and upgrading it to Cmd.OfAsync.either once you have written some error-handling code). I'll show you both ways:

type Msg =
    // ... rest of your messages go here
    | GetLength of string
    | LengthResult of int

let update msg model =
    match msg with
    // ... rest of your update function
    | GetLength s ->
        let usePerform = true
        if usePerform then
            model, Cmd.OfAsync.perform server.getLength s LengthResult
        else
            let length : Async<Msg> = async {
                let! length = server.getLength s
                return (LengthResult length)
            }
            model, Cmd.OfAsync.result length
    | LengthResult len ->
        // Do whatever you needed to do with the API result
        printfn "Length was %d" len
        model, Cmd.none

And if you were using either (which you really should do once you go to production), there would be a third message LogError of exn that would be handled like:

    | LogError e ->
        printfn "Error: %s" e.Message
        model, Cmd.none

and the Cmd.OfAsync.perform line in the code above would become:

        model, Cmd.OfAsync.either server.getLength s LengthResult LogError

That's the right way to handle async-producing functions in Elmish.

like image 167
rmunn Avatar answered Nov 15 '22 10:11

rmunn


Async is one of the places where you use return in F#. So you need to return the length value. Also, Async.StartImmediate returns () (unit). Use something else, e.g. Async.RunSynchronously if you need the extracted value. Depends on what you need to achieve with it.

let length = 
    async {
         let! length = async {return String.length "hello"}
         do printfn "%d" length // 5
         return length
        } |> Async.RunSynchronously

length // val it : int = 5

Btw, you mention fable. So you might be able to use JS promise.

Some resources on Async in F#:

F# Async Guide from Jet

Async Programming

FSharp for Fun and Profit

Microsoft Docs

C# and F# Async

like image 23
s952163 Avatar answered Nov 15 '22 11:11

s952163


For those who want to call from js code.

// Client code (Compiled to Javascript using Fable)
// ============
open Fable.Remoting.Client
open Fable.Core // required for Async.StartAsPromise 


let server = Proxy.create<IServer>
let len_from_fable () = 
    async {
         let! length = server.getLength “hello”
         return length
    } |> Async.StartAsPromise

call from js

    async func() {
        let len = await len_from_fable()
        print(len)
    }

works in fable 3.0.

like image 27
echo Avatar answered Nov 15 '22 09:11

echo