サンプルコードでわかる！Ruby 3.2の主な新機能と変更点

## はじめに

Rubyは毎年12月25日にアップデートされます。
Ruby 3.2は2022年12月25日に3.2.0が正式リリースされました。

https://www.ruby-lang.org/ja/news/2022/12/25/ruby-3-2-0-released/

この記事ではRuby 3.2で導入される変更点や新機能について、サンプルコード付きでできるだけわかりやすく紹介していきます。

ただし、すべての変更点を網羅しているわけではありません。個人的に「Railsアプリケーションの開発時に役立ちそうだな」と思った内容をピックアップしています。本記事で紹介していない変更点も多数ありますので、以下のような情報源もぜひチェックしてみてください。

https://www.ruby-lang.org/ja/news/2022/12/25/ruby-3-2-0-released/

https://techlife.cookpad.com/entry/2022/12/26/121950

https://qiita.com/advent-calendar/2022/ruby32

https://github.com/ruby/ruby/blob/v3_2_0/NEWS.md

備考：本記事は以下の記事を「プロを目指す人のためのRuby入門 改訂2版」をお持ちでない方向けに再編集したものです。

https://qiita.com/jnchito/items/d4562c8116bb705517e2

### 動作確認したRubyのバージョン

本記事は以下の環境で実行した結果を記載しています。

```
$ ruby -v
ruby 3.2.0 (2022-12-25 revision a528908271) [arm64-darwin21]
```

### フィードバックお待ちしています
本文の説明内容に間違いや不十分な点があった場合はコメント欄から指摘 or 修正をお願いします🙏

それでは以下が本編です！

## 言語仕様の変更

### 引数名が付かない`*`や`**`を別のメソッドに渡せるようになった

Ruby 3.2では引数名が付かない`*`や`**`を別のメソッドに渡せるようになりました。あまり良いコードではありませんが、この言語仕様を確認できるコード例を以下に示します。

```ruby
# 引数として受け取った配列の数値を合計して返す
def sum_all(*numbers)
  numbers.sum
end

def sum_and_add_10(*)
  # 引数として受け取った配列をそのままsum_allメソッドに
  # 渡してから戻り値に10を足す
  sum_all(*) + 10
end

# (1 + 2 + 3 + 4) + 10 = 20 が返る
sum_and_add_10(1, 2, 3, 4) #=> 20
```

```ruby
# 商品情報を整形する
def format_item(name, price: nil)
  "#{name} (price: #{price})"
end

def decorate_data(name, **)
  # nameは大文字にし、キーワード引数はそのままで
  # format_itemメソッドを呼ぶ
  puts "SALE!! #{format_item(name.upcase, **)}"
end

decorate_data('1959 Les Paul', price: 'ASK')
#=> SALE!! 1959 LES PAUL (price: ASK)
```

Ruby 3.1ではどちらのコードも構文エラーになります。

```
syntax error, unexpected ')'
  sum_all(*) + 10
```

```
syntax error, unexpected ')'
... #{format_item(name.upcase, **)}"
```

https://bugs.ruby-lang.org/issues/18351

### パターンマッチのfindパターンが実験的機能ではなくなった

Ruby 3.2ではパターンマッチのfindパターンが実験的機能ではなくなりました。そのためfindパターンを使っても警告が出ません。

```ruby
# Ruby 3.2ではfindパターンを使っても警告が出ない
case [1, 2, 3]
in [*, 2.. => n, *]
  puts n
end
#=> 2
```

Ruby 3.1までは以下のように警告が出ていました。

```ruby
# Ruby 3.1
case [1, 2, 3]
in [*, 2.. => n, *]
  puts n
end
#=> warning: Find pattern is experimental, and the behavior may change in future versions of Ruby!
#=> 2
```

ちなみに上のコードは「配列の中から2以上である要素を見つけて変数`n`に代入する」という意味のパターンマッチです。
パターンマッチについて詳しく知りたい方は拙著「プロを目指す人のためのRuby入門 改訂2版」の第11章をご覧ください。

https://bugs.ruby-lang.org/issues/18585

## 開発体験の向上

### TypeErrorとArgumentError発生時の表示がわかりやすくなった

Ruby 3.2ではTypeErrorやArgumentErrorが発生した場合に、問題の発生箇所をわかりやすく下線付きで教えてくれるようになりました。

たとえば、以下のサンプルコードを実行すると以下のように表示されます（irb上ではなく、ファイルに保存したコードをrubyコマンドで実行してください）。

```ruby
1 + '10'
```

```
$ ruby sample.rb 
sample.rb:1:in `+': String can't be coerced into Integer (TypeError)

1 + '10'
    ^^^^
	from sample.rb:1:in `<main>'
```

Ruby 3.1以前では以下のように表示されていました。

```
$ ruby sample.rb 
sample.rb:1:in `+': String can't be coerced into Integer (TypeError)
	from sample.rb:1:in `<main>'
```

同じく、以下のサンプルコードをRuby 3.2で実行すると以下のように表示されます。

```ruby
'a' * -1
```

```
$ ruby sample.rb
sample.rb:3:in `*': negative argument (ArgumentError)

'a' * -1
       ^
	from sample.rb:3:in `<main>'
```

Ruby 3.1以前では以下のように表示されていました。

```
$ ruby sample.rb
sample.rb:3:in `*': negative argument (ArgumentError)
	from sample.rb:3:in `<main>'
```

### endキーワードに過不足がある場合のエラー表示が改善された

Ruby 3.2ではendキーワードに過不足によってSyntaxErrorが発生した場合に、問題の発生箇所をわかりやすく教えてくれるsyntax_suggestという機能が入りました。

たとえば、以下のサンプルコードを実行すると以下のように表示されます（irb上ではなく、ファイルに保存したコードをrubyコマンドで実行してください）。

```ruby
users.each { |user|
  send_mail_to(user)
rescue => e
  puts e.full_message
}
```

```
$ ruby sample.rb
sample.rb: --> sample.rb
syntax error, unexpected `rescue', expecting '}'
> 1  users.each { |user|
> 3  rescue => e
> 5  }
sample.rb:3: syntax error, unexpected `rescue', expecting '}' (SyntaxError)
rescue => e
^~~~~~
```

Ruby 3.1以前では以下のように表示されていました。

```
$ ruby sample.rb
sample.rb:3: syntax error, unexpected `rescue', expecting '}'
rescue => e
sample.rb:5: syntax error, unexpected '}', expecting end-of-input
```

たとえば、すごく長いコードでendの過不足があった場合でも、Ruby 3.2ならわかりやすく「ここだよ」と問題の発生箇所を教えてくれます。

<details><summary>構文エラーが発生するとても長いコード（クリックで表示）</summary>

```ruby
require 'date'

RSpec.describe CalcTaxSandbox::Item do
  describe '#price_with_tax' do
    subject { item.price_with_tax(on: date, **options) }
    let(:options) { {} }
    let(:date_20190930) { Date.new(2019, 9, 30) }
    let(:date_20191001) { Date.new(2019, 10, 1) }
    context '食品でも新聞でもない場合' do
      let(:item) { CalcTaxSandbox::Item.new('プロを目指す人のためのRuby入門', 2980) }
      context '税率変更前の場合' do
        let(:date) { date_20190930 }
        it { is_expected.to eq 3218 }
      end
      context '税率変更後の場合' do
        let(:date) { date_20191001 }
        it { is_expected.to eq 3278 }
      end
    end
    context '食品だった場合' do
      context '酒類だった場合' do
        let(:item) { CalcTaxSandbox::Food.new('ビール', 300, alcohol: true) }
        context '税率変更前の場合' do
          let(:date) { date_20190930 }
          it { is_expected.to eq 324 }
        end
        context '税率変更後の場合' do
          let(:date) { date_20191001 }
          it { is_expected.to eq 330 }
        end
      end
      context '酒類でなかった場合' do
        let(:item) { CalcTaxSandbox::Food.new('ハンバーガー', 300) }
        context '税率変更前の場合' do
          let(:date) { date_20190930 }
          it { is_expected.to eq 324 }
        end
        context '税率変更後の場合' do
          let(:date) { date_20191001 }
          it { is_expected.to eq 324 }
          context '外食だった場合' do
            let(:options) { { eating_out: true } }
            it { is_expected.to eq 330 }
          end
        end
      end
    end
    context '新聞の定期購読だった場合' do
      let(:item) { CalcTaxSandbox::NewspaperSubscription.new('Ruby新聞', 5000, per_week: per_week) }
      context '週1回発行の場合' do
        let(:per_week) { 1 }
        context '税率変更前の場合' do
          let(:date) { date_20190930 }
          it { is_expected.to eq 5400 }
        end
        context '税率変更後の場合' do
          let(:date) { date_20191001 }
          it { is_expected.to eq 5500 }
      end
      context '週2回発行の場合' do
        let(:per_week) { 2 }
        context '税率変更前の場合' do
          let(:date) { date_20190930 }
          it { is_expected.to eq 5400 }
        end
        context '税率変更後の場合' do
          let(:date) { date_20191001 }
          it { is_expected.to eq 5400 }
        end
      end
    end
  end
end
```

</details>

実行結果

```
$ ruby sample.rb
sample.rb: --> sample.rb
Unmatched keyword, missing `end' ?
   3  RSpec.describe CalcTaxSandbox::Item do
   4    describe '#price_with_tax' do
  48      context '新聞の定期購読だった場合' do
> 50        context '週1回発行の場合' do
> 56          context '税率変更後の場合' do
> 59        end
  71      end
  72    end
  73  end
sample.rb:73: syntax error, unexpected end-of-input (SyntaxError)
```

開発の効率が上がりそうな便利機能ですね。

https://bugs.ruby-lang.org/issues/18159

### binding.irbからdebug.gemを起動できるようになった

Ruby 3.2では`binding.irb`でプログラムを停止（irbを起動）してから`debug`コマンドを入力すると、debug.gemを起動してデバッグを開始できるようになりました。

![Screen Shot 2022-12-24 at 11.22.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7465/2115296d-fa59-c265-5277-3f1771f1f538.png)

このほかにもirb上で`$ クラス名`や`$ メソッド名`といったコマンドを入力することで、クラスやメソッドのコードを表示できる機能が追加されています。

![Screen Shot 2022-12-24 at 11.28.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7465/eb51da4e-fd41-b7a7-f1ad-b20635c56ea6.png)

その他、Ruby 3.2におけるirbの便利な新機能については以下の記事をご覧ください。

https://qiita.com/k0kubun/items/4600e52a1a245495dfaa

https://st0012.dev/whats-new-in-ruby-3-2-irb

### 環境変数でirbの自動補完の有効・無効を制御できるようになった

Ruby 3.2のirbでは`IRB_USE_AUTOCOMPLETE`という環境変数で自動補完の有効・無効を制御できるようになりました。たとえば次のような形でirbを起動すると、自動補完が無効になります。

```
IRB_USE_AUTOCOMPLETE=false irb
```

これに関連してproduction環境でrails console（rails c）を起動するとデフォルトで自動補完が無効になる変更も採用されています（[参考](https://github.com/rails/rails/pull/46656)）。

参考文献: [Ruby 3\.2 のIRBの新機能 \- Qiita](https://qiita.com/k0kubun/items/4600e52a1a245495dfaa)

## 実行環境

### Webブラウザ上でもRubyが実行できるようになった

Ruby 3.2ではWASI(The WebAssembly System Interface)ベースのWebAssembly(Wasm)へのコンパイルがサポートされたため、webブラウザ上でもRubyが実行できるようになりました。

以下は[TryRuby playground](https://try.ruby-lang.org/playground/)でRuby 3.2を実行した例です。

![Screen Shot 2022-12-24 at 10.53.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7465/91eedc12-7af3-ef65-96fc-c506d9fefd48.png)

TryRuby playgroundがあれば、簡単なサンプルコードであればブラウザ上で動作確認できるようになります。

また、Rubyがブラウザ上で実行できるようになったことから、今後さらにRubyを活用できる場面が増えていくことも期待できます。

RubyのWebAssembly/WASIサポートに関する詳しい情報は下記の公式リリースノートをご覧ください。

https://www.ruby-lang.org/ja/news/2022/12/25/ruby-3-2-0-released/

https://bugs.ruby-lang.org/issues/18462

## パフォーマンス改善

### 正規表現が高速化した

Ruby 3.2では正規表現が高速化し、ReDoS攻撃（正規表現の計算コストが膨大に発生する文字列を送り込んで、サーバーの計算リソースを占有する攻撃）の影響を受けにくくなりました。

たとえば `"_a_____________________" =~ /(_+|\w+)*a/` というコードはRuby 3.1までは結果が得られるまで膨大な時間がかかりましたが、Ruby 3.2では一瞬で結果が返ります。

![LO3ptRSQfQ.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7465/93a9b591-5e24-179b-b99d-38e1ce523800.gif)

公式リリースノートによれば、この高速化によって90%程度の正規表現が線形時間でマッチ判定できるようになるとのことです（一部の正規表現では高速化が有効に働かないケースがあることに注意してください）。

> このアルゴリズムの改善で、ほとんどの（我々の実験では90%程度の）正規表現が線形時間でマッチ判定できるようになります。
>
> [Ruby 3\.2\.0 リリース](https://www.ruby-lang.org/ja/news/2022/12/25/ruby-3-2-0-released/)

なお、高速化されるかどうかの判定は`Regexp.linear_time?`メソッドで判定できます（`true`なら高速化される）。

```ruby
Regexp.linear_time?(/(_+|\w+)*a/)   #=> true
Regexp.linear_time?(/^a*b?a*()\1$/) #=> false
```

この改善に関する技術的な説明は以下の記事を参照してください。

https://techlife.cookpad.com/entry/2022/12/12/162023

https://bugs.ruby-lang.org/issues/19104

### 正規表現のタイムアウト時間を指定できるようになった

上記の高速化によりReDoS攻撃の影響は受けにくくなったものの、正規表現と入力文字列の組み合わせによっては非常に時間のかかるケースがあるかもしれません。その対策として、Ruby 3.2では正規表現のタイムアウト秒数を指定できるようになりました。

```ruby
# デフォルトのタイムアウト秒数はnil（無制限）
Regexp.timeout #=> nil

# タイムアウト秒数を1秒に設定
Regexp.timeout = 1.0

# Ruby 3.2の高速化でも対応できない重たい正規表現を実行する
/^a*b?a*()\1$/ =~ "a" * 50000 + "x"

# 1秒後にタイムアウトして例外が発生する
#=> regexp match timeout (Regexp::TimeoutError)
```

`Regexp.timeout`はグローバルな設定です。一部の正規表現だけにタイムアウト秒数を指定したい場合は`Regexp.new`に`timeout`キーワードを指定します。

```ruby
# この正規表現オブジェクトにだけ、タイムアウト秒数を指定する
re = Regexp.new('^a*b?a*()\1$', timeout: 1.0)
re =~ "a" * 50000 + "x"

# 1秒後にタイムアウトして例外が発生する
#=> regexp match timeout (Regexp::TimeoutError)

# 読み取りはできるが、上書きはできない
re.timeout #=> 1.0
re.timeout = nil
#=> undefined method `timeout=' for /^a*b?a*()\1$/:Regexp (NoMethodError)
```

https://bugs.ruby-lang.org/issues/17837

### YJITが実験的機能でなくなった

Ruby 3.1で実験的に導入されたYJITが実験的機能ではなくなりました。また、YJITはRustを必要とするため、YJITを使う場合はRubyをビルドする前にRust 1.58.0以上をインストールしておく必要があります。

`--yjit`オプションを付けたときに以下のようなメッセージが出る場合は、YJITが有効化されていません。
Rustをインストールしてから再度Rubyをビルド（インストール）してください。

```
$ ruby --yjit sample.rb
ruby: warning: Ruby was built without YJIT support. You may need to install rustc to build Ruby with YJIT.
```

僕の場合、こんな感じでRustをセットアップ＆Ruby 3.2.0をインストールしました（macOS環境です）。

```
$ brew install rustup-init
$ rustup-init

# rustc 1.58.0以上がインストールされたことを確認
$ rustup --version
rustup 1.25.1 (2022-07-12)
info: This is the version for the rustup toolchain manager, not the rustc compiler.
info: The currently active `rustc` version is `rustc 1.66.0 (69f9c33d7 2022-12-12)`

$ rbenv install 3.2.0
```

公式リリースノートによると、YJITを有効にした場合、ActiveRecordが約1.6倍速くなるようです（一番左のグラフがActiveRecordで、オレンジのバーがYJIT有効時のパフォーマンス）。

![X9ulfac.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7465/f15deefa-e101-d76f-4468-0718b9d14fe9.png)
Image: https://www.ruby-lang.org/ja/news/2022/12/25/ruby-3-2-0-released/

[こちらのブログ記事](https://www.solnic.dev/p/benchmarking-ruby-32-with-yjit)でもdry-structというライブラリを使ったパフォーマンステストでYJITなしで実行したときに447745.3 i/sだったパフォーマンスが、YJITを有効にすると 1102846.4 i/s （約2.5倍!!）に上がったと報告されています。

![Screen Shot 2022-12-26 at 8.54.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7465/cb33cd3b-5439-3525-3f80-8d45591c2721.png)
Image: https://www.solnic.dev/p/benchmarking-ruby-32-with-yjit

### Bundlerが速くなった

Ruby 3.2では以下のような改善によってBundlerが速くなりました。

- 依存解決ライブラリを[Molinillo](https://github.com/CocoaPods/Molinillo)から[PubGrub](https://github.com/jhawthorn/pub_grub)に変更した
- gitリポジトリをcloneする際に必要最小限の変更履歴だけを取得するようになった

https://github.com/rubygems/rubygems/pull/4475

### `ERB::Util.html_escape`と`ERB::Util.url_encode`が速くなった

Ruby 3.2では`ERB::Util.html_escape`が`CGI.escapeHTML`よりも速くなりました。また、`ERB::Util.url_encode`は`CGI.escapeURIComponent`よりも速くなりました。

```ruby
require 'cgi'
require 'erb'

# Ruby 3.2なら速い
ERB::Util.html_escape("is a > 0 & a < 10?")
#=> "is a &gt; 0 &amp; a &lt; 10?"

# 同じ結果になるが遅い
CGI.escapeHTML("is a > 0 & a < 10?")
#=> "is a &gt; 0 &amp; a &lt; 10?"

# Ruby 3.2なら速い
ERB::Util.url_encode("Programming Ruby:  The Pragmatic Programmer's Guide")
#=> "Programming%20Ruby%3A%20%20The%20Pragmatic%20Programmer%27s%20Guide"

# 同じ結果になるが遅い
CGI.escapeURIComponent("Programming Ruby:  The Pragmatic Programmer's Guide")
#=> "Programming%20Ruby%3A%20%20The%20Pragmatic%20Programmer%27s%20Guide"
```

### このほかにも様々なパフォーマンス改善が行われた

Ruby 3.2では様々なパフォーマンス改善が行われています。
技術的に難しい内容になるためここでは詳しく説明しませんが、気になる方はNEWS.mdの"Implementation improvements"欄や"JIT"の欄を参照してください。

https://github.com/ruby/ruby/blob/v3_2_0/NEWS.md

## コアクラス（組み込みライブラリ）のアップデート

### 組み込みライブラリにDataクラスが新たに導入された

Ruby 3.2では組み込みライブラリとしてDataクラスが新たに追加されました。Dataクラスを使うとイミュータブルなクラスを簡単に定義できます。

```ruby
# Dataクラスを利用してxとyというプロパティを持つPointクラスを定義
Point = Data.define(:x, :y)

# Pointクラスのインスタンスを作成
point = Point.new(x: 10, y: 20)

# インスタンスからプロパティを読み取る
point.x #=> 10
point.y #=> 20

# プロパティは代入不可（つまりイミュータブル）
point.x = 30
#=> undefined method `x=' for #<data Point x=10, y=20> (NoMethodError)

# withメソッドを使って別のプロパティ値を持つ新しいインスタンスを作ることは可能
new_point = point.with(x: 30)
#=> #<data Point x=30, y=20>
point.object_id     #=> 66420
new_point.object_id #=> 82600
```

Dataクラスを使って定義したクラスは自動的に`==`メソッドも実装されます。

```ruby
point_10_20 = Point.new(x: 10, y: 20)
point_a = Point.new(x: 10, y: 20)
point_b = Point.new(x: 15, y: 20)

point_10_20 == point_a #=> true
point_10_20 == point_b #=> false
```

`to_h`メソッドでプロパティをハッシュに変換することもできます。

```ruby
point = Point.new(x: 10, y: 20)
point.to_h #=> {:x=>10, :y=>20}
```

独自のメソッドを定義することもできます。

```ruby
Point = Data.define(:x, :y) do
  def swap
    Point.new(x: y, y: x)
  end
end

point = Point.new(10, 20)
point.swap #=> #<data Point x=20, y=10>
```

さらに、Dataクラスを使って定義したクラスには`deconstruct`メソッドと`deconstruct_keys`メソッドが実装されているので、パターンマッチで使うこともできます。

```ruby
point = Point.new(x: 10, y: 20)

case point
in [1, 2]
  # ここはマッチしない
in [10, 20]
  # ここにマッチする
  'matched'
end
#=> "matched"

case point
in {x: 1, y: 2}
  # ここはマッチしない
in {x: 10, y: 20}
  # ここにマッチする
  'matched'
end
#=> "matched"
```

### DataクラスとStructクラスの違い

Dataクラスとよく似た組み込みライブラリにStructクラスがあります。以下は上で作成したPointクラスをStructで作成する例です。
Dataクラスは`define`メソッドを使ってPointクラスを定義したのに対し、Structクラスは`new`メソッドで新しいクラスを定義します。

```ruby
# Structクラスを利用してxとyというプロパティを持つPointクラスを定義
# ただし、Ruby 3.2ではkeyword_initオプションは省略可（後述）
Point = Struct.new(:x, :y, keyword_init: true)

# Pointクラスのインスタンスを作成
point = Point.new(x: 10, y: 20)

# インスタンスからプロパティを読み取る
point.x #=> 10
point.y #=> 20
```

また、Dataクラスから定義したクラスはイミュータブルですが、Structクラスから定義した場合はミュータブルになります。

```ruby
Point = Struct.new(:x, :y, keyword_init: true)
point = Point.new(x: 10, y: 20)

# Structクラスから作ったクラスはミュータブル（プロパティの変更ができる）
point.x = 30

point.x #=> 30
```

このため、イミュータブルなクラスを定義したい場合はDataクラスを使った方が便利です。

### 注意：既存のコードにDataクラスがあると名前が衝突するかも？

Dataクラスは組み込みライブラリです。つまりrequireしなくても使えるクラスです。
もし既存のコードでDataという名前のクラスやモジュールを定義していると名前が衝突してプログラムが動かなくなる可能性があるので注意してください。

```ruby
# 以下のコードはRuby 3.1までは動作するが、Ruby 3.2ではクラスの名前が
# 衝突するため動作しない
class Data
  def initialize(foo, bar)
    @foo = foo
    @bar = bar
  end
end
data = Data.new(123, 456)
```

```
# 実行結果
$ ruby my_data.rb
my_data.rb:7:in `<main>': undefined method `new' for Data:Class (NoMethodError)

data = Data.new(123, 456)
           ^^^^
```

もし衝突してしまった場合は既存のDataクラスを別の名前（MyDataなど）に変更してください。

（追記）
[プロと読み解く Ruby 3\.2 NEWS](https://techlife.cookpad.com/entry/2022/12/26/121950)にDataという名前になった由来が書かれていました↓

> Rubyに新しいクラスを導入する場合、問題になるのが名前です。下手に定数を増やしてしまうと、その名前を使っているgemがあったとき、非互換となってしまう可能性があります。にもかかわらずDataという思い切った名前になったのは、かつてRuby自身がDataというトップレベルの定数を定義していたからです。
>
> これは拡張ライブラリ作者が内部実装に使うために用意されていたクラスだったのですが、なぜか誰も使いませんでした。そして長らく非推奨となっていて、Ruby 3.0くらいでついに削除されました。なので、2年ほど経ってはいますが、今ならまだ他のライブラリとの衝突の可能性が低いのでは？ということで、この名前になりました。

https://bugs.ruby-lang.org/issues/16122

### Struct.newで明示的に`keyword_init: false`を指定しない限り、キーワード引数による初期化が有効になった

Ruby 3.1の時点で予告されていたとおり（[参考](https://qiita.com/jnchito/items/bcd9b7f59bf4b30ea5b3#keyword_init%E3%82%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%92%E6%98%8E%E7%A4%BA%E7%9A%84%E3%81%AB%E6%8C%87%E5%AE%9A%E3%81%9B%E3%81%9A%E3%81%AB%E7%94%9F%E6%88%90%E3%81%95%E3%82%8C%E3%81%9Fstruct%E3%81%AB%E5%AF%BE%E3%81%97%E3%81%A6%E3%82%AD%E3%83%BC%E3%83%AF%E3%83%BC%E3%83%89%E5%BC%95%E6%95%B0%E3%81%A0%E3%81%91%E3%81%A7new%E3%81%97%E3%82%88%E3%81%86%E3%81%A8%E3%81%99%E3%82%8B%E3%81%A8%E8%AD%A6%E5%91%8A%E3%81%8C%E5%87%BA%E3%82%8B%E3%82%88%E3%81%86%E3%81%AB%E3%81%AA%E3%81%A3%E3%81%9F)）、Ruby 3.2ではStructクラスを使って生成したクラスは、明示的に`keyword_init: false`を指定しない限り、キーワード引数による初期化が有効になりました。

```ruby
# Ruby 3.2の場合
Foo = Struct.new(:id, :name)
foo = Foo.new(id: 1, name: 'foo')
#=> #<struct Foo id=1, name="foo">

# Ruby 3.1の場合
Foo = Struct.new(:id, :name)
foo = Foo.new(id: 1, name: 'foo')
#=> #<struct Foo id={:id=>1, :name=>"foo"}, name=nil>
```

Ruby 3.1と同じ挙動にしたい場合は明示的に`keyword_init: false`（`nil`ではなく`false`）を指定する必要があります。

```ruby
# Ruby 3.2でもkeyword_init: falseを指定すればRuby 3.1と同じ挙動になる
Foo = Struct.new(:id, :name, keyword_init: false)
foo = Foo.new(id: 1, name: 'foo')
#=> #<struct Foo id={:id=>1, :name=>"foo"}, name=nil>
```

ちなみに、`keyword_init?`メソッドの戻り値はRuby 3.1も3.2も、どちらも`nil`です。Ruby 3.2でデフォルトで`true`がセットされるようになったわけではない点に注意してください。

```ruby
Foo = Struct.new(:id, :name)

# Ruby 3.1も3.2も戻り値はnil
Foo.keyword_init? #=> nil
```

（追記）
[プロと読み解く Ruby 3\.2 NEWS](https://techlife.cookpad.com/entry/2022/12/26/121950)に`keyword_init`に設定する値と挙動の関係が詳しく書かれていたので追記します。

> 少し細かいことを言うと、`keyword_init`はtrue/false/nilでそれぞれ意味が微妙に違います。
> 
> - `keyword_init: true` → `Point.new(x: 1, y: 2)`で初期化できる、`Point.new(1, 2)`はエラー
> - `keyword_init: nil` → `Point.new(x: 1, y: 2)`でも`Point.new(1, 2)`でも初期化できる
> - `keyword_init: false` → `Point.new(1, 2)`で初期化できる、`Point.new(x: 1, y: 2)`は要注意の挙動（次の動作例を参照）
> 
> ```ruby
> # Ruby 3.1 の挙動：Point.new({x: 1, y: 2}) と同じ扱い
> Point.new(x: 1, y: 2) #=> #<struct A x={:y=>2, :x=>1}, y=nil>
> ```
> 
> 今回の変更は、`keyword_init`のデフォルトがfalseからnilに変わったということになります。

https://bugs.ruby-lang.org/issues/16806

### Setクラスが組み込みクラスになった

Ruby 3.2ではSetクラスが組み込みクラスになりました。そのため、以下のサンプルコードでは`require 'set'`を書く必要がありません。

```ruby
# Ruby 3.2ではSetクラスを使うのにrequireは不要
# require 'set'

a = Set[1, 2, 3]
b = Set[3, 4, 5]
a | b #=> #<Set: {1, 2, 3, 4, 5}>
a - b #=> #<Set: {1, 2}>
a & b #=> #<Set: {3}>
```

ちなみに、Ruby 3.1以前では基本的に`require 'set'`が必要になりますが、irbでは内部的にsetライブラリがrequireされているため、例外的に`require 'set'`なしでSetクラスが使えます。

```
$ ruby -v
ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [arm64-darwin21]

# irbで実行する場合はRuby 3.1以前でも例外的にrequireなしでSetクラスが使える
$ irb
irb(main):001:0> a = Set[1, 2, 3]
=> #<Set: {1, 2, 3}>
```

https://bugs.ruby-lang.org/issues/16989

### 商を切り上げしてくれる`Integer#ceildiv`メソッドが追加された

Ruby 3.2では商を切り上げしてくれる`Integer#ceildiv`メソッドが追加されました。

```ruby
5 / 4        #=> 1
5.0 / 4      #=> 1.25
# 商の端数を切り上げる
5.ceildiv(4) #=> 2
# 割り切れる場合は商がそのまま返る
6.ceildiv(2) #=> 3

# 商が負になる場合
-5 / 4        #=> -2
-5.0 / 4      #=> -1.25
-5.ceildiv(4) #=> -1
-6.ceildiv(2) #=> -3
```

Ruby 3.1以前では以下のように書く必要がありました。

```ruby
(5.0 / 4).ceil  #=> 2
(-5.0 / 4).ceil #=> -1
```

https://bugs.ruby-lang.org/issues/18809

### Regexp.newのオプションを文字列でも指定できるようになった

```ruby
# 大文字小文字を無視する正規表現オブジェクトを生成する
/hello/i
Regexp.new('hello', Regexp::IGNORECASE)
# Ruby 3.2からは文字列"i"でも指定可能になった
Regexp.new('hello', 'i') #=> /hello/i

# 大文字小文字を無視し、ドット(.)を改行にマッチさせる
# 正規表現オブジェクトを生成する
/hello/im
Regexp.new('hello', Regexp::IGNORECASE | Regexp::MULTILINE)
# Ruby 3.2からは文字列"im"や"mi"でも指定可能になった
Regexp.new('hello', 'im') #=> /hello/mi

# i, m, x以外の文字列は無効なオプションなのでエラー
Regexp.new('hello', 'a')
#=> unknown regexp option: a (ArgumentError)
```

ちなみにRuby 3.1以前では第2引数に文字列を渡すと、文字列の内容にかかわらず常に大文字小文字を無視する正規表現オブジェクトが生成されていました。これは「第2引数が真（`nil`と`false`以外）なら大文字小文字を無視する」という仕様になっていたためです。

```ruby
# Ruby 3.1以前の場合、第2引数が真なら常に大文字小文字を無視
Regexp.new('hello', 'i')  #=> /hello/i
Regexp.new('hello', 'im') #=> /hello/i
Regexp.new('hello', 'a')  #=> /hello/i
```

```ruby
# true, false, nilを渡した場合はRuby 3.1も3.2も挙動は同じ
Regexp.new('hello', true)  #=> /hello/i
Regexp.new('hello', false) #=> /hello/
Regexp.new('hello', nil)   #=> /hello/
```

Ruby 3.2で挙動が変わるのは第2引数が文字列の場合のみです。たとえばシンボルを渡した場合はシンボルの値にかかわらず、Ruby 3.1以前と同様に「真なので大文字小文字を無視」とみなされます。

```ruby
# シンボルを渡したときはRuby 3.1も3.2も挙動は同じ
Regexp.new('hello', :i)  #=> /hello/i
Regexp.new('hello', :im) #=> /hello/i
Regexp.new('hello', :a)  #=> /hello/i
```

https://bugs.ruby-lang.org/issues/18788

### MatchDataオブジェクトがパターンマッチに対応した

Ruby 3.2ではMatchDataオブジェクトがパターンマッチで使えるようになりました。

```ruby
m = "Ruby 3.2.0".match(/(\d+)\.(\d+)\.(\d+)/)
#=> #<MatchData "3.2.0" 1:"3" 2:"2" 3:"0">

# キャプチャされた"3", "2", "0"はarrayパターンとして利用できる
case m
in [major, minor, teeny]
  puts "major:#{major}, minor:#{minor}, teeny:#{teeny}"
end
#=> major:3, minor:2, teeny:0
```

名前付きキャプチャの場合はhashパターンとして扱うこともできます。

```ruby
m = "Ruby 3.2.0".match(/(?<major>\d+)\.(?<minor>\d+)\.(?<teeny>\d+)/)
#=> #<MatchData "3.2.0" major:"3" minor:"2" teeny:"0">

# 名前付きキャプチャをhashパターンで利用する
case m
in {major:, minor:, teeny:}
  puts "major:#{major}, minor:#{minor}, teeny:#{teeny}"
end
#=> major:3, minor:2, teeny:0

# arrayパターンで利用することも可能
case m
in [major, minor, teeny]
  puts "major:#{major}, minor:#{minor}, teeny:#{teeny}"
end
#=> major:3, minor:2, teeny:0
```

Ruby 3.1以前では`deconstruct`メソッドや`deconstruct_keys`メソッドが定義されていないのでエラーになります。

```ruby
case m
in [major, minor, teeny]
  # ...
end
#=> #<MatchData "3.2.0" 1:"3" 2:"2" 3:"0">: #<MatchData "3.2.0" 1:"3" 2:"2" 3:"0"> does not respond to #deconstruct (NoMatchingPatternError)
```

https://bugs.ruby-lang.org/issues/18821

### TimeオブジェクトとDateオブジェクトがパターンマッチに対応した

Ruby 3.2ではTimeオブジェクトがパターンマッチ（hashパターン）で使えるようになりました。

```ruby
time = Time.now
#=> 2022-12-24 19:06:40.436435 +0900

case time
in {month: 1, day: 1}
  puts "元旦です"
in {month: 12, day: 24}
  puts "クリスマスイブです"
in {month: 12, day: 25}
  puts "クリスマスです"
else
  puts "その他です"
end
#=> クリスマスイブです

case time
in {hour: 12, min: 0}
  puts "正午です"
in {hour: 12..}
  puts "午後です"
else
  puts "午前です"
end
#=> 午後です
```

hashパターンで指定可能なtimeオブジェクトのキーについては`deconstruct_keys`メソッドの戻り値を参考にしてください。

```ruby
time = Time.now
#=> 2022-12-24 19:06:40.436435 +0900

pp time.deconstruct_keys(nil)
#=> {:year=>2022,
#    :month=>12,
#    :day=>24,
#    :yday=>358,
#    :wday=>6,
#    :hour=>19,
#    :min=>6,
#    :sec=>40,
#    :subsec=>(87287/200000),
#    :dst=>false,
#    :zone=>"JST"}
```

なお、`deconstruct`メソッドは実装されていないため、arrayパターンで使うことはできません（年月日の並び順は各国の文化によって異なるから、というのがその理由のようです - [参考](https://bugs.ruby-lang.org/issues/19071)）。

```ruby
# Timeオブジェクトをarrayパターンで使おうとするとエラーになる
case time
in [2022, 12, 24]
  # ...
end
#=> 2022-12-24 19:06:40.436435 +0900: 2022-12-24 19:06:40.436435 +0900 does not respond to #deconstruct (NoMatchingPatternError)
```

Timeオブジェクトと同様、Dateオブジェクトもhashパターンに対応しています。

```ruby
require 'date'

date = Date.today
#=> #<Date: 2022-12-24 ((2459938j,0s,0n),+0s,2299161j)>

# Dateオブジェクトをhashパターンで使う
case date
in {month: 1, day: 1}
  puts "元旦です"
in {month: 12, day: 24}
  puts "クリスマスイブです"
in {month: 12, day: 25}
  puts "クリスマスです"
else
  puts "その他です"
end
#=> クリスマスイブです

# hashパターンで使えるキーは以下の通り
date.deconstruct_keys(nil)
#=> {:year=>2022, :month=>12, :day=>24, :yday=>358, :wday=>6}

# Timeオブジェクトと同様、arrayパターンでは使えない
case date
in [2022, 12, 24]
  # ...
end
#=> #<Date: 2022-12-24 ((2459938j,0s,0n),+0s,2299161j)>: #<Date: 2022-12-24 ((2459938j,0s,0n),+0s,2299161j)> does not respond to #deconstruct (NoMatchingPatternError)
```

DateTimeクラスもhashパターンで使えるようになりましたが、非推奨クラスであるためTimeクラスを使った方が良いでしょう。

```ruby
require 'date'

# 注：DateTimeクラスは非推奨なので、なるべくTimeクラスを使った方が良い
date_time = DateTime.now
#=> #<DateTime: 2022-12-25T12:06:53+09:00 ((2459939j,11213s,367401000n),+32400s,2299161j)>

case date_time
in {month: 1, day: 1}
  puts "元旦です"
in {month: 12, day: 24}
  puts "クリスマスイブです"
in {month: 12, day: 25}
  puts "クリスマスです"
else
  puts "その他です"
end
#=> クリスマスです

pp date_time.deconstruct_keys(nil)
#=> {:year=>2022,
#    :month=>12,
#    :day=>25,
#    :yday=>359,
#    :wday=>0,
#    :hour=>12,
#    :min=>6,
#    :sec=>53,
#    :sec_fraction=>(367401/1000000),
#    :zone=>"+09:00"}
```

https://bugs.ruby-lang.org/issues/19071

### Time.newが日時文字列をパースできるようになった

Ruby 3.2では`Time#inspect`が返す文字列の形式や、[ISO-8601](https://ja.wikipedia.org/wiki/ISO_8601)風の文字列を`Time.new`に渡すと、その文字列をパースしてTimeクラスのインスタンスを作れるようになりました。

```ruby
t = Time.now
str = t.inspect
#=> "2022-12-25 09:15:34.810474 +0900"

# inspectで得た文字列をTime.newに渡してインスタンスを作成する
Time.new(str)
#=> 2022-12-25 09:15:34.810474 +0900

# https://github.com/ruby/ruby/pull/4825 から引用したその他の入力例
Time.new("2020-12-24T15:56:17Z")
Time.new("2020-12-25 00:56:17 +09:00")
Time.new("2020-12-25 00:57:47 +09:01:30")
Time.new("2020-12-25 00:56:17 +0900")
Time.new("2020-12-25 00:57:47 +090130")
Time.new("2020-12-25T00:56:17+09:00")

# 不正な文字列を渡すとエラー
Time.new("2020-12-25 00:56 +09:00")
#=> missing sec part: 00:56  (ArgumentError)
```

Ruby 3.1までは`Time.parse`や`Time.iso8601`メソッドを使って日時文字列のパースができましたが、`require 'time'`が必要でした。

```ruby
# Time.parseやTime.iso8601を使うにはtimeライブラリのrequireが必須
require 'time'

str = "2022-12-25 09:15:34.810474 +0900"
Time.parse(str)
#=> 2022-12-25 09:15:34.810474 +0900

Time.iso8601("2008-08-31T12:34:56+09:00")
#=> 2008-08-31 12:34:56 +0900
```

[こちらのissue](https://bugs.ruby-lang.org/issues/18033)によると、requireが不要になる以外にも以下のようなメリットがあるようです。

- `Time.iso8601`は`Time#inspect`が返す文字列をパースできない、という問題を解消できる
- `Time.parse`は予期しない結果になることがよくある、という問題を解消できる
- `Time.new`は`Time.iso8601`より約1.9倍速い

https://bugs.ruby-lang.org/issues/18033

### File.exists?とDir.exists?が削除された

Ruby 3.1までは`File.exist?`と`File.exists?`、`Dir.exist?`と`Dir.exists?`という、同じ役割を持つメソッドが2つずつありました。

後者（最後にsが付く方）はRuby 2.1から非推奨メソッドだったのですが、このメソッドがRuby 3.2で（ようやく）削除されました。

```ruby
# Ruby 3.2では削除された
File.exists?
#=> undefined method `exists?' for File:Class (NoMethodError)

# Ruby 3.2では削除された
Dir.exists?
#=> undefined method `exists?' for Dir:Class (NoMethodError)
```

ちなみに、Ruby 3.1以前で`File.exists?`や`Dir.exists?`を使っていても`RUBYOPT=-W:deprecated`という環境変数を設定しておかないと、警告メッセージが出ない点に注意してください。既存のアプリケーションをRuby 3.2にアップグレードする前は必ずこの環境変数を設定し、それから非推奨警告が出ていないか確認するようにしましょう。

```
# Ruby 3.1でFile.exists?を呼び出す例

# 普通にirbを起動するだけでは警告メッセージは出ない
$ irb
irb(main):001:0> File.exists? 'hoge.txt'
=> false
irb(main):002:0> exit

# 警告メッセージが出るのは環境変数を設定したときだけ！
$ RUBYOPT=-W:deprecated irb
irb(main):001:0> File.exists? 'hoge.txt'
(irb):1: warning: File.exists? is deprecated; use File.exist? instead
=> false
```

余談ですが、「そろそろこのメソッド消しませんか？」というissueを出したのは僕ですw

https://bugs.ruby-lang.org/issues/17391

## 標準ライブラリ

### URI向けにスペースと`%20`をエンコード/デコードする`CGI.escapeURIComponent`/`CGI.unescapeURIComponent`が追加された

Ruby 3.2ではURI向けに半角スペースと`%20`をエンコード/デコードする`CGI.escapeURIComponent`/`CGI.unescapeURIComponent`が追加されました。

```ruby
require 'cgi'

# URI用にエンコードする（半角スペースもパーセントエンコーディングされる点に注目）
CGI.escapeURIComponent("'Stop!' said Fred")
#=> "%27Stop%21%27%20said%20Fred"

# URI用の文字列（半角スペースが%20になっているもの）をデコードする
CGI.unescapeURIComponent("%27Stop%21%27%20said%20Fred")
#=> "'Stop!' said Fred"
```

ちなみに既存のメソッドでは`URI.encode_www_form_component`を使うとほぼ同じようにエンコードされますが、スペースが`%20`ではなく`+`に変換される点が異なります。

```ruby 
require 'uri'

# 半角スペースは%20ではなく+に変換される
URI.encode_www_form_component("'Stop!' said Fred")
#=> "%27Stop%21%27+said+Fred"
```

URIに含まれるスペースは`+`ではなく`%20`に変換するのがRFC的には正しいようです（[RFC 3986](https://www.asahi-net.or.jp/~ax2s-kmtn/ref/uric.html)を参照）。

なお、`URI.encode_www_form_component`についてはこちらの記事でも紹介しています。

https://qiita.com/jnchito/items/dc9285884b2349833fad

https://bugs.ruby-lang.org/issues/18822

## まとめ

というわけで、この記事ではRuby 3.2の変更点と新機能をいろいろと紹介してみました。
冒頭にも書いたとおり、本記事で紹介していない変更点もまだまだたくさんあるので、以下の情報源もぜひチェックしてみてください。

https://www.ruby-lang.org/ja/news/2022/12/25/ruby-3-2-0-released/

https://techlife.cookpad.com/entry/2022/12/26/121950

https://qiita.com/advent-calendar/2022/ruby32

https://github.com/ruby/ruby/blob/v3_2_0/NEWS.md

Ruby 3.2は派手な言語仕様の変更はありませんが、正規表現が高速化したり、YJITが実験的機能でなくなったりと、業務レベルの運用を考えるとすごく実用的かつ魅力的な改善がたくさん導入されたように思います。個人的にはYJITを実際のRailsアプリケーションで早く試してみたいです。

今年も2022年のクリスマスにRuby 3.2を届けてくれたMatzさんやコミッタのみなさんに感謝したいと思います。どうもありがとうございました！
みなさんもぜひRuby 3.2の新機能を試してみてください😉

### PR: 拙著「プロを目指す人のためのRuby入門」の改訂2版が発売されました🍒

2021年12月2日に拙著「プロを目指す人のためのRuby入門」（通称・チェリー本）の改訂2版が発売されました。第1版の対象バージョンはRuby 2.4でしたが、改訂2版ではRuby 3.0をフルサポートしています。特に、Ruby 2.7から導入されたパターンマッチについては、新しく章を追加して基本から発展的な内容まで詳細に説明しています。

https://gihyo.jp/book/2021/978-4-297-12437-3

その他、改訂2版の変更点については以下のブログ記事で詳しく説明しています。

https://blog.jnito.com/entry/2021/12/02/072804

前述の通り、本書の対象バージョンはRuby 3.0ですが、Ruby 3.1以降で発生する記述内容との差異は、それぞれ以下の記事にまとめてあります。なので、多少バージョンが古くても安心して読んでいただけます😊

https://qiita.com/jnchito/items/bbabb21a1ce3f4799b52

https://qiita.com/jnchito/items/d4562c8116bb705517e2