いつでもどこでも
クールなRubyを書く方法

yebis0942 at Osaka RubyKaigi02

最近やったこと

Ruby 2.7で導入予定の構文を使ったコードをRuby 2.7未満の環境で動くように変換する。

今日話すこと

  • なぜそんなことをしたのか
  • コード変換のしくみ
    • Rubyのコードを解釈する
    • コードを書き換える
  • Rubyでsourcemap
  • 使ってみる
  • 実用に向けての課題

なぜそんなことを
したのか

  • Rubyに新機能が入ったらすぐ使いたい
  • しかし諸事情により旧バージョンのRubyのサポートも続ける必要が…
  • つらい
  • 旧バージョンでも動くように
    コードを変換したらいいのでは💡
  • JavaScriptのBabelみたいなやつがほしい

今回対象にした機能

新機能は3つに分類できる。

  1. 言語機能そのものの拡張
  2. クラス、メソッドの追加
  3. 糖衣構文←これを扱う

言語機能そのものの拡張

  • TracePoint
    • メソッドの呼び出しなどをフックする
  • refinement
    • スコープが制限されたモンキーパッチ

Rubyの処理系そのものに手を入れない限り再現不能、あるいは再現困難なもの

クラス、メソッドの追加

糖衣構文

  • 今回扱うのはこれ
    # numbered parameters
    [1, 2, 3].map { |n| n * 2 }
    [1, 2, 3].map { _1 * 2 }
  • &.とか.:とかパターンマッチとか
  • なくても同じことができるけど、
    コードをよりシンプルにしてくれるもの

コード変換のしくみ

  1. Rubyのコードを解釈する
    • 「n行目のm列でnumbered parameterを使ってる」
  2. 解釈した結果をもとに書き換える
    • 「n行目のm列のnumbered parameterを通常のブロック引数に書き換える」

Rubyのコードを解釈する

いくつかライブラリがある。主要なものは以下(登場順)

  • Ripper: Rubyに同梱
  • Parser: ふつうのgem
    • RuboCopとかが使ってる。
    • 出力が明快。
    • ライブラリが充実していていじりやすい。
  • RubyVM::AbstractSyntaxTree: Rubyに同梱

RubyVM::AST+ Parser

  • 両方のいいとこどりをしようとした
    • RubyVM::ASTは最新の構文に対応している
    • Parserにはコードの書き換えのための便利機能がある
  • うまくいかなかった

作戦

  1. まずRubyVM::ASTでパースする
  2. 新しい構文が出てきたらその箇所をダミーのコードで置き換える
  3. Parserで再度パースする
  4. 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行にまとめてしまうと、元コードのどの行で例外が発生したのか確定できなくなる。

  • 元コード
    • a = nil
      a[0] # ここでエラー
  • 変換後コード
    • a = nil; a[0] # ここでエラー

Rubyでminifyとかしないので大丈夫そう

対応づけの処理が楽になるので割と助かった…

デモ

  • np.rb
  • run.rb
  • bundle exec ruby -Ilib ,/run.rb
  • np.rb with 例外
  • bundle exec ruby -Ilib ,/run.rb

実装がかなり汚い

  • ノット be cool
  • 直していきます…

実用上の課題

  • コンパイルしたRubyコードをRubyGems.orgに上げたくない
  • インストール時 or 実行時にビルド?
    • npmで滅びた手法だったような
  • ビルドの過程の妥当性を機械的に検証できるような何か
    • CIの自動リリースに組み込めばなんかいい感じでいけるかもしれない

Thanks for listening!

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