Galaaz を触ってみた（TruffleRuby + ggplot2 で散布図を描いてみた）


日本語での言及がまだないようだったので TruffleRuby + Galaaz の人柱やってみました。GraalVM, R, ggplot2 について詳しくない人が見様見真似で書いています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234055/e9b1a91a-580b-2a60-b841-21dd1bdba3ba.png)
（このグラフは [graalvm-demos に入っているデモコード](https://github.com/graalvm/graalvm-demos/blob/e0517bd52c376f3f/galaaz-ggplot/README.md)を実行して描いたもの）

# Galaaz

GraalVM を使って R の機能を Ruby から使うための gem。

https://rubygems.org/gems/galaaz

https://github.com/rbotafogo/galaaz

---

作者の Rodrigo Botafogo さんによる解説記事。これらの記事で Galaaz の存在を知りました。

<!--
Ruby Plotting with Galaaz: An example of tightly coupling Ruby and R in GraalVM | by Rodrigo Botafogo | Towards Data Science
-->
https://towardsdatascience.com/ruby-plotting-with-galaaz-an-example-of-tightly-coupling-ruby-and-r-in-graalvm-520b69e21021

<!--
How to make Beautiful Ruby Plots with Galaaz | by Rodrigo Botafogo | We’ve moved to freeCodeCamp.org/news | Medium
-->
https://medium.com/free-code-camp/how-to-make-beautiful-ruby-plots-with-galaaz-320848058857




# graalvm-demos

<!--
graalvm/graalvm-demos: This repository contains several small applications. These programs illustrate the capabilities of GraalVM
-->
https://github.com/graalvm/graalvm-demos

> This repository contains several small applications. These programs illustrate the capabilities of GraalVM

GraalVM を使うとこんなことができるよ、というデモを集めたリポジトリのようです。いくつかあるデモの1つとして Galaaz が使えるようになっています。

以下、これを使って試しました。



# フォーク版を使う場合（おすすめ）

（この節は 2022-06-25 に追記しました）

フォークしてファイルの追加を行った `galaaz` ブランチを作りました。java11-21.2.0 を使う場合はこれを使うと簡単に試せます。
[https://github.com/sonota88/graalvm-demos/tree/galaaz](https://github.com/sonota88/graalvm-demos/tree/galaaz)

```sh
git clone https://github.com/sonota88/graalvm-demos.git
cd graalvm-demos
git checkout galaaz
```

イメージをビルドします。
[Dockerfile-galaaz](https://github.com/sonota88/graalvm-demos/blob/galaaz/galaaz-ggplot/Dockerfile-galaaz) の中を見ると分かりますが、 graalvm-demos で使っているイメージをベースとして、 `bzip2` と R のパッケージのインストールを追加しただけです。

<!--
neomatrix369/graalvm-demos:java11-21.2.0
-->

```sh
cd galaaz-ggplot

  # graalvm-demos-galaaz:1 イメージを作成
./build.sh
```

私の環境では90分程かかりました。
イメージができたら、あとはコンテナを起動して実行するだけ。

```sh
./run.sh

  # ---- 以下はコンテナ内 ----

cd /graalvm-demos/galaaz-ggplot

ruby --polyglot sample_min.rb
  # => sample_min.svg が生成される
ruby --polyglot sample.rb
  # => sample.svg が生成される
```

以上。
**この方法で試す場合は、以下に書いている「準備」の手順は全部無視して大丈夫です。**



# 準備

:::note info
（2022-06-25 追記）

- 後から気付いたのですが、 java11-21.2.0 を使う場合は以下の手順は不要でした。自分でイメージをビルドしても    `runDockerImage.sh` では無視されてしまいます。 :sweat:
- 上述の「フォーク版を使う場合」の方法で試す場合もこの手順は不要です。
:::





## Dockerfile などの修正

ダウンロード対象が存在しなくなっていてビルドに失敗するので、現在でもダウンロードできるバージョンに修正。

```diff
--- a/docker-images/Dockerfile
+++ b/docker-images/Dockerfile
@@ -99,9 +99,9 @@ ENV PATH=${GRADLE_HOME}/bin:${PATH}
 RUN echo " --- Gradle version:"; gradle --version; echo
 
 # Install jmeter
-RUN cd /tmp/; wget -q -nv "https://dlcdn.apache.org//jmeter/binaries/apache-jmeter-5.4.1.zip"
-RUN unzip /tmp/apache-jmeter-5.4.1.zip
-ENV JMETER_HOME="${WORKDIR}/apache-jmeter-5.4.1"
+RUN cd /tmp/; wget -q -nv "https://dlcdn.apache.org//jmeter/binaries/apache-jmeter-5.4.3.zip"
+RUN unzip /tmp/apache-jmeter-5.4.3.zip
+ENV JMETER_HOME="${WORKDIR}/apache-jmeter-5.4.3"
 ENV PATH=${JMETER_HOME}/bin:${PATH}
 RUN echo " --- Jmeter version:"; jmeter --version; echo
```

```diff
--- a/docker-images/buildDockerImage.sh
+++ b/docker-images/buildDockerImage.sh
@@ -24,7 +24,7 @@ FULL_GRAALVM_VERSION="${1:-"${DEFAULT_GRAALVM_VERSION}"}"
 FULL_DOCKER_TAG_NAME="graalvm-demos"
 GRAALVM_HOME_FOLDER="/opt/graalvm"
 
-MAVEN_VERSION="3.8.4"
+MAVEN_VERSION="3.8.6"
 GRADLE_VERSION="7.2"
 SCALA_VERSION="3.0.2"
 SBT_VERSION="1.5.5"
```

試していませんが、 Galaaz を試す分には必要なさそうなので JMeter と Maven をダウンロードしている箇所をコメントアウトしてもいいかもしれません。






## イメージのビルド

所要時間は30分弱。

```sh
cd docker-images
time ./buildDockerImage.sh
```

引数で指定しなければデフォルトで JDK 11 + GraalVM 21.2.0 になります。
最新のバージョンではありませんが、下手に触らずデフォルト設定で進めます。

できあがったイメージのサイズは 4.24GB。

```
$ docker images
REPOSITORY                        TAG                    IMAGE ID       CREATED          SIZE
neomatrix369/graalvm-demos        java11-21.2.0          c9ad84ce5a6a   7 months ago     4.24GB
（他のイメージは省略）
```

Galaaz 専用のイメージではなく他のデモでも使う共通のイメージなので、Galaaz を試すのには関係ないものも含まれています。必要なものに絞ればイメージサイズとビルド時間などが節約できるかもしれません。



## コンテナ起動

```sh
cd .. # リポジトリのルートに戻る
./runDockerImage.sh
```


環境変数 `DEMO_TYPE` に `gui` を指定して実行すると VNC を利用してグラフィカルな機能も利用できますが、この記事の内容的には不要なので GUI なしで使います。



以下、コンテナ内での操作。

---

とりあえず env。

```sh
root@9fa633e4b18f:/graalvm-demos# env | sort
GRAALVM_HOME=/opt/graalvm
GRADLE_HOME=/gradle-7.2
HOME=/root
HOSTNAME=9fa633e4b18f
JAVA_HOME=/opt/graalvm
JDK8_HOME=/usr/lib/jvm/java-1.8.0-openjdk-amd64
JMETER_HOME=/apache-jmeter-5.4.1
LANG=en_US.UTF-8
LANGUAGE=en_US
M2_HOME=/usr/local/apache-maven/apache-maven-3.8.3/
MICRONAUT_HOME=/root/.micronaut/micronaut-cli
PATH=/root/.micronaut/micronaut-cli/bin:/apache-jmeter-5.4.1/bin:/gradle-7.2/bin:/usr/local/apache-maven/apache-maven-3.8.3//bin:/opt/graalvm/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PWD=/graalvm-demos
SCALA_HOME=/usr/share/scala
SHLVL=0
TERM=xterm
_=/usr/bin/env
```


---

gu コマンドでインストール状況を見てみます。

```
root@9fa633e4b18f:/graalvm-demos# gu --version
GraalVM Updater 21.2.0

root@9fa633e4b18f:/graalvm-demos# gu list
ComponentId              Version             Component name                Stability                     Origin 
---------------------------------------------------------------------------------------------------------------------------------
graalvm                  21.2.0              GraalVM Core                  -                             
R                        21.2.0              FastR                         Experimental                  github.com
espresso                 21.2.0              Java on Truffle               Experimental                  github.com
js                       21.2.0              Graal.js                      Supported                     
llvm-toolchain           21.2.0              LLVM.org toolchain            Supported                     github.com
native-image             21.2.0              Native Image                  Early adopter                 github.com
nodejs                   21.2.0              Graal.nodejs                  Supported                     github.com
python                   21.2.0              Graal.Python                  Experimental                  github.com
ruby                     21.2.0.1            TruffleRuby                   Experimental                  github.com
```

諸々インストール済みになっていますね。
今回は最低限 `ruby` と `R` だけあればよさそう？


---

Ruby（TruffleRuby） と R（FastR） のバージョン。

```sh
root@9fa633e4b18f:/graalvm-demos# ruby -v
truffleruby 21.2.0.1, like ruby 2.7.3, GraalVM CE Native [x86_64-linux]

root@9fa633e4b18f:/graalvm-demos# R --version
R version 4.0.3 (FastR)
... snip ...
```

---

`/opt/graalvm/bin` を見てみます。

```sh
root@9fa633e4b18f:/graalvm-demos# ls -lF /opt/graalvm/bin/ | grep ruby
lrwxrwxrwx 1 root root     28 Oct 19  2021 bundle -> ../languages/ruby/bin/bundle*
lrwxrwxrwx 1 root root     29 Oct 19  2021 bundler -> ../languages/ruby/bin/bundler*
lrwxrwxrwx 1 root root     25 Oct 19  2021 erb -> ../languages/ruby/bin/erb*
lrwxrwxrwx 1 root root     25 Oct 19  2021 gem -> ../languages/ruby/bin/gem*
lrwxrwxrwx 1 root root     25 Oct 19  2021 irb -> ../languages/ruby/bin/irb*
lrwxrwxrwx 1 root root     26 Oct 19  2021 racc -> ../languages/ruby/bin/racc*
lrwxrwxrwx 1 root root     28 Oct 19  2021 racc2y -> ../languages/ruby/bin/racc2y*
lrwxrwxrwx 1 root root     26 Oct 19  2021 rake -> ../languages/ruby/bin/rake*
lrwxrwxrwx 1 root root     26 Oct 19  2021 rdoc -> ../languages/ruby/bin/rdoc*
lrwxrwxrwx 1 root root     24 Oct 19  2021 ri -> ../languages/ruby/bin/ri*
lrwxrwxrwx 1 root root     26 Oct 19  2021 ruby -> ../languages/ruby/bin/ruby*
lrwxrwxrwx 1 root root     33 Oct 19  2021 truffleruby -> ../languages/ruby/bin/truffleruby*
lrwxrwxrwx 1 root root     28 Oct 19  2021 y2racc -> ../languages/ruby/bin/y2racc*
```

`/opt/graalvm/languages/ruby/bin` 以下にあるものへのシンボリックリンクになっているので、そっちも見てみます。

```
root@9fa633e4b18f:/graalvm-demos# ls -lF /opt/graalvm/languages/ruby/bin
total 173876
-rwxr-xr-x 1 root root      1211 Oct 19  2021 bundle*
-rwxr-xr-x 1 root root      1213 Oct 19  2021 bundler*
-rwxr-xr-x 1 root root      5745 Oct 19  2021 erb*
-rwxr-xr-x 1 root root       552 Oct 19  2021 galaaz*
-rwxr-xr-x 1 root root      1227 Oct 19  2021 gem*
-rwxr-xr-x 1 root root       550 Oct 19  2021 gknit*
-rwxr-xr-x 1 root root       562 Oct 19  2021 gknit-draft*
-rwxr-xr-x 1 root root       548 Oct 19  2021 grun*
-rwxr-xr-x 1 root root       554 Oct 19  2021 gstudio*
-rwxr-xr-x 1 root root       567 Oct 19  2021 htmldiff*
-rwxr-xr-x 1 root root      1189 Oct 19  2021 irb*
-rwxr-xr-x 1 root root       561 Oct 19  2021 ldiff*
-rwxr-xr-x 1 root root      1268 Oct 19  2021 racc*
-rwxr-xr-x 1 root root      1272 Oct 19  2021 racc2y*
-rwxr-xr-x 1 root root      1195 Oct 19  2021 rake*
-rwxr-xr-x 1 root root      1195 Oct 19  2021 rdoc*
-rwxr-xr-x 1 root root      1191 Oct 19  2021 ri*
-rwxr-xr-x 1 root root       566 Oct 19  2021 rspec*
lrwxrwxrwx 1 root root        11 Oct 19  2021 ruby -> truffleruby*
-rwxr-xr-x 1 root root 177962856 Oct 19  2021 truffleruby*
-rwxr-xr-x 1 root root      1272 Oct 19  2021 y2racc*
```

`ruby` は `truffleruby` へのシンボリックリンク。

---

`gem environment` も見てみます。

```sh
root@9fa633e4b18f:/graalvm-demos# gem environment
RubyGems Environment:
  - RUBYGEMS VERSION: 3.1.6
  - RUBY VERSION: 2.7.3 (2021-07-29 patchlevel 0) [x86_64-linux]
  - INSTALLATION DIRECTORY: /opt/graalvm/languages/ruby/lib/gems
  - USER INSTALLATION DIRECTORY: /root/.gem/truffleruby/2.7.3.21.2.0
  - RUBY EXECUTABLE: /opt/graalvm/languages/ruby/bin/truffleruby
  - GIT EXECUTABLE: 
  - EXECUTABLE DIRECTORY: /opt/graalvm/languages/ruby/bin
  - SPEC CACHE DIRECTORY: /root/.gem/specs
  - SYSTEM CONFIGURATION DIRECTORY: /b/b/e/truffleruby/etc
  - RUBYGEMS PLATFORMS:
    - ruby
  - GEM PATHS:
     - /opt/graalvm/languages/ruby/lib/gems
     - /root/.gem/truffleruby/2.7.3.21.2.0
  - GEM CONFIGURATION:
     - :update_sources => true
     - :verbose => true
     - :backtrace => false
     - :bulk_threshold => 1000
     - "gem" => "--no-document"
  - REMOTE SOURCES:
     - https://rubygems.org/
  - SHELL PATH:
     - /root/.micronaut/micronaut-cli/bin
     - /apache-jmeter-5.4.1/bin
     - /gradle-7.2/bin
     - /usr/local/apache-maven/apache-maven-3.8.3//bin
     - /opt/graalvm/bin
     - /usr/local/sbin
     - /usr/local/bin
     - /usr/sbin
     - /usr/bin
     - /sbin
     - /bin
```

galaaz gem がインストール済みであることを確認。

```sh
root@9fa633e4b18f:/graalvm-demos# gem list | grep galaaz
galaaz (0.5.0)

root@9fa633e4b18f:/graalvm-demos# ls -lF /opt/graalvm/languages/ruby/lib/gems/gems/ | grep galaaz
drwxr-xr-x 9 root root 4096 Oct 19  2021 galaaz-0.5.0/
```



## 追加のインストール

ggplot2 のインストール時に

```
error reading connection: Cannot run program "bzip2": error=2, No such file or directory
```

というエラーが出て失敗するので、先に bzip2 をインストールしておきます。

```
apt update
apt install bzip2
```



# 散布図を描く

graalvm-demos の [galaaz-ggplot/README.md](https://github.com/graalvm/graalvm-demos/blob/master/galaaz-ggplot/README.md) などを参考にしつつ、まずは最低限のもの。

```ruby
# sample_min.rb

require "galaaz"
require "ggplot" # ... r_requires/ggplot.rb

d = R.data__frame(
  x: R.c( 1,  2,  4, 0.3, 2.5, 5.1),
  y: R.c(12, 13, 26,  22,  29,  54)
)

gg = d.ggplot(E.aes(x: :x, y: :y)) +
     R.geom_point()

R.svg("sample_min.svg")
puts gg
R.dev__off
```

実行する際、`--polyglot` オプションを付けずに単に `ruby sample_min.rb` と実行するとこうなります。

```sh
root@9fa633e4b18f:/graalvm-demos/galaaz-ggplot# ruby sample_min.rb 
/opt/graalvm/languages/ruby/lib/gems/gems/galaaz-0.5.0/lib/R_interface/r.rb:26:in `eval_file': No language for id R found. Supported languages are: [ruby] (ArgumentError)
        from /opt/graalvm/languages/ruby/lib/gems/gems/galaaz-0.5.0/lib/R_interface/r.rb:26:in `<top (required)>'
        from <internal:core> core/kernel.rb:293:in `require_relative'
        from /opt/graalvm/languages/ruby/lib/gems/gems/galaaz-0.5.0/lib/galaaz.rb:26:in `<top (required)>'
        from <internal:core> core/kernel.rb:234:in `gem_original_require'
        from /opt/graalvm/languages/ruby/lib/mri/rubygems/core_ext/kernel_require.rb:158:in `require'
        from <internal:core> core/unbound_method.rb:18:in `bind_call'
        from <internal:core> core/kernel.rb:272:in `require'
        from sample_min.rb:1:in `<main>'
<internal:core> core/kernel.rb:236:in `gem_original_require': cannot load such file -- galaaz (LoadError)
        from /opt/graalvm/languages/ruby/lib/mri/rubygems/core_ext/kernel_require.rb:83:in `require'
        from <internal:core> core/unbound_method.rb:18:in `bind_call'
        from <internal:core> core/kernel.rb:272:in `require'
        from sample_min.rb:1:in `<main>'
```

Ruby から R のプログラムを実行しようとしたところで `No language for id R found` と言われています。

```
（メモ: require "galaaz" したときの呼び出しの流れ）
requrie "galaaz"
=> galaaz.rb
=> R_interface/r.rb
=> R_interface/r_libs.R
```

`--polyglot` オプションを付けて実行しましょう。

```sh
time ruby --polyglot sample_min.rb 
```

実行すると、初回の場合は R のライブラリのインストールが始まります。
↓こういうのがインストールされるようです。

```sh
# R_interface/r_libs.R
list.of.packages <- c('rlang', 'purrr', 'formula.tools', 'lobstr', 'rticles')

# r_requires/ggplot.rb
R.install_and_loads('ggplot2', 'grid', 'gridExtra')
```

私の環境だと70分くらいかかりました（初回実行時のみです。念のため）。
実行が終わると `sample_min.svg` ができています。

:::note info
上述の「フォーク版を使う場合」の方法で試す場合は、必要なパッケージがインストール済みになっているため、すぐに実行が完了します。
:::

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234055/3bb45b50-0594-7920-4ffd-7d4780301e59.png)

（ちなみにこの画像はコンテナ外で Inkscape を使って PNG に変換したもの）

---

タイトル、軸ラベル、日本語テキスト、複数系列、回帰直線を含めたものを描いてみました。

```ruby
# sample.rb

require "galaaz"
require "ggplot"

d = R.data__frame(
  x: R.c( 1,  2,  4, 0.3, 2.5, 5.1),
  y: R.c(12, 13, 26,  22,  29,  54),
  type: R.c("系列1", "系列1", "系列1", "系列2", "系列2", "系列2")
)

gg = d.ggplot(E.aes(x: :x, y: :y, color: :type)) +
     R.geom_point() +
     R.xlim(R.c(0, 6)) +
     R.ylim(R.c(0, 60)) +
     R.geom_smooth(method: "glm", se: false) +
     R.ggtitle("タイトル") +
     R.xlab("x軸ラベル") +
     R.ylab("y軸ラベル")

R.svg("sample.svg")
puts gg
R.dev__off
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234055/961d39b8-cf8f-92c8-0972-62b5a421afc1.png)

---

というわけで、Galaaz を使って簡単な散布図を描くところまでやってみました。

graalvm-demos が便利だな、というのと、Galaaz は GraalVM の polyglot 機能の実際の利用例・デモとしてもおもしろいな、というのが素朴な感想です。

あと、以前から GraalVM に興味はあったのですが、きっかけがなくて触れていませんでした。今回実際に触ってみて雰囲気が知れてよかったです。

以下、適当なメモ。




# メモ

たとえば R で `c(...)` と書くところを、Galaaz では `R.c(...)` と書く。
……と聞くとピンとくるかもしれませんが、 method_missing が使われています。

<!--
galaaz/r.rb at v0.4.9 · rbotafogo/galaaz
-->
[github.com/rbotafogo/galaaz/blob/v0.4.9/lib/R_interface/r.rb](https://github.com/rbotafogo/galaaz/blob/v0.4.9/lib/R_interface/r.rb)

`data.frame(...)` や `dev.off(...)` のようにドットを含む場合は
`R.data__frame(...)`, `R.dev__off(...)` のようにアンダースコア2つ書く（`R::Support.parse_arg` で `__` を `.` に変換している）。

---

実際に R の機能を呼び出している箇所を探すと、次のように `Polyglot.eval` が使われている箇所が見つかります。

[github.com/rbotafogo/galaaz/blob/v0.4.9/lib/R_interface/rsupport.rb](https://github.com/rbotafogo/galaaz/blob/v0.4.9/lib/R_interface/rsupport.rb)

```rb
# 例として R::Support.eval

    def self.eval(string)
      begin
        Polyglot.eval("R", string)
      rescue RuntimeError
        raise NoMethodError.new("Method #{string} not found in R environment")
      end
    end
```

---

`R` は分かったけど `E.aes` の `E` は何？

これは R の遅延評価を Ruby でエミュレートするためのもののようです。

<!--
graalvm-demos/galaaz-ggplot at master · graalvm/graalvm-demos
-->

https://github.com/graalvm/graalvm-demos/tree/master/galaaz-ggplot

> In Ruby, there is no lazy evaluation and doing R.aes would try to evaluate aes immediately. In order to delay the evaluation of function aes we need to use E.aes. 


---

method_missing をうまく利用しているためか、意外と（？）コード量は少なめ。

[galaaz  の現在の master 7ebe763](https://github.com/rbotafogo/galaaz/tree/7ebe763704be899bc4dafa09c7ea5b3e10ae0d78) では、雑にコメント行を除くと 2,000行ちょっと。

```
$ find lib -name '*.rb' | xargs cat | grep -v '^ *#' | wc -l
2144
```