HeinrichApfelmus / threepenny-gui

GUI framework that uses the web browser as a display.

Home Page:https://heinrichapfelmus.github.io/threepenny-gui/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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

thealexgraham opened this issue · comments

Offshoot of #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!

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

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.

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).

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

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.

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.