Skip to content

R JavaScript binding

Gordon Woodhull edited this page Oct 19, 2015 · 9 revisions

An RCloud session runs on both client and server, and it is possible for R functions on the server to call JavaScript functions on the client, and vice versa.

The mechanism used is called an ocap, for Object Capability. In the context of RCloud, this just means a function with its environment/closure, identified by an unguessable hash key. Generally you won't see or care about the value of this hash key, because there are convenience functions which make it look like you are just calling a function.

Calling JavaScript from R

The connection between R and JavaScript usually starts on the R side, with R code requesting JavaScript code to be installed.

Often, this code comes from a resource in an R package; it may also come from an asset in the RCloud notebook.

  • Use system.file to get the path to a resource in an R package, and paste(readLines(f), collapse='\n')) to read the file.
  • Or, use rcloud.get.asset to read the contents of a notebook asset

Then, install the code in the client by calling rcloud.install.js.module, which takes the module name and the source to install.

For example, most RCloud extension packages contain a function that looks something like this, which loads the code from a package resource and installs the source on the client:

  install.js.resource <- function(package.name, module.path, module.name) {
    path <- system.file("javascript", module.path, package=package.name)
    caps <- rcloud.install.js.module(module.name,
                                     paste(readLines(path), collapse='\n'))
    caps
  }

package.name is the name of the package the resource can be found in, usually the current package.

module.path is the filename of the resource within the package.

module.name is a unique key that the javascript "module" will be stored under; it's important to remember that this code is evaluated only once, and can have state, which we'll show in a moment. By convention, this should be a valid JavaScript identifier.

The equivalent for installing JavaScript from an asset is

  install.js.asset <- function(asset.name, module.name) {
    caps <- rcloud.install.js.module(module.name, rcloud.get.asset(asset.name))
    caps
  }

asset.name is the name of the asset in the notebook, and module.name is as above.

The format of JavaScript code required by rcloud.install.js.module is particular, and deserves some explanation. Because the code will be evaluated on the client using, yes, eval, the code must be a single expression, without a terminating ;. It must also evaluate to a single function, or an object containing only functions.

Additionally, any JavaScript functions called from R will be called asynchronously: they take a function called a continuation as their last parameter, and only when the continuation is called does execution return to the R function. The continuation takes the return value as its argument. Errors are currently not supported.

Although the continuation does not need to be called immediately, the RCloud compute process will be hung until the continuation is called. As of RCloud 1.3, this means that all of RCloud will be hung (e.g. you can't edit or save code), since there is only one process.

The simplest valid JavaScript code that can be installed by RCloud is

(function(x, k) {
    k();
})

Which can be installed and called from R like this:

f <- install.js.resource('my.package', 'my.package.js', 'myModule') # or install.js.asset
f('hi')

Or, in object form:

({
f: function(a, k) {
    k()
}
})

Which can be installed and called from R like this:

o <- install.js.resource('my.package', 'my.package.js', 'myModule') # or install.js.asset
o$f(1)

As anywhere else in JavaScript, you can introduce a scope using an Immediately Invoked Function Expression if you want the code to have hidden state. However, since the functions are wrapped individually, you can't use this to refer to the same object (issue here), but there is a workaround:

((function() {
    var private_ = 0;
    var that = {
        init: function(k) {
            that.incr(k);
        },
        incr: function(k) {
            k(++private_);
        }
    };
    return that;
})())

Yes, that is a lot of brackets! If you run into a syntax error, it will be displayed in the session pane.

Calling R from JavaScript

To wrap an R function as an ocap so it can be called from JavaScript, call rcloud.support:::make.oc on it, and then pass the result to a JavaScript function that was already wrapped as an ocap.

The function will be exposed to JavaScript as an asynchronous function with an error-first callback. It's asynchronous so that it won't tie up the browser while it executes, and it "returns" the error first so that it can't be ignored because that's the de facto standard for asynchronous callbacks.

For example, say you have a function fib in R which takes a single argument len and returns len Fibonnaci numbers:

fib <- function(len) {
    fibvals <- numeric(len)
    fibvals[1] <- 1
    fibvals[2] <- 1
    for (i in 3:len) { 
        fibvals[i] <- fibvals[i-1]+fibvals[i-2]
    }
    fibvals
}

Then we wrap it in an ocap like so:

fib.oc <- rcloud.support:::make.oc(fib)

We can define our JavaScript function that calls fib.oc like so:

({
f: function(context_id, fib, k) {
    // the RCloud process is not currently reentrant
    // we have to call it later. let's use a button
    var button = $('<input type=button value="20 fibs"/>');
    button.click(function() {
        fib(20, function(err, ff) {
            // this will go to the session pane
            // because cell context is gone
            var p = $('<p></p>').append(ff.join(','));
            RCloud.session.invoke_context_callback('selection_out', context_id, p);
        })
    });
    RCloud.session.invoke_context_callback('selection_out', context_id, button)
    k('yes');
}
})

Passing the R function to JavaScript looks like this:

print(o$f(Rserve.context(), fib.oc))

Two things are of note here:

  1. The R process in RCloud is currently not reentrant: you can't call from R to JavaScript and back to R. To demonstrate this functionality in a simple way, we are creating a button which will call into R when it's clicked — after the cell is finished executing. This is one common use for calling R from JavaScript, in response to events in the user interface.
  2. This example uses an RCloud 1.6 function RCloud.session.invoke_context_callback for an easy way to add content to the cell from JavaScript. (That's also the purpose for passing Rserve.context() to JavaScript, which identifies the current cell to receive output.) The language bindings work the same in earlier versions of RCloud, and these parts can be ignored.

Promisification

Promises are a nicer way to deal with asynchronous functions. Instead of passing a callback to fib when calling it from JavaScript, one could write those lines like this:

    var fib_p = Promise.promisify(fib);
    button.click(function() {
        fib_p(7).then(function(ff) {
            // this will go to the session pane
            // because cell context is gone
            var p = $('<p></p>').append(ff.join(','));
            RCloud.session.invoke_context_callback('selection_out', context_id, p);
        })
    });

This gets especially useful when there are multiple asynchronous calls which have to be sequenced in order.