Ruby 3.1の構文アップデート

この記事は「ESM Advent Calendar 2021」の1日目の記事です。

adventar.org

RuboCop のバックエンドでは、Ruby を解析して AST (抽象構文木) の Ruby オブジェクトとして扱えるようにする Parser gem を使っています。Parser gem のメンテナンスは、Ruby の parse.y に入った構文のアップデートを元に行っています。本エントリでは parse.y の観察を元にして、クリスマスのリリース予定までカウントダウンとなっている Ruby 3.1 構文のアップデート (2021年12月1日時点) をまとめてみます。

所感としては、Ruby 3.1 ではいくつかの構文への省略記法を中心に導入されるといった印象です。

新構文

Hash リテラルの値省略の構文が追加される

ruby/ruby@c60dbcd で導入されました。

値を代入した変数を Hash リテラルの要素に記す際に、変数名を Hash のキーとして、値を省略できるようになります。

a = 1
b = 2

{a:, b:} #=> {:a=>1, :b=>2}

以下と同意です。

a = 1
b = 2

{a: a, b: b} #=> {:a=>1, :b=>2}

個人的には、ES6 のプロパティのショートハンドという例もあり、キーと値を "DRY" にできるので良さそうという印象を持っていますが、明示的に書くという見方の好みもありそう?といった印象です。

匿名ブロック移譲の構文が追加される

ruby/ruby@4adb012 で導入されました。

ブロック引数を移譲する際にブロック変数名を省略できるようになります。

def foo(&)
  bar(&)
end

Ruby 3.0 までは引数に何か名前が必要です。

def foo(&block)
  bar(&block)
end

個人的には、&block&proc 以外ほとんど名前をつけることなさそうなので、そういった情報落ちがない (たぶんほとんどの場合) はこの & という象徴で十分良さそうに思っています (移譲専用というのが少し残念に思うくらい良さそう) 。

なお、匿名ブロック移譲のメソッド定義の外で bar(&) 単独で使おうとすると no anonymous block parameter エラーが発生します。

パターンマッチに pin オペレーターが追加される

ruby/ruby@2186347ruby/ruby@fa87f72 で導入されました。

パターンマッチの新構文となる pin オペレータをサポートするようになります。大元のサンプルから持ってくると ^ を使った式をこんな感じでパターンマッチ中で取れるようになります。

Prime.each_cons(2).lazy.find_all { _1 in [n, ^(n + 2)] }.take(3).to_a
#=> [[3, 5], [5, 7], [11, 13]]

既存構文の拡張

endless メソッド定義の本体メソッド呼び出しの括弧が省略可能になる

ruby/ruby@31794d2 で導入されました。

endless メソッド定義の本体メソッド呼び出しの括弧が省略可能になります。

def foo = puts 'Hello'

Ruby 3.0 までは引数にカッコが必須です。

def foo = puts('Hello')

1行パターンマッチのメソッド呼び出しの括弧が省略可能になる

ruby/ruby@ecb6d6a で導入されました。

1行パターンマッチのメソッド呼び出しの括弧が省略可能になります。

[1, 2] => a, b

Ruby 3.0 までは右辺にカッコが必須です。

[1, 2] => [a, b]

Arguments Forwarding のメソッド仮引数のカッコが省略可能になる

ruby/ruby@13a9597 で導入されました。

Arguments Forwarding のメソッド仮引数のカッコが省略可能になります。

def foo a, ...
end

Ruby 3.0 までは仮引数にカッコが必須です。

def foo(a, ...)
end

またデフォルト引数と Arguments Forwarding を一緒に定義できるようにもなっていました。これはカッコの有無に関わらず、Ruby 3.0 では構文エラーだったものです。

def foo(a, b = 42, ...)
end

ここで紹介した構文アップデートは Parser gem 3.0.3.1 までで対応済みで、RuboCop で使おうとすると匿名ブロック移譲でエラーになるのが現状です。修正パッチは PR 送付済みですので、クリスマスリリースには RuboCop でもひととおりの Ruby 3.1 構文は使えるようになっていると思います。

なお、Ruby 3.1 は開発版となります。ちょっと試してみようという場合、rbenv であれば以下でインストールができます。

% rbenv install 3.1.0-dev

以上、Ruby 3.1 の先行プレビュー記事でした。クリスマスリリースが楽しみですね。

「ESM Advent Calendar 2021」の 2日目は @m_pixy のエントリです。

Ruby 3.1を使ってRuboCopでエラーが起きた場合の対処

先日、Ruby 3.1 で起きる each_conseach_slice の非互換について記しましたが、RuboCop で非互換の影響を受ける部分とその対処について記しておきます。

koic.hatenablog.com

対象となる組み合わせは Ruby 3.1 と RuboCop 1.22.2 以下です。

RuboCop 1.22.2 までは以下のパッチが当たっていません。

github.com

そのため、Layout/BlockAlignment cop が有効になっている場合は、Ruby 3.1 での rubocop コマンドの実行で以下のエラーが起きえます。

      # ./lib/rubocop/cop/layout/block_alignment.rb:97:in `start_for_block_node'
      # ./lib/rubocop/cop/layout/block_alignment.rb:82:in `on_block'
      # ./lib/rubocop/cop/commissioner.rb:100:in `public_send'
      # ./lib/rubocop/cop/commissioner.rb:100:in `block (2 levels) in trigger_responding_cops'
      # ./lib/rubocop/cop/commissioner.rb:160:in `with_cop_error_handling'
      # ./lib/rubocop/cop/commissioner.rb:99:in `block in trigger_responding_cops'

(デフォルトで有効になっている Cop なので、遭遇率はそれなりにあるかもしれません。)

この問題に遭遇した場合の対処方法を列挙します。

1. RuboCop 1.22.3 以上にアップグレードする

これが本手です。問題が解決されている RuboCop 1.22.3 以上にアップグレードしてください。

2. Layout/BlockAlignment cop を無効にする

.rubocop.yml へのワークアラウンドとして、.rubocop.yml に以下を指定してください。

Layout/BlockAlignment:
  Enabled: false

RuboCop へのユーザーフィードバック にあったように、Ruby 2.4 サポートをしたい場合は RuboCop 1.12 までしかアップグレードできません。こういったアップグレードできない事情がある場合のひとつの選択肢になります。

3. モンキーパッチをあてる

例示しないのですが upstream の修正パッチ 同様のモンキーパッチをアプリケーションで当ててください。将来的に実装が変わる可能性があるため、おすすめしないのですが Ruby 2.4 と Ruby 3.1 を同時にサポートして Layout/BlockAlignment cop を有効にしたいケースの選択肢になります。

その他

いちおう他にも CI で問題を解決するという手段もありますが、本手としては RuboCop 1.22.3 以上にアップグレードするになります。また、Ruby 3.1 での each_cons の挙動の変更についてフィードバックを重ねるというのも考えはしたものの、each_cons の挙動変更自体はユーザーにとって自然な方向に向かっていくものだと思うので、付いていける変更具合であれば付いていきたい気持ちから、私の方でフィードバックを重ねることはしないことにしました (もし困っていてお気持ちのある方がいれば、お任せます) 。

なお、この問題について、Rails/OSS パッチ会で osyo さんや amatsuda さんに相談に乗ってもらいました。ありがとうございました。

次回の Rails/OSS パッチ会は 2021年12月21日(火) です。参加は以下の Discord URL までどうぞ。

discord.gg

銀座Rails#39に登壇した

銀座Rails#39に登壇した。

主催者の morimorihoge さんからは、3年目あたりを経ったエンジニアに響くような話を題材にというオファーで頂いていた。

"3年経った" がひとつのキーワードになるものの、参加者にはベテランもいるわけで「3年目をひとつの節目に、3年目以降いつなんどき遭遇するかもしれないポイント」みたいな形で話を構成してみた。当日のスライドは以下。

静的サイトジェネレータ繋がりの yasulab さん、okuramasafumi さんから始まり、自分の方も okuramasafumi さんのライブコーディングセッションから、名付けで悩むシーン (名付けは難しい!) をメンション先とさせてもらったりと、銀座Railsとしても部分部分でリンクした話になっていたのではと思っている。

登壇中に気づいたことだけれど、意識的、無意識的に『リファクタリング』や『Patterns of Enterprise Application Architecture』といった、マーチン・ファウラーの足跡からの引用が多めだったみたいで、マーチン・ファウラーの影響力の大きさを再認識する機会にもなりました。マーチン・ファウラーは偉大。

今回発表の機会をもらって話したものの、なかなか現実は難しく私自身うまくできているところの話や失敗談も入れた一方で、現在進行形であまり手が回っておらず絶賛進行中の話もあったりしていて、本編の内容を一緒に解決していこうという方がいれば、中途/新卒、ビジネスパートナーいずれの立ち位置もエンジニア募集中ですのでお声がけください (直近ですと私や colorbox メンバーたちが関わっているプロジェクトへの参画メンバーを熱烈募集中です) 。

agile.esm.co.jp

楽しかったです。主催者の morimorihoge さんはじめ、発表者、参加者、スポンサーのみなさんありがとうございました。

銀座Rails#39に登壇します

2021年11月19日(金) に開催される「銀座Rails#39」に登壇します。

ginza-rails.connpass.com

銀座Rails#9 以来、30回ぶり2度目のゲスト登壇です。

テーマは、「動いた!」の先へ踏み出す、システムのメンテナビリティ向上の基礎技術です。

主催者の森さんからは、新卒から3年経ったくらいのタスクをこなせるようになったエンジニアに向けて、さらに成長していくためのとっかかりや抑えておくべきスキル、考え方など 「エンジニアとして働けるようになってからのレベルアップ」についての話をしてもらいたいとのオファーを頂いているので、そんな感じの話をする予定です (まだこの世にスライド1枚もないですが) 。

よければ遊びに来てください。

Ruby 3.1 での each_cons と each_slice の (コーナーケース?) 非互換変更

先日、RuboCop の ruby-head CI が落ちていたので、bugs.ruby-lang.org にフィードバックしていたイシューが以下。

bugs.ruby-lang.org

osyo さんがコメントで教えてくれた PR が以下で、each_conseach_slice の戻り値が nil から self に変わっているというものだった。

github.com

以下は破壊的変更が起きる (単純化した) サンプルです。ブロックで条件によって break を使った戻り値を使うか、each_cons としての戻り値を使うか判定するようなロジックが遭遇したケースです。

Ruby 3.0 以下

% ruby -v
ruby 3.0.2p107 (2021-07-07 revision 0db68f0233) [x86_64-darwin19]

% irb
irb(main):001:0> [1, 2, 3].each_cons(2) { |i| break i if true}
=> [1, 2]
irb(main):002:0> [1, 2, 3].each_cons(2) { |i| break i if false}
=> nil

Ruby 3.1

% ruby -v
ruby 3.1.0dev (2021-10-26T00:30:42Z master 7d4c59203f) [x86_64-darwin19]

% irb
irb(main):002:0> [1, 2, 3].each_cons(2) { |i| break i if true}
=> [1, 2]
irb(main):003:0> [1, 2, 3].each_cons(2) { |i| break i if false}
=> [1, 2, 3]

たしかに振る舞いとして戻り値 nil より自然になる気もするが、Ruby 3.1 向けに進んでいる NEWS などで見かけなかったので、影響をどれくらい加味された変更かよく分かっていなかった。

Rails/OSS パッチ会で osyo さんと NEWS にあるといいですねと話していて、osyo さんがパッチを出してくれたのが以下。

github.com

ということで、こういったケースで Ruby 3.1 での非互換変更として影響を受ける可能性があるので、いちおう each_conseach_slice で同様のことを行っていないか見ておくと良いです。

(なお、最初に遭遇した RuboCop の方はそもそものロジックが 🤔 な部分もあったので、Ruby 3.1 互換のロジックに修正済みです。)

Rails/OSSパッチ会の会場をDiscordにした

勤務先で行っている今月のパッチ会で、Discord に Rails/OSS パッチ会のサーバーを立てていました。

これまでやりとりに使っていた Idobata の rails ルームが 11 月中旬に閉鎖されることになったためです。Idobata を情報チャンネルとしていた方は Discord への引越しをお願いします (勤務先の開発者ブログでも後日案内します) 。

Discord の招待 URL は以下です。これからパッチ会に参加してみようという方もぜひどうぞ。

discord.gg

Discord を選んだ理由は、テキストチャンネル、音声チャンネル、画面共有と欲しいものが一式揃っていたことと、yahonda さんから Asakusa.rb での活用実績を聞いて良さそうとしたことです (ありがとうございます!) 。Slack と Discord で悩んでいたところでしたが、あまり悩んでいるのに時間を浪費しても仕方ないのでサッと決めて進めました。

次回の Rails/OSS パッチ会は 11月25日(木) 17:00-19:00 です。

Active Record x Oracle 12c (19c) における `ALL_SYNONYMS` のパフォーマンスリグレッション回避

勤務先で行っている今月のパッチ会で、yahonda さんに相談した Active Record Oracle enhanced adapter の発行しているスロークエリ対策について書き残しておきます。先に結論を書くと処理によっては、処理によってはパフォーマンスが3〜5倍ほど改善されました。

問題点を端的に書くと Oracle 12c で ALL_SYNONYMS が遅くなっています (Oracle 19c も同様) 。以下はそのリグレッションについて言及された記事です。

blog.pythian.com

Rails アプリケーションでシノニムを使っていないようであれば、Active Record Oracle enhanced adapter での以下のクエリを削除するモンキーパッチを作成することで改善できます。もしシノニムを使っていたら、、、分かりません。頑張ってください!

さて、ここからはシノニムを使っていない前提で書きます。

Oracle enhanced adapter 6.1.4

以下の範囲のクエリを削除してください。

github.com

Oracle enhanced adapter 6.0.6

対応は Oracle enhanced adapter 6.1.4 と同様です。

github.com

モンキーパッチコード

具体的には config/initializers/oracle.rb のようなファイルとして以下のようなファイルを、アプリケーションに置くことになります。

module ActiveRecord
  module ConnectionAdapters
    # interface independent methods
    module OracleEnhanced
      class Connection #:nodoc:
        def describe(name)
          name = name.to_s
          if name.include?("@")
            raise ArgumentError "db link is not supported"
          else
            default_owner = @owner
          end
          real_name = OracleEnhanced::Quoting.valid_table_name?(name) ? name.upcase : name
          if real_name.include?(".")
            table_owner, table_name = real_name.split(".")
          else
            table_owner, table_name = default_owner, real_name
          end
          sql = <<~SQL.squish
            SELECT owner, table_name, 'TABLE' name_type
            FROM all_tables
            WHERE owner = '#{table_owner}'
              AND table_name = '#{table_name}'
            UNION ALL
            SELECT owner, view_name table_name, 'VIEW' name_type
            FROM all_views
            WHERE owner = '#{table_owner}'
              AND view_name = '#{table_name}'
          SQL
          if result = _select_one(sql)
            case result["name_type"]
            when "SYNONYM"
              describe("#{result['owner'] && "#{result['owner']}."}#{result['table_name']}")
            else
              [result["owner"], result["table_name"]]
            end
          else
            raise OracleEnhanced::ConnectionException, %Q{"DESC #{name}" failed; does it exist?}
          end
        end
      end
    end
  end
end

(Oracle enhanced adapter 内部のいくつかの主要箇所にある) describe メソッドを使っているあらゆる箇所のパフォーマンスリグレッションを回避できる見立てです。開発環境でのスキーマ読み込みに関する例で言えば、このモンキーパッチで bin/rails db:migrate で実行される bin/rails db:schema:dump が3〜5倍くらい高速になりました。

今後の展望

モンキーパッチからの本対応としては、既存の use_old_oracle_visitor のように use_synonyms といった新規設定オプションを用意して、削除しているクエリをバイパスできるようにするのが一案です。

 ActiveSupport.on_load(:active_record) do
   ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.class_eval do
     # true and false will be stored as 'Y' and 'N'
     self.emulate_booleans_from_strings = true

     # start primary key sequences from 1 (and not 10000) and take just one next value in each session
     self.default_sequence_start_value = "1 NOCACHE INCREMENT BY 1"

     # Use old visitor for Oracle 12c database
     self.use_old_oracle_visitor = true

+    # Use `all_synonyms`, true by default.
+    self.use_synonyms = false
+
     # other settings ...
   end
 end

互換性のためデフォルトでは ALL_SYNONYMS をバイパスしない設定にすると思います。

ただ、以下の理由から私の方で本対応への着手をする場合はまだ先の話になりそうです。

  • この日記を書いている時点の rsim/oracle-enhanced リポジトリは master ブランチが落ちているので、まずそこを直す必要がある
  • https://github.com/yahonda/rails-dev-box/tree/runs_oracleOracle enhanced adapter の開発環境が使えなくなったため、新たに構築が必要で時間の捻出が必要 (先が長そう)
  • 新機能になるため、既存の安定版へのバックポーティングがされないと思われることから、急ぎではない (いまなら Rails 7.0 に間に合うかも)

yahonda さんアドバイスありがとうございました!