HeinrichApfelmus / threepenny-gui

GUI framework that uses the web browser as a display.
https://heinrichapfelmus.github.io/threepenny-gui/
Other
441 stars 77 forks source link

Call a Haskell function from JavaScript and have that function return data to JavaScript? #213

Open thealexgraham opened 6 years ago

thealexgraham commented 6 years ago

Offshoot of https://github.com/HeinrichApfelmus/threepenny-gui/issues/182.

I've been able to create a Haskell function that my JavaScript code can call and send variables back to Haskell:

createHaskellFunction :: (JS.IsHandler a) => String -> a -> UI ()
createHaskellFunction nm fn = do
    handler <- ffiExport fn
    runFunction $ ffi ("window." ++ nm ++ " = %1") handler

printArguments :: Int -> String -> IO ()
printArguments i str = putStrLn ((show i) ++ " " str)

-- Somewhere inside threepenny UI code....
createHaskellFunction "haskellPrintArguments" printArguments

Then in my JavaScript code I can do this:

haskellPrintArguments(15, "hello there");

and it prints in the REPL.

What I need to do is be able to pass back something from Haskell, so I can then use it in the JavaScript code:

var x = haskellValidateString("this String may be valid");
// Do something with x...

The reason for this is to use Haskell functions to validate text in a prebuilt JavaScript json editing widget (https://github.com/jdorn/json-editor) which do validation inside a JS lambda.

Any insight on this? I've been banging my head on it for a bit...

Thanks!

bradrn commented 6 years ago

Maybe you could do something like this:

haskellValidateString("this String may be valid", function (x) {
    // Do something with x...
});

The callback function would be passed to Haskell as a JSObject parameter, and could then be called from Haskell:

haskellValidateString :: Window -> String -> JSObject -> IO ()
haskellValidateString = runUI $ \s callback -> runFunction $ ffi "callback(%1)" $ validate s

-- Then, when you have access to a Window object...
createHaskellFunction "haskellValidateString" $ haskellValidateString window
thealexgraham commented 6 years ago

Hey, thanks for the quick response. I tried that (with the Window moved to follow the runUI signature) like so:

haskellValidateString :: Window -> String -> JS.JSObject -> IO ()
haskellValidateString w = runUI w $ \s callback -> runFunction $ ffi "callback(%1)" $ validate s

and got this:

src/View.hs:90:27: error:
    • Couldn't match expected type ‘String -> JS.JSObject -> IO ()’
                  with actual type ‘IO a0’
    • In the expression:
        runUI w
        $ \ s callback -> runFunction $ ffi "callback(%1)" $ validate s
      In an equation for ‘haskellValidateString’:
          haskellValidateString w
            = runUI w
              $ \ s callback -> runFunction $ ffi "callback(%1)" $ validate s

src/View.hs:90:37: error:
    • Couldn't match expected type ‘UI a0’
                  with actual type ‘String -> t0 -> UI ()’
    • The lambda expression ‘\ s callback
                               -> runFunction $ ffi "callback(%1)" $ validate s’
      has two arguments,
      but its type ‘UI a0’ has none
      In the second argument of ‘($)’, namely
        ‘\ s callback -> runFunction $ ffi "callback(%1)" $ validate s’
      In the expression:
        runUI w
        $ \ s callback -> runFunction $ ffi "callback(%1)" $ validate s

I also tried this:

haskellValidateString :: Window -> String -> JS.JSObject -> IO ()
haskellValidateString w s callback = runUI w $ runFunction $ ffi "callback(%1)" $ validate s

which did typecheck and register, but got haskell.js:267 Uncaught ReferenceError: callback is not defined when the browser tries to run the function.

I think I'm misunderstanding how the JSObject callback works.

Any further insight would be greatly appreciated.

bradrn commented 6 years ago

Oops - maybe I should have tested that function before I posted it...

The correct function is:

haskellValidateString :: Window -> String -> JS.JSObject -> IO ()
haskellValidateString w s callback = runUI w $ runFunction $ ffi "%1(%2)" callback $ validate s

I did actually test this one and it worked: I successfully managed to send a result to JS using this function.

It would be nice to have a less hacky way to do this though: the nicest way to do it would probably be to add an instance ToJS a => IsHandler (IO a).

thealexgraham commented 6 years ago

That worked great, thanks.

Unfortunately since the callback is an asynchronous call it can't do anything outside of its scope. Still useful, though. I started experimenting with passing in a variable as a JSObject which I planned to change/manipulate through the FFI, but it got pretty deep into the FFI's pointer system which I do not yet understand.

At some point when I have some free time I plan to dig a bit deeper into this, I'd love to be able to contribute to the project!

Thanks again, Alex

HeinrichApfelmus commented 6 years ago

At the moment, asynchronous calls are the only way to call Haskell from JavaScript. The reason is that I don't know how to handle nested chains like

Haskell → JavaScript → Haskell → JavaScript → …

For this to be possible, the JavaScript runtime would have to be multi-threaded (because a synchronous function has to freeze the program flow until the result is available, but a nested chain requires another program flow to be run.) Of course, that opens another can of worms.

By the way, I have written up some details about the design of the JavaScript FFI.

bradrn commented 6 years ago

At the moment, asynchronous calls are the only way to call Haskell from JavaScript. The reason is that I don't know how to handle nested chains like

Haskell → JavaScript → Haskell → JavaScript → …

For this to be possible, the JavaScript runtime would have to be multi-threaded (because a synchronous function has to freeze the program flow until the result is available, but a nested chain requires another program flow to be run.) Of course, that opens another can of worms.

I'm not a JavaScript expert by any stretch, but promises look like they could work. Something like the following API could be used:

newtype Promise = Promise { getPromise :: JSObject } -- Constructor and accessor are kept private; this newtype is for type-safety
toPromise :: ToJS a => a -> IO Promise
instance IsHandler (IO Promise) -- This returns the value as a JavaScript promise

--- then, in the program:

haskellValidateString :: String -> IO Promise
haskellValidateString s = toPromise $ validate s

obj <- exportHandler window haskellValidateString
runFunction $ ffi "window.validate = %1" obj
// In the JavaScript program:
validate("test string").then(function(isValid) {
  console.log(isValid);
});

Potentially a type argument could be added to Promise (so toPromise :: ToJS a => a -> IO (Promise a)), but I don't see any value in doing this.