forked from JohnCoene/javascript-for-r
-
Notifications
You must be signed in to change notification settings - Fork 0
/
6-01-computations-v8.Rmd
329 lines (238 loc) Β· 12.4 KB
/
6-01-computations-v8.Rmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
# (PART) JavaScript for Computations {-}
# The V8 Engine {#v8}
V8 is an R interface to Google's open-source JavaScript engine of the same name; it powers Google Chrome, Node.js, and many other things. It is the last integration of JavaScript with R that is covered in this book. Both the V8 package and the engine it wraps are straightforward yet amazingly powerful.
## Installation {#v8-installation}
First, install the V8 engine itself, instructions to do so are well detailed on [V8's README](https://github.com/jeroen/v8#installation) and below.
On Debian or Ubuntu use the code below from the terminal to install [libv8](https://v8.dev/).
```bash
sudo apt-get install -y libv8-dev
```
On Centos install v8-devel, which requires the EPEL tools.
```bash
sudo yum install epel-release
sudo yum install v8-devel
```
On Mac OS use [Homebrew](https://brew.sh/).
```bash
brew install v8
```
Then install the R package from \index{CRAN}.
```r
install.packages("V8")
```
## Basics {#v8-basics}
V8 provides an JavaScript execution environment\index{environment} through returning a closure-based object with `v8()`; Each of such environments\index{environment} is independent of another.
```{r v8-basic-load}
library(V8)
engine <- v8()
```
The `eval` method allows running JavaScript code from R.
```{r}
engine$eval("var x = 3 + 4;") # this is evaluated in R
engine$eval("x")
```
Two observations are worth making on the above snippet of code. First, the variable we got back in R is a character vector when it should have been either an integer or a numeric. This is because we used the `eval` method, which returns what is printed in the V8 console, but `get` is more appropriate; it converts the output to its appropriate R equivalent.
```{r}
# retrieve the previously created variable
(x <- engine$get("x"))
class(x)
```
Second, while creating a scalar with `eval("var x = 1;")` appears painless, imagine if you will the horror of having to convert a data frame to a JavaScript array via jsonlite then flatten it to character string so it can be used with the `eval` method. Horrid. Thankfully V8 comes with a method `assign`, complimentary to `get`, which declares R objects as JavaScript variables. It takes two arguments, first the name of the variable to create, second the object to assign to it.
```{r}
# assign and retrieve a data.frame
engine$assign("vehicles", cars[1:3, ])
engine$get("vehicles")
```
All of the conversion is handled by V8 internally with jsonlite, as demonstrated in the previous chapter. We can confirm that the data frame was converted to a list row-wise, using `JSON.stringify` to display how the object is stored in V8.
```{r}
cat(engine$eval("JSON.stringify(vehicles, null, 2);"))
```
However this reveals a tedious cyclical loop: 1) creating an object in JavaScript to 2) run a function on the aforementioned object, 3) get the results back in R, and repeat. So V8 also allows calling JavaScript functions on R objects directly with the `call` method and obtains the results back in R.
```r
engine$eval("new Date();") # using eval
```
```
#> [1] "Sun Oct 18 2020 18:34:45 GMT+0200
(Central European Summer Time)"
```
```r
engine$call("Date", Sys.Date()) # using call
```
```
#> [1] "Sun Oct 18 2020 18:34:45 GMT+0200
(Central European Summer Time)"
```
Finally, one can run code interactively rather than as strings by calling the console from the engine with `engine$console()`. You can then exit the console by typing `exit` or hitting the <kbd>ESC</kbd> key.
## External Libraries {#v8-external}
V8 is quite bare in and of itself; there is, for instance, no functionalities built in to read or write files from disk. It thus becomes truly interesting when you can leverage JavaScript libraries. We'll demonstrate this using [fuse.js](https://fusejs.io/) a fuzzy-search library.
The very first step of integrating any external library is to look at the code (often examples) to grasp an idea of what is to be achieved from R. Below is an example from the official documentation. First, an array of two `books` is defined; this is later used to test the search. Then another array of options is defined. This should at the very least include the key(s) that should be searched; here it is set to search through the title and authors. Then, the fuse object is initialised based on the array of books and the options. Finally, the `search` method is used to retrieve all books, the title or author of which partially match the term `tion`.
```js
// books to search through
var books = [{
'ISBN': 'A',
'title': "Old Man's War",
'author': 'John Scalzi'
}, {
'ISBN': 'B',
'title': 'The Lock Artist',
'author': 'Steve Hamilton'
}]
const options = {
// Search in `author` and in `title` array
keys: ['author', 'title']
}
// initialise
const fuse = new Fuse(books, options)
// search 'tion' in authors and titles
const result = fuse.search('tion')
```
With some understanding of what is to be reproduced in R, we can import the library with the `source` method, which takes a `file` argument that will accept a path or URL to a JavaScript file to source. Below we use the handy CDN (Content Delivery Network) to avoid downloading a file.
```{r, echo=FALSE}
rm(engine)
engine <- V8::new_context()
```
```{r v8-load-fuse}
uri <- paste0(
"https://cdnjs.cloudflare.com/ajax/",
"libs/fuse.js/3.4.6/fuse.min.js"
)
engine$source(uri)
```
You can think of it as using the `script` tag in HTML\index{HTML} to source (`src`) said file from disk or CDN\index{CDN}.
```html
<html>
<head>
<script
src='https://cdnjs.cloudflare.com/.../fuse.min.js'>
</script>
</head>
<body>
</body>
</html>
```
Now onto replicating the array (list) which we want to search through, the `books` object used in a previous example. As already observed, this is in essence, how V8 stores data frames in the environment\index{environment}. Below we define a data frame of books that looks similar and load it into the engine.
```{r}
books <- data.frame(
title = c(
"Rights of Man",
"Black Swan",
"Common Sense",
"Sense and Sensibility"
),
id = c("a", "b", "c", "d")
)
engine$assign("books", books)
```
Then again, we can make sure that the data frame was turned into a row-wise JSON\index{JSON} object.
```{r}
cat(engine$eval("JSON.stringify(books, null, 2);"))
```
Now we can define options for the search; we don't get into the details of fuse.js here as this is not the purpose of this book. You can read more about the options in the [examples section](https://fusejs.io/#Examples) of the site. We can mimic the format of the JSON options shown on the website with a simple list and assign that to a new variable in the engine. Note that we wrap the title in a `list` to ensure it is converted to an array of length 1: `list("title")` should be converted to a `["title"]` array and not a `"title"` scalar.
```js
// JavaScript
var options = {
keys: ['title'],
id: 'id'
}
```
```{r}
# R
options <- list(
keys = list("title"),
id = "id"
)
engine$assign("options", options)
```
Then we can finish the second step of the online examples, instantiate a fuse.js object with the books and options objects, then do a search, the result of which is assigned to an object which is retrieved in R with `get`.
```{r}
engine$eval("var fuse = new Fuse(books, options)")
engine$eval("var results = fuse.search('sense')")
engine$get("results")
```
A search for "sense" returns a vector of ids where the term "sense" was found; `c` and `d` or the books *Common Sense*, *Sense and Sensibility*. We could perhaps make that last code simpler using the `call` method.
```{r}
engine$call("fuse.search", "sense")
```
## NPM Packages {#v8-npm}
We can also use [npm](https://www.npmjs.com/) packages, though not all will work. NPM is Node's package manager, or in a sense Node's equivalent of CRAN\index{CRAN}.
To use NPM packages we need [browserify](http://browserify.org/), a node library to bundle all dependencies of an NPM package into a single file, which can subsequently be imported in V8. Browserify is itself an NPM package, and therefore requires Node.js to be installed. The reason browserify is required will be covered in more depth in chapter 20, in essence, NPM assumes disk access to load dependencies in `require()` (JavaScript) statements. This will not work with V8. Browserify will bundle all the files that comprise an NPM module into a single file that does not require disk access.
You can install browserify globally with the following the `g` flag. Once Node.js installed, browserify can be installed from the _terminal_ (not R console) with the `npm` command.
```bash
npm install -g browserify
```
We can now "browserify" an npm package. To demonstrate, we will use [ms](https://github.com/zeit/ms), which converts various time formats to milliseconds. First, we install the npm package.
```bash
npm install ms
```
Then we browserify it. From the terminal, the first line creates a file called `in.js` which contains `global.ms = require('ms');` we then call browserify on that file specifying `ms.js` as output file. The `require` function in JavaScript is used to import files, `require('ms')` imports `ms.js`, it's to some extend like `source("ms.R")`.
```bash
echo "global.ms = require('ms');" > in.js
browserify in.js -o ms.js
```
We can now source `ms.js` with V8. Before we do so we ought to look at example code to see what has to be reproduced using V8. Luckily the library is very straightforward: it includes a single function for all conversions, e.g.: `ms('2 days')` to convert two days in milliseconds.
```{r, results='hide'}
library(V8)
ms <- v8()
ms$source("ms.js")
```
Then using the library simply consists of using `eval` or preferably `call` (for cleaner code and data interpretation to R).
```{r}
ms$eval("ms('2 days')")
ms$call("ms", "2s") # 2 seconds
```
## Use in Packages {#v8-pkg}
In this section, we detail how one should go about using V8 in an R package. If you are not familiar with package development you can skip ahead. We start by creating a package called "ms" that will hold functionalities we explored in the previous section on NPM packages.
```r
usethis::create_package('ms')
```
The package is going to rely on V8 so it needs to be added under `Imports` in the `DESCRIPTION` file, then again this can be done with usethis as shown below.
```r
# add V8 to DESCRIPTION
usethis::use_package("V8")
```
The package should also include the external library `ms.js` browserified from the NPM package, which should be placed it in the `inst` directory. Create it and place the `ms.js` file within the latter.
```r
dir.create("inst")
```
As explored, the core of the V8 package is the execution environment(s)\index{environment} that are spawned using the `v8` function. One could perhaps provide a function that returns the object created by `v8`, but it would not be convenient: this function would need to be called explicitly by the users of the package, and the output of it would need to be passed to every subsequent function. Thankfully there is a better way.
Instead, we can use the function `.onLoad`, to create the execution environment\index{environment} and import the dependency when the package is loaded by the user.
You can read more about this function in Hadley Wickham's [*Advanced R* book](http://r-pkgs.had.co.nz/r.html). This is, in effect, very similar to how the Python integration of R, [reticulate](https://rstudio.github.io/reticulate) [@R-reticulate], is [used in packages](https://rstudio.github.io/reticulate/articles/package.html). This function is often placed in a `zzz.R` file.
```r
# zzz.R
ms <- NULL
.onLoad <- function(libname, pkgname){
ms <<- V8::v8()
}
```
At this stage the package's directory structure should look similar to the tree below.
```bash
.
βββ DESCRIPTION
βββ NAMESPACE
βββ R
β βββ zzz.R
βββ inst
βββ ms.js
```
Now the dependency\index{dependency} can be sourced in the `.onLoad` function. We can locate the files in the `inst` directory with the `system.file` function.
```r
# zzz.R
ms <- NULL
.onLoad <- function(libname, pkgname){
ms <<- V8::v8()
# locate dependency file
dep <- system.file("ms.js", package = "ms")
ms$source(dep)
}
```
We can then create a `to_ms` function. It will have access to the `ms` object we instantiated in `.onLoad`.
```{r}
#' @export
to_ms <- function(string){
ms$call("ms", string)
}
```
After running `devtools::document()` and installing the package with `devtools::install()`, it's ready to be used.
```{r}
ms::to_ms("10 hrs")
```