いつでもどこでも
クールなRubyを書く方法
yebis0942 at Osaka RubyKaigi02
最近やったこと
Ruby 2.7で導入予定の構文を使ったコードをRuby 2.7未満の環境で動くように変換する。
今日話すこと
- なぜそんなことをしたのか
- コード変換のしくみ
- Rubyでsourcemap
- 使ってみる
- 実用に向けての課題
なぜそんなことを
したのか
- Rubyに新機能が入ったらすぐ使いたい
- しかし諸事情により旧バージョンのRubyのサポートも続ける必要が…
- つらい
- 旧バージョンでも動くように
コードを変換したらいいのでは💡 - JavaScriptのBabelみたいなやつがほしい
今回対象にした機能
新機能は3つに分類できる。
- 言語機能そのものの拡張
- クラス、メソッドの追加
- 糖衣構文←これを扱う
言語機能そのものの拡張
Rubyの処理系そのものに手を入れない限り再現不能、あるいは再現困難なもの
糖衣構文
- 今回扱うのはこれ
# numbered parameters
[1, 2, 3].map { |n| n * 2 }
[1, 2, 3].map { _1 * 2 }
&.
とか.:
とかパターンマッチとか- なくても同じことができるけど、
コードをよりシンプルにしてくれるもの
コード変換のしくみ
- Rubyのコードを解釈する
- 「n行目のm列でnumbered parameterを使ってる」
- 解釈した結果をもとに書き換える
- 「n行目のm列のnumbered parameterを通常のブロック引数に書き換える」
Rubyのコードを解釈する
いくつかライブラリがある。主要なものは以下(登場順)
- Ripper: Rubyに同梱
- Parser: ふつうのgem
- RuboCopとかが使ってる。
- 出力が明快。
- ライブラリが充実していていじりやすい。
RubyVM::AbstractSyntaxTree
: Rubyに同梱
RubyVM::AST
+ Parser
- 両方のいいとこどりをしようとした
RubyVM::AST
は最新の構文に対応している- Parserにはコードの書き換えのための便利機能がある
- うまくいかなかった
作戦
- まず
RubyVM::AST
でパースする - 新しい構文が出てきたらその箇所をダミーのコードで置き換える
- Parserで再度パースする
-
RubyVM::AST
で新しい構文の箇所をパースしてParserの出力と同じ形式に変換する
敗因
- 式を内包する可能性がある構文の対応が大変すぎた
- コードの位置情報を保っていないと
次の書き換えフェーズで使えない
- Parserが新構文に対応するほうが早かった…
というわけでParser
# こんな感じで手軽に使える
require 'parser/ruby27'
pp Parser::Ruby27.parse('[1].map { @1 * 2 }')
Ruby2.7の文法にも部分的に対応している
- numbered parametersは
@1
形式になっている - パターンマッチングは#574で提案中
Rubyでsourcemap
- 変換前のコードと変換後のコードの位置関係を対応づける
- フロントエンドのビルドツール(JSとかCSSとかを出力するあれこれ)では広く使われてる
- 例外のバックトレースとかで便利
- 変換後の行番号が出てもどこがバグってるのかわからない
実装上の微妙な問題
デモ: demo/error.{js,rb}
- JavaScriptは行番号 + 列番号が取れる
- Rubyの例外は発生位置の行番号しか取れないっぽい
- 列番号が取れない = 複数行の変換元コードを1行にまとめてしまうと、元コードのどの行で例外が発生したのか確定できなくなる。
例
Rubyでminifyとかしないので大丈夫そう
対応づけの処理が楽になるので割と助かった…
デモ
np.rb
run.rb
bundle exec ruby -Ilib ,/run.rb
np.rb
with 例外bundle exec ruby -Ilib ,/run.rb
実用上の課題
- コンパイルしたRubyコードをRubyGems.orgに上げたくない
- インストール時 or 実行時にビルド?
- ビルドの過程の妥当性を機械的に検証できるような何か
- CIの自動リリースに組み込めばなんかいい感じでいけるかもしれない
Thanks for listening!

RailsとかReactとかの仕事をしています。