Rが好きすぎてRでJVMを実装したnoteエンジニアに話を聞いてみた
「Rは統計専用の言語ではない」
そう公言しているのはnoteエンジニアのigjitさんです。igjitさんはRのもつ変わった言語仕様の魅力にハマり、少し変わったものをいくつも開発しています。
なぜこういった開発を続けているのか本人に聞いてみると「Rのもつおもしろい言語仕様の力をもっと知ってほしくて」というなんとも言語愛にあふれる回答が返ってきました。
今回はそんなigjitさんにRでのJava仮想マシン(以下:JVM)実装をどのように進めていったのかをお聞きしたうえで、開発するうえでの重要な「すばやく試す」というエンジニアの心得についてもお話してもらいました。
igjitさん プロフィール
情報系の大学を卒業後、派遣エンジニアとしてキャリアをスタートし、Javaでのデスクトップアプリ作成やCでの組み込み開発など様々な現場で経験を積む。そこからスタートアップ企業に転職しWebエンジニアとなりRubyでの開発が中心になる。2020年6月にnoteに入社。noteでは主にデータを扱う機能のバックエンドの実装を担当。
Rとの出会いは客先で気象予測システムの評価業務を担当することになり、たまたまそのサーバにRが入っていたのがキッカケ。その仕事でRを使い、言語のおもしろさに気づく。さらにそこからR言語徹底解説を読んで言語仕様の魅力にハマっていき、Rで様々な開発を行うようになる。
Rのへんてこな言語仕様が好きだ
ー RでJVMをつくろうと思ったキッカケは何かあったのでしょうか?
もともとRで変なものをつくるのが趣味で、常におもしろい開発ネタを探していました。ここでいう「変なもの」というのは「Rを統計やデータ分析以外に使う」という意味です。
そんなあるときに、めもりーさんという方がPHPでJVMを実装しているツイートを見つけました。「これはおもしろいな」と思うと同時に「PHPでできるならRでもできるだろう」という思いがこみ上げてきて開発することにしました。
ー そんなに簡単にやってみようと思ったんですね(笑)
もともとコンパイラやインタプリタは作ったことがあったので、仮想マシンも作れるかなって(笑)
私が作ったjvmrrはFizz BuzzくらいのJavaコードは実行することができるのですが、めもりーさんの資料のおかげで開発するためのハードルが低く済みました。
ー そもそもなぜRで「変なもの」を開発するようになったのでしょうか?
Rって言語仕様が変わっていて本当におもしろいんですよ。それをもっとみんなに知ってほしくて言語仕様を活かしたものを作って公開するようになりました。Rってデータサイエンティストや研究者しか使わないイメージがあるじゃないですか?
ー たしかにそのイメージはありますね。igjitさんが思うRの言語仕様の魅力って具体的にはどんなところにあるのでしょうか?
データ分析用の言語なので集計やグラフを書くのが簡単で便利なのはもちろんなのですが、それよりもとにかく変な言語なんです(笑)
触っていると言語仕様をくわしく理解したくなる魅力があります。
x
#> Error: object 'x' not found
log(x)
#> Error: object 'x' not found
curve(log(x))
特徴的なのが、非標準評価、NSE (Non-standard evaluation)と呼ばれるものです。例えば変数xに値が割り当てられていない場合、xを評価すると当然のようにエラーが発生しますがcurve(log(x))だとエラーは発生せずに正常に実行されます。
一般的なプログラミング言語では関数は引数の値のみを利用できます。一方NSEは引数に与えられた式を利用する評価方法です。
curveによって描画された図
左側にcurve()に渡したlog(x)が表示されているじゃないですか?curveはどんな式が渡されたかわかっているんですよね。Rでは関数は与えられた式を評価せずに捕捉したり、与えられた式を利用して計算したりできるんです。
ー 他の言語をやっているとcurve(log(x))のxを見たらどこに定義されているのか気になってしまいますね。
そうですよね、初めてこの書き方を見たときに「なんでこんなことができるんだろう?」と気になってしまって、調べていくうちにどんどんRにハマっていきました。
my_curve <- function(expr) eval(substitute(expr), list(x = 1:10))
my_curve(x * 10)
#> [1] 10 20 30 40 50 60 70 80 90 100
curveのように式を利用する関数は簡単に実装ができます。与えられた式のxに1から10の値を代入した結果を返す関数はこんな感じ。
構文木を自由に読み書きできるRのメタプログラミングの力は本当に興味深いし、言語処理系を自作する際に役に立っています。
ー それだけRが好きであればnoteでもRを使っていきたいという思いはあったりするんでしょうか?
実はもうデータ分析やログの集計、ミドルウェアの性能評価などには使っているんですよね。グラフが必要なページのプロトタイプもRで書いたりしています。本体のnoteとは関係ない使い捨てのコードだったりプロトタイプだったりするので好きな言語を使わせてもらっています(笑)
ー さすがです(笑)社内でもRをひろめていきたいという気持ちはあるのでしょうか?
いや、それはそんなにないですね。もちろん使ってくれたらうれしいですけど。私はRが得意だから使っているだけなので「みんな得意な言語使ってくれ」と思っています。
JVMをつくるのは「ゆるい傾斜がずっとつづく登山をしている」ような感覚
https://igjit.github.io/slides/2019/12/jvmrr/#/19 より引用
ー 「JVMをRで実装する」と聞くと想像がしづらいのですが、どのような動作になっているのでしょうか?
ものすごく簡単に言えば、JavaクラスファイルをRで読み込んで書いてあるVM命令をパースし、それをRで実行していく形になります。
ー RとJavaだと言語仕様がだいぶ違うと思うのですが、そのあたりの差異を作り込むのは大変じゃありませんでしたか?
Javaのコードとコンパイルされたクラスファイルのバイナリコードはそもそも独立している別ものなんですよ。クラスファイルの中身はあくまでVM命令列であり、Javaの構文とは関係ないんですね。結局のところ、Rでやるのは仮想マシンの命令を解釈するインタプリタを作るということになります。
ー なるほど!よくよく考えて見ればクラスファイルを手書きで書いても実行できますもんね。
そうです、そうです。JVMをつくろうと思ったらJavaを知らなくてもできるんですよ。クラスファイルの中身さえ理解できれていれば。
ー 仮想マシンを作るうえで一番大変な作業はなんだったのでしょうか?
やっぱり最初のHello Worldを出力するまでは時間がかかりました。最初はバイナリコードを読み慣れるのが大変です。バイナリコードは人が読むようにできていないので。
https://igjit.github.io/slides/2019/12/jvmrr/#/6 より引用
https://igjit.github.io/slides/2019/12/jvmrr/#/7 より引用
ー それってかなり大変なのでは?
うーん、そうですね。バイナリが読みづらくて大変なのはあります。でも苦ではないですね。めんどうだけどその先に楽しさがあるのがわかっているので。傾斜はきつくないけど道のりが長い登山みたいなものですかね。ゆっくり続けていけばいつかは終わるみたいな。
ー わかりやすい例え!まさにバイナリを読んでいくのは、ゆっくりと長い道のりを歩いていくように、かなり地道な作業になりそうですね。
そうですね、それとVM命令を一つづつ実装していくのも割と地味な作業かもしれません。個々のVM命令は単純なので簡単に実装できるんですが、例えばFizzBuzzを動かすためには40個くらいのVM命令を正しく実装する必要があります。
ー 実際にはどのようにVM命令を実装していくのでしょうか?
javap -cコマンドでクラスファイルの実行に必要なVM命令を調べて順に実装していきます。
Hello Worldに必要なVM命令を実装して、四則演算のためのVM命令を実装して……の繰り返しで少しずつ積み上げていく感じです。高級なJavaのコードがどういうバイトコードにコンパイルされるのかを知っていくのがおもしろい。
magic <- readBin(con, "raw", 4, NA, FALSE, "big")
minor_version <- read_u2(con)
major_version <- read_u2(con)
https://github.com/igjit/jvmrr/blob/9bf7aa8fc01b825a11bad39894e8d95fe0f7aeb8/R/read_class.R#L21-L23 より引用
クラスファイルを読むコードの一部を見てみます。JVMの仕様書にクラスファイルの構造が定義されているので、その仕様通りにバイナリを読んでいきます。
最初の4バイトは0xCAFEBABEのお決まりのマジックナンバーで、その後2バイトづつクラスファイルのminor_version、major_versionというように順に読み進めます。
クラスファイルを読めたら、そこに書いてあるVM命令を順に実行していきます。例えばiaddという整数の足し算を行うVM命令はこのように実装できます。
iadd <- function() {
value2 <- pop(stack)
value1 <- pop(stack)
result <- value1 + value2
push(stack, result)
}
iaddでやることはオペランドスタックから2つの値を取ってきて、それらを足し合わせた結果をオペランドスタックに積むことです。これだけ見ると簡単にできそうに見えませんか?(笑)
ー たしかに「自分でもできそう」って勘違いするくらいコードが簡潔ですね。
個々のVM命令でやることはとても単純な処理なんですよ。JVMはスタックマシンなので、各VM命令はスタックに値をpushしたりpopしたりしながら計算を進めていきます。そういうスタック操作をどんどん書いていく。JVMは名前の通り、そういう低レベルな命令を実行する仮想的な機械です。
「小さくつくる」を積み重ねていく
ー igjitさんの開発は「バイナリコード読む」→「Hello Worldをだす」→「四則演算ができる」→「FizzBuzzができる」……という風にひとつずつ小さく積み重ねて開発していっているんですね。
最低限のコードで何かできることを積み重ねていくのは開発において大事です。道のりが長すぎると挫折してしまいますから。
最低限、Hello Worldができるくらいクラスファイルを読めるようにして、あとはちょっとずつできることを増やしていきます。常になにかが動く状態を積み重ねていき、小さい成功体験を得ていくような感じです。
ー いつからそういった開発のやり方をされていたのでしょうか?
開発の進め方だと「低レイヤを知りたい人のためのCコンパイラ作成入門」という本には影響を受けています。これがものすごく良い本で、ちょっとずつCコンパイラに機能を追加していく体験ができます。スーパープログラマとペアプロをしているような気持ちになれる。
テストを書きつつできることを増やしていくやり方で開発の基礎が学べるため、自分がコンパイラやJVMの開発をするときにはこの本がベースになっているのかなって。
「すばやく試す」と予想外のできごとに早く出会える
ー JVMの実装って最初から迷わずできるものなんですか?
いや、最初はそもそも実現可能かもわからないし、JVMの仕様をまったく知らないところから始まるので手探りで進めていきます。
実はjvmrrも実装する前にプロトタイプを作っていて、そこでいろいろ試していました。Rでバイナリを読む方法の確認から始まって、クラスファイルをちょっとづつ読みながらその仕様を理解したり、導入するライブラリの検証をしたり。
ある程度のVM命令が動くところまでは設計などは深く考えずに手早く実装していきました。
ー まずは手を動かしてみる、という感じなんですね。
ある程度、動くところまでいけば、自分が作ろうとしているものが何なのか理解できます。逆にわからないものは設計しようがありません。
プロトタイプだと、クラスファイルを実行するコードは長いif elseが続く巨大な一つの関数で、保守性が皆無でした。しかし、本実装だと必要なクラス設計がなされているし、保守性も考えて作られています。VM命令を一つづつ追加したり個別にテストするのも簡単になりました。
ー igjitさんは普段の開発でもプロトタイプを量産していますよね。
今、作ろうとしている画面のUIが使いやすいか不安だったので、プロトタイプを作ってプロジェクトのメンバーと共有しながら仕様を考えていきました。作って試す、を短い時間で3回繰り返した結果、全員が納得できる仕様にたどり着きました。個人開発でも仕事でも、雑にまずは作ってみるというやり方は同じですね。
どこまで準備しても仕様が変わったり、見落としがあったり予想外のことは起こります。すばやく試すことは予想外のことに早く出会うための手段だとも言えます。
次は「RでRからWebAssemblyへのコンパイラ」を作りたい
ー igjitさんが開発をするうえでのモチベーションはどこにあるのでしょうか?
自分の中でモチベーションは3つの軸があります。
1つ目は動くものをつくるのが単純に楽しいこと。コンパイラやインタプリタが人間の命令を解釈して実行するのを見ると、魔法みたいで何回つくってもワクワクします。
2つ目は作って発表してみんなに共有する楽しさです。私はつくったものをRの勉強会でほとんど発表しているのですが、そこでの楽しさは確実にモチベーションになっています。
3つ目は技術力向上のためです。言語はあくまで我々にとって何かを作るための道具です。道具なのだから使い方を習熟しておきたいじゃないですか。言語仕様に悩むのではなく、自由自在に使える言語があって何を作るのかに悩みたいんですよ。
ーこの3つの中で一番軸になっているものはありますか?
やっぱり「単純につくるのが楽しい」という点です。言語処理系ってワクワクするんですよね。ずっと好きです。大人になってもハンバーグとカレーライスが好きみたいな気持ち(笑)
ものを解釈する仕組みをつくっていくのが楽しいんだと思います。
ー ものづくりは「作っている途中のツラさ」というのがあると思いますが、igjitさんはそれを感じませんか?
趣味で好きなものを作っているのでつらいという感覚が正直よくわからないです。まあ一部めんどうな作業はあるかもしれませんが。登山で登りが続くとしんどいけど、だからと言って山に登るのをやめたりしないのと一緒なんじゃないですかね。
ー 今後、Rでつくっていこうと思っているものはありますか?
今はWebAssemblyに興味があります。RのコードをWebAssemblyにコンパイルするコンパイラを書こうかなと。まだ手はつけてないんですけど、RでRをパースする部分は楽なはず。
ー JVMの逆をつくる感じですね。
そうですね。あるシステムが解釈する低レベルのコードを吐き出す側なので。
ー また長い山を登っていくような開発が始まっていくんですね(笑)
そうですね(笑)
まだ自分が手をつけていない分野なのでどうなるかはわかりませんが、楽しいことにはなると思っています。
▼noteを一緒に作りませんか?
▼エンジニアの紹介記事
Text by megaya