(※以下はV8
パッケージのvignetteであるIntroduction to V8 for Rの和訳です。)
V8とV8パッケージ
V8はGoogleが開発するオープンソースのハイパフォーマンスなJavaScriptエンジンです。C++で記述されており、ECMAScript(ECMA-262)5th Editionに準拠しています。V8
パッケージはこのC++ライブラリをベースにしており、Rとは完全に独立したJavaScriptエンジンを提供します。
V8
では次のようにコンテキストを生成することでコードの評価などを行います。
library(V8)
# コンテキストの作成
ct <- v8()
# コードの評価
ct$eval("var foo = 123")
ct$eval("var bar = 456")
ct$eval("foo + bar")
[1] "579"
C++やJavaなどの他の言語とRとのインターフェース(Rcpp
やrJava
)に比べると、V8
はコンパイルが不要な上に外部の実行ファイルやランタイムに依存しないという大きな利点があります。実行エンジンの全ては6MBのパッケージ(これがさらに2MBに圧縮されています)に含まれており、主要なプラットフォームの全てで動作します。
もう少し例を見てみましょう。
# 適当なJSONの生成
cat(ct$eval("JSON.stringify({x: Math.random()})"))
{"x":0.21224139956757426}
# 適当なクロージャ
ct$eval("(function(x){return x + 1;})(123)")
[1] "124"
なお、V8それ自体は単なるJavaScriptエンジンに過ぎないということに注意してください。現状ではDOMがありませんし(つまりwindowオブジェクトもありません)、ネットワークもディスクI/Oもイベントループもありません。しかし、これらはすべてRにありますから、大した問題ではありません。この点ではV8
はRcpp
やrJava
といった他言語のインターフェースに似ています。ただ対象がJavaScriptであるというだけです。
JavaScriptライブラリの読み込み
ct$source
を使うと、ファイルやurlからライブラリを読み込めます。
ct$source(system.file("js/underscore.js", package = "V8"))
ct$source("https://cdnjs.cloudflare.com/ajax/libs/crossfilter/1.3.11/crossfilter.min.js")
データのやりとり
デフォルトでは、RとJavaScript間のすべてのデータ交換はJSONで行われます。JSONとRのデータ形式の間のマッピングは、jsonlite
パッケージによって双方向に行われます。
ct$assign("mydata", mtcars) # JSONとして読み込ませられる
head(ct$get("mydata")) # データフレームとして読み出せる
mpg cyl disp hp drat wt qsec vs am gear carb
Mazda RX4 21.0 6 160 110 3.90 2.620 16.46 0 1 4 4
Mazda RX4 Wag 21.0 6 160 110 3.90 2.875 17.02 0 1 4 4
Datsun 710 22.8 4 108 93 3.85 2.320 18.61 1 1 4 1
Hornet 4 Drive 21.4 6 258 110 3.08 3.215 19.44 1 0 3 1
Hornet Sportabout 18.7 8 360 175 3.15 3.440 17.02 0 0 3 2
Valiant 18.1 6 225 105 2.76 3.460 20.22 1 0 3 1
JS()
を使うと、JSONに変換せずにJavaScriptの式として代入することもできます。
ct$assign("foo", JS("function(x){return x*x}"))
ct$assign("bar", JS("foo(9)"))
ct$get("bar")
[1] 81
関数呼び出し
ct$call
を使うとJavaScriptの関数を呼び出せます。このとき、オブジェクトは(引数も返り値も両方とも)自動的にRとJavaScriptの間で変換されます。
ct$call("_.filter", mtcars, JS("function(x){return x.mpg < 15}"))
mpg cyl disp hp drat wt qsec vs am gear carb
Duster 360 14.3 8 360 245 3.21 3.570 15.84 0 0 3 4
Cadillac Fleetwood 10.4 8 472 205 2.93 5.250 17.98 0 0 3 4
Lincoln Continental 10.4 8 460 215 3.00 5.424 17.82 0 0 3 4
Chrysler Imperial 14.7 8 440 230 3.23 5.345 17.42 0 0 3 4
Camaro Z28 13.3 8 350 245 3.73 3.840 15.41 0 0 3 4
これはC言語の代わりにJavaScriptが呼び出される.Call
のようなものと考えると良いでしょう。
インタラクティブなコンソール
JavaScriptを学んだりセッションのデバッグをするにはコンソールでインタラクティブな操作をするのが簡単なやり方です。ct$console()
を実行すると、V8のコンソールが開き、入力待機状態となります。
ct$assign("diamonds", ggplot2::diamonds)
ct$console()
この状態ではct$eval
を実行することなく、JavaScriptをインタラクティブに実行できます。V8のコンソールに以下の入力をしてみてください。
var cf = crossfilter(diamonds)
var price = cf.dimension(function(x){return x.price})
var depth = cf.dimension(function(x){return x.depth})
price.filter([2000, 3000])
output = depth.top(10)
入力が終わったらエスケープキーを叩くかexit
とタイプしてコンソールを終了してください。結果はoutput
に保存しましたから、次のようにRから結果にアクセスできます。
output <- ct$get("output")
output
carat cut color clarity depth table price x y z
1 0.50 Fair E VS2 79.0 73 2579 5.21 5.18 4.09
2 0.50 Fair E VS2 79.0 73 2579 5.21 5.18 4.09
3 0.90 Fair G SI1 72.9 54 2691 5.74 5.67 4.16
4 0.96 Fair G SI2 72.2 56 2438 6.01 5.81 4.28
5 1.00 Fair G SI2 70.2 58 2326 6.00 5.73 4.13
6 1.00 Fair J SI2 69.9 61 2117 6.13 5.86 4.23
7 1.00 Fair H SI2 69.5 55 2875 6.17 6.10 4.26
8 1.00 Fair D SI2 69.3 58 2974 5.96 5.87 4.10
9 0.90 Fair F SI2 69.0 59 2536 5.89 5.82 4.04
10 0.81 Fair F SI2 68.8 79 2301 5.26 5.20 3.58
警告、エラー、console.log
無効なJavaScriptコードを評価すると、SyntaxErrorが返ります。
ct$eval("var foo <- 123;")
context_eval(join(src), private$context) でエラー: SyntaxError: Unexpected token <
また、JavaScriptのランタイムエラーは自動的にRに伝えられます。
ct$eval("123 + doesnotexist")
context_eval(join(src), private$context) でエラー: ReferenceError: doesnotexist is not defined
さらにconsole.log
、console.warn
、console.error
を使えば手動でJavaScriptからRにコールバックすることもできます。これにより、JavaScriptアプリケーション内からの出力、警告、エラーの詳細をRに伝えることができます。
ct$eval('console.log("this is a message")')
this is a message
ct$eval('console.warn("Heads up!")')
Warning: Heads up!
ct$eval('console.error("Oh no! An error!")')
context_eval(join(src), private$context) でエラー: Oh no! An error!
console.error
の使用例としては、外部リソースの読み込み確認が挙げられます。
ct$source("https://cdnjs.cloudflare.com/ajax/libs/crossfilter/1.3.11/crossfilter.min.js")
[1] "true"
ct$eval('var cf = crossfilter || console.error("failed to load crossfilter!")')
Global Namespace
Nodeやブラウザからの場合と違って、新しいコンテキストの名前空間にはごくわずかなものしか置かれていません。デフォルトでは、global
(自分自身への参照)、console
(console.log
やその他のため)、print
(console.log
のエイリアスですが、これを必要とするJavaScriptライブラリがあります)だけが含まれています。
ct <- v8(typed_arrays = FALSE)
ct$get(JS("Object.keys(global)"))
[1] "console" "print" "global"
型付き配列(typed array)を有効にすると、いくつか追加でグローバルオブジェクトが読み込まれます。
ct <- v8(typed_arrays = TRUE)
ct$get(JS("Object.keys(global)"))
[1] "console" "print" "global" "ArrayBuffer"
[5] "Int8Array" "Uint8Array" "Int16Array" "Uint16Array"
[9] "Int32Array" "Uint32Array" "Float32Array" "Float64Array"
[13] "DataView"
名前が設定されていない状態でも、コンテキストは常にグローバルスコープを持ちます。コンテキストをglobal = NULL
とともに初期化した場合でも、this
キーワードを評価すればグローバルスコープに到達できます。
ct2 <- v8(global = NULL, console = FALSE)
ct2$get(JS("Object.keys(this).length"))
[1] 10
ct2$assign("cars", cars)
ct2$eval("var foo = 123")
ct2$eval("function test(x){x + 1}")
ct2$get(JS("Object.keys(this).length"))
[1] 13
ct2$get(JS("Object.keys(this)"))
[1] "ArrayBuffer" "Int8Array" "Uint8Array" "Int16Array"
[5] "Uint16Array" "Int32Array" "Uint32Array" "Float32Array"
[9] "Float64Array" "DataView" "cars" "foo"
[13] "test"
独自のグローバルオブジェクトを使いたければ次のようにします。
ct2$eval("var __global__ = this")
ct2$eval("(function(){var bar = [1, 2, 3, 4]; __global__.bar = bar;})()")
ct2$get("bar")
[1] 1 2 3 4
シンタックスバリデーション
V8はJavaScriptのシンタックスのバリデーションを行うこともできます。実際に評価をすることなしに、です。
ct$validate("function foo(x){2*x}")
[1] TRUE
ct$validate("foo = function(x){2*x}")
[1] TRUE
これは、テンプレート化されたJavaScriptを通じてグラフィックスを生成するようなRパッケージで有用です。
また、JavaScriptはグローバルスコープで無名関数を定義できないことに注意してください。
ct$validate("function(x){2*x}")
[1] FALSE
無名関数の構文が正しいかどうか確認するためには()
で囲むか、!
を頭に付けます。
ct$validate("(function(x){2*x})")
[1] TRUE
ct$validate("!function(x){2*x}")
[1] TRUE
Rへのコールバック
最近追加された機能ですが、console.r
APIを利用することでJavaScript内からRとやりとりすることもできます。簡単な例を示しましょう。まずコンソールを開きます(これは以下の説明を簡単にするためであり、ct$eval
を通じてコードを評価しても同様の結果が得られるはずです)。
ctx <- v8()
ctx$console()
console.r.get
やconsole.r.assign
を通じてJavaScriptからRオブジェクトの読み書きが可能です。この関数は最後の引数にtoJSON
やfromJSON
に渡す引数をリストとして指定することもできます。
R内のデータセットであるirisをJavaScriptに読み込む例を示します。
var iris = console.r.get("iris")
var iris_col = console.r.get("iris", {dataframe: "col"})
次に、RのオブジェクトにJavaScriptから書き込む例を示します。
console.r.assign("iris2", iris)
console.r.assign("iris3", iris, {simplifyVector: false})
また、console.r.call
を使うと、Rの関数をJavaScriptから呼び出せます。最初の引数にはRの関数名を文字列で指定しますが、無名関数を指定することもできます。2つ目の引数にはリストとして引数を指定します。do.call()
と似たようなものと考えると良いでしょう。リストは名前付き、名前なし、どちらもサポートされています。実行結果はJSONとしてJavaScriptに返されます。
// rnormで乱数を生成する例
var out = console.r.call('rnorm', {n: 2,mean:10, sd:5})
var out = console.r.call('rnorm', [2, 10, 5])
// 無名関数を呼び出す例
var out = console.r.call('function(x){x^2}', {x:12})
似たようなものにconsole.r.eval
関数があります。この関数には引数は1つしかなく、文字列として渡されたコードを評価し、その結果を単にコンソールに出力します。
// sessionInfo()の結果が表示される
console.r.eval('sessionInfo()')
V8はオブジェクトの自動的な変換を行うだけでなく、R、C++、JavaScript間で例外をスタックの上下に伝搬するということもします。つまり、JavaScriptから呼び出したRのエラーをJavaScriptの例外としてキャッチできますし、その逆もできるわけです。もし例外がキャッチされなければ、これはRセッションのトップレベルにエラーとして表出します。
// Rのエラーを発生させる例
console.r.call('stop("ouch!")')
// RのエラーをJavaScriptで補足する例
try{
console.r.call('stop("ouch!")')
} catch (e) {
console.log("Uhoh R had an error: " + e)
}