Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
thealexgraham opened this issue Jan 5, 2018 · 6 comments

Comments

@thealexgraham
Copy link

thealexgraham commented Jan 5, 2018

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!

@bradrn
Copy link
Contributor

bradrn commented Jan 5, 2018

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
Copy link
Author

thealexgraham commented Jan 5, 2018

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
Copy link
Contributor

bradrn commented Jan 6, 2018

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
Copy link
Author

thealexgraham commented Jan 12, 2018

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
Copy link
Owner

HeinrichApfelmus commented Jan 17, 2018

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
Copy link
Contributor

bradrn commented Jan 18, 2018

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants