-
Notifications
You must be signed in to change notification settings - Fork 141
R JavaScript binding
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.
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.
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:
- 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.
- 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 passingRserve.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.
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.