tweag/inline-js

Name: inline-js

Owner: Tweag I/O

Description: Execute Node.js scripts in Haskell.

Created: 2017-09-23 10:25:25.0

Updated: 2018-04-24 10:13:05.0

Pushed: 2018-04-07 09:28:29.0

Homepage: null

Size: 13098

Language: Haskell

GitHub Committers

UserMost Recent Commit# Commits

Other Committers

UserEmailMost Recent Commit# Commits

README

inline-js: execute Node.js scripts in Haskell

CircleCI AppVeyor

NOTE: this is alpha quality software. Please don't publicize widely.

Usage
Adding dependency

First, add inline-js to the custom-setup stanza of your package's .cabal config. The Setup.hs script looks like:

rt Language.JavaScript.Inline.Configure

 :: IO ()
 = defaultMainWithInlineJS defaultConfigureOptions

This will execute some additional post-configure actions when building your package, for example running npm install to initialize the eval server. The initialization will be performed only once if your package is built without re-configuring. The ConfigureOptions type in Language.JavaScript.Inline.Configure provide several fields to customize the initialization, for example, specifying a minimum node version, supplying additional environment variables or running custom shell commands.

If you need to add npm dependencies, you can add them like this:

LANGUAGE OverloadedStrings #-}

rt Language.JavaScript.Inline.Configure

 :: IO ()
 = defaultMainWithInlineJS $ defaultConfigureOptions { commands = ["npm install left-pad"] }

Then, add inline-js as a regular dependency for your build target. The above Setup.hs scripts will produce a .buildinfo file in the project directory, so you may want to add it to the .gitignore.

Using inline JavaScript
LANGUAGE OverloadedStrings #-}
LANGUAGE QuasiQuotes #-}
LANGUAGE TemplateHaskell #-}

rt Control.Exception
rt Data.Aeson
rt Language.JavaScript.Inline.Configure
rt Language.JavaScript.Inline.Session

 :: IO ()
 = do
<- newSession $(configureOptionsQ)
ip finally (closeSession s) $ do
nul <- eval s "let answer = 6 * 7"
print (nul :: Value)
answer <- eval s "answer"
print (answer :: Double)
tmpdir <- eval s "require('os').tmpdir()"
print (tmpdir :: String)
async <-
  eval
    s
    [js|new Promise((resolve, reject) => resolve('the answer is: ' + $(answer)))|]
print (async :: String)

Executing newSession will start the eval server. An eval server has a single V8 execution context, and bindings are shared across multiple eval calls. require is available. newSession requires a ConfigureOptions value which should not be written by hand, but supplied by using the $(configureOptionsQ) splice, which fetches the value from .buildinfo generated by our custom Setup.hs script.

eval accepts Text as a JavaScript source code, performs evaluation and returns the result. The result is marshalled to the Haskell world with a FromJSON instance, so you may need to annotate the result type if it can't be inferred from the context. Evaluation failure will raise an exception.

You can use the js quasi-quoter to embed inline JavaScript in Haskell. The inline JavaScript snippet can contain interpolations like $(var). var is a Haskell binding in scope, and it will be marshalled from the Haskell world with a ToJSON instance.

closeSession will terminate the eval server. Remember to use a bracket-like function to make sure the finalizer is invoked even in case of exception, to prevent dangling node processes.

Using foreign import javascript

There is another higher-level wrapper for Session and its operations. Language.JavaScript.Inline.MonadJS provides a MonadJS class which supports eval, along with a JST monad transformer.

Combined with Language.JavaScript.Inline.Import.js, we can actually use foreign import javascript in our code like this:

LANGUAGE QuasiQuotes #-}
LANGUAGE TemplateHaskell #-}

rt Control.Monad.IO.Class
rt Data.Aeson
rt Language.JavaScript.Inline.Configure
rt Language.JavaScript.Inline.Import
rt Language.JavaScript.Inline.MonadJS

 [d|

reign import javascript "$(0) * $(1)" mul :: MonadJS m => Double -> Double -> m Double

reign import javascript "require('os').tmpdir()" getTmpDir :: MonadJS m => m FilePath

reign import javascript "new Promise(resolve => resolve('The answer is: ' + $(0)))" oracle :: (MonadJS m, ToJSON a) => a -> m String

)

 :: IO ()
 =
nJST $(configureOptionsQ) $ do
answer <- mul 6 7
liftIO $ print answer
tmpdir <- getTmpDir
liftIO $ print tmpdir
s <- oracle answer
liftIO $ print s

The above demo is included in examples. The haddock documentation of the master branch is available here.

Sponsors

         Tweag I/O

inline-js is maintained by Tweag I/O.

Have questions? Need help? Tweet at @tweagio.


This work is supported by the National Institutes of Health's National Center for Advancing Translational Sciences, Grant Number U24TR002306. This work is solely the responsibility of the creators and does not necessarily represent the official views of the National Institutes of Health.