Ruby Advent Calendar 2023 18日目の記事です。
みなさんOpalを知っていますか?
OpalはRubyスクリプトをブラウザ等のJavaScriptランタイムで動かすための仕組みです。
Qiitaでも2016年、2018年にOpalのアドベントカレンダーが立てられていましたが、最近はOpalのタグもやや寂しい状況です1。
ブラウザでRubyを動かすといえばruby.wasmがすっかりお馴染みになった感もありますが、Opalもまだまだ開発が続けられ、近日中にv2.0のリリースが予定されていますので、今改めてOpalの紹介をしておきます。
Opalの仕組み
Opalはざっくり言うと、以下の3つが合わさったものと言えます。
- コードをRuby→JavaScriptに変換するトランスパイラ
- クラス・メソッドのシステムなど、Rubyランタイムのコア部分の実装
- Rubyの組み込みクラス・標準ライブラリの実装
3つ目の各種クラス(Array, Stringなど)も基本的にRubyで実装されていて、Opalの利用者側が書いたコードと合わせてコンパイルされます。
また、1つ目のトランスパイラはRubyで実装されているのですが、これ自体もJavaScriptへ変換することで、TryRubyのプレイグラウンド2のようにブラウザ上で入力されたRubyコードをその場で動かすといったことも可能になります。
Ruby⇔JavaScriptの連携
Opalでは``
または%x{}
の中にJavaScriptコードを書くことができます3。
さらにその中に#{}
でRubyコードを埋め込むことができ、Ruby⇔JavaScriptの連携が容易です。
# backtick_javascript: true
`#{(1..3).to_a}.map(x => x * 2)`.each_with_index do |x, index|
puts `Math.pow(x, index)`
end
こんな風にRubyとJavaScriptをごちゃまぜに書いてもちゃんと動きます。
この例ではRange#to_a
の戻り値にArray.prototype.map
を呼び出し、その結果に対してArray#each_with_index
を呼び出しています。
つまり、RubyのArrayがそのままJavaScriptのArrayとして使えたり、その逆ができています。
他にも、以下のクラスはRuby⇔JavaScript間で連携できるようになっています。
Ruby | JavaScript |
---|---|
Array | Array |
Boolean | Boolean |
Exception | Error |
Hash | Map |
Number | Number |
Proc | Function |
Regexp | RegExp |
String | String |
Time | Date |
RubyとOpalの非互換
Opalは、主にJavaScript上の制限のためにいくつか本家であるCRubyと仕様が異なっている面があります。
たとえば、上の表でRuby列に見慣れないクラスがあるのに気づいたでしょうか?OpalではCRubyにないクラスが一部存在しています。
Booleanクラス
true, falseがそれぞれTrueClass
, FalseClass
のインスタンスであることは変わりませんが、両クラスともBoolean
の子クラスという形になっています。
Numberクラス
最も大きな非互換の1つがInteger
, Float
の区別が存在せず、Number
クラスのみになっていることです。
そのため、整数演算でも場合によっては浮動小数点誤差が発生することがあります。
それが問題になる場合はBigDecimal
か、JavaScriptのBigInt
を使います。
文字列の破壊的操作
もう一つの大きな非互換が、文字列の破壊的操作ができないことです。これはJavaScriptの文字列がimmutableであることから来る制限です。
str << other
ができないので、str += other
に置き換える必要が出てきます。
これは文字列だけの話なので、配列のarr << other
は問題ありません。
SymbolがString
RubyのSymbolは現在のOpalではサポートされておらず、Symbol
クラスはString
のエイリアスになっています。
シンボルリテラルを書いても文字列としてコンパイルされるので、:foo == "foo"
がtrueになります。
ruby.wasm vs Opal
WASMによってCRubyが直接ブラウザ上で動くようになった今、Opalを使うメリットは何でしょうか?
- JavaScriptとの親和性
- 上にも書いたように、OpalはJavaScriptの配列をそのままRubyの配列として扱えるなど、直感的な操作が可能です
- ruby.wasmでは、JavaScriptのオブジェクトをRuby側に持ってくると常に
JS::Object
なので、変換が必要になってきます
一方、ruby.wasmの利点もあります。
- CRubyとの互換性
- ruby.wasmは基本的にCRubyをそのままWASMに移植したものなので、ほとんど同じ動きをすると言えます。
- Opalは先述した非互換以外に、標準クラスの各メソッドも仕様を元にRuby+JavaScriptで再実装したものであるため、細かいエッジケースでCRubyと異なる挙動をする箇所が少なからずあります。
-
今回のアドベントカレンダーではDXOpalでスイカゲームみたいなのを作ったがありましたね ↩
-
このプレイグラウンドはもともとOpalによるものがデフォルトでしたが、現在はruby.wasmがデフォルトでOpalと選択できるようになっています ↩
-
v2.0以降はマジックコメント
# backtick_javascript: true
が必要になります(無ければ通常のRubyと同じKernel#`
の呼び出しになる)。 ↩