91
45

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-12-26

はじめに

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

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

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

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

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

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

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

フィードバックお待ちしています

本文の説明内容に間違いや不十分な点があった場合はコメント欄から指摘 or 修正をお願いします🙏

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

言語仕様の変更

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

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

# 引数として受け取った配列の数値を合計して返す
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
# 商品情報を整形する
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, **)}"

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

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

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

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

# 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章をご覧ください。

開発体験の向上

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

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

たとえば、以下のサンプルコードを実行すると以下のように表示されます(irb上ではなく、ファイルに保存したコードを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で実行すると以下のように表示されます。

'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コマンドで実行してください)。

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ならわかりやすく「ここだよ」と問題の発生箇所を教えてくれます。

構文エラーが発生するとても長いコード(クリックで表示)
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

実行結果

$ 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)

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

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

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

Screen Shot 2022-12-24 at 11.22.12.png

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

Screen Shot 2022-12-24 at 11.28.01.png

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

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

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

IRB_USE_AUTOCOMPLETE=false irb

これに関連してproduction環境でrails console(rails c)を起動するとデフォルトで自動補完が無効になる変更も採用されています(参考)。

参考文献: Ruby 3.2 のIRBの新機能 - Qiita

実行環境

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

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

以下はTryRuby playgroundでRuby 3.2を実行した例です。

Screen Shot 2022-12-24 at 10.53.29.png

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

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

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

パフォーマンス改善

正規表現が高速化した

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

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

LO3ptRSQfQ.gif

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

このアルゴリズムの改善で、ほとんどの(我々の実験では90%程度の)正規表現が線形時間でマッチ判定できるようになります。

Ruby 3.2.0 リリース

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

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

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

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

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

# デフォルトのタイムアウト秒数は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.newtimeoutキーワードを指定します。

# この正規表現オブジェクトにだけ、タイムアウト秒数を指定する
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)

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
Image: https://www.ruby-lang.org/ja/news/2022/12/25/ruby-3-2-0-released/

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

Screen Shot 2022-12-26 at 8.54.14.png
Image: https://www.solnic.dev/p/benchmarking-ruby-32-with-yjit

Bundlerが速くなった

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

  • 依存解決ライブラリをMolinilloからPubGrubに変更した
  • gitリポジトリをcloneする際に必要最小限の変更履歴だけを取得するようになった

ERB::Util.html_escapeERB::Util.url_encodeが速くなった

Ruby 3.2ではERB::Util.html_escapeCGI.escapeHTMLよりも速くなりました。また、ERB::Util.url_encodeCGI.escapeURIComponentよりも速くなりました。

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"の欄を参照してください。

コアクラス(組み込みライブラリ)のアップデート

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

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

# 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クラスを使って定義したクラスは自動的に==メソッドも実装されます。

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メソッドでプロパティをハッシュに変換することもできます。

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

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

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メソッドが実装されているので、パターンマッチで使うこともできます。

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メソッドで新しいクラスを定義します。

# 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クラスから定義した場合はミュータブルになります。

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 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にDataという名前になった由来が書かれていました↓

Rubyに新しいクラスを導入する場合、問題になるのが名前です。下手に定数を増やしてしまうと、その名前を使っているgemがあったとき、非互換となってしまう可能性があります。にもかかわらずDataという思い切った名前になったのは、かつてRuby自身がDataというトップレベルの定数を定義していたからです。

これは拡張ライブラリ作者が内部実装に使うために用意されていたクラスだったのですが、なぜか誰も使いませんでした。そして長らく非推奨となっていて、Ruby 3.0くらいでついに削除されました。なので、2年ほど経ってはいますが、今ならまだ他のライブラリとの衝突の可能性が低いのでは?ということで、この名前になりました。

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

Ruby 3.1の時点で予告されていたとおり(参考)、Ruby 3.2ではStructクラスを使って生成したクラスは、明示的にkeyword_init: falseを指定しない限り、キーワード引数による初期化が有効になりました。

# 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: falsenilではなくfalse)を指定する必要があります。

# 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がセットされるようになったわけではない点に注意してください。

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

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

(追記)
プロと読み解く Ruby 3.2 NEWSkeyword_initに設定する値と挙動の関係が詳しく書かれていたので追記します。

少し細かいことを言うと、keyword_initはtrue/false/nilでそれぞれ意味が微妙に違います。

  • keyword_init: truePoint.new(x: 1, y: 2)で初期化できる、Point.new(1, 2)はエラー
  • keyword_init: nilPoint.new(x: 1, y: 2)でもPoint.new(1, 2)でも初期化できる
  • keyword_init: falsePoint.new(1, 2)で初期化できる、Point.new(x: 1, y: 2)は要注意の挙動(次の動作例を参照)
# 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に変わったということになります。

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

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

# 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}>

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

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

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以前では以下のように書く必要がありました。

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

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

# 大文字小文字を無視する正規表現オブジェクトを生成する
/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引数が真(nilfalse以外)なら大文字小文字を無視する」という仕様になっていたためです。

# Ruby 3.1以前の場合、第2引数が真なら常に大文字小文字を無視
Regexp.new('hello', 'i')  #=> /hello/i
Regexp.new('hello', 'im') #=> /hello/i
Regexp.new('hello', 'a')  #=> /hello/i
# 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 3.1も3.2も挙動は同じ
Regexp.new('hello', :i)  #=> /hello/i
Regexp.new('hello', :im) #=> /hello/i
Regexp.new('hello', :a)  #=> /hello/i

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

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

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パターンとして扱うこともできます。

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メソッドが定義されていないのでエラーになります。

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)

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

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

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メソッドの戻り値を参考にしてください。

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パターンで使うことはできません(年月日の並び順は各国の文化によって異なるから、というのがその理由のようです - 参考)。

# 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パターンに対応しています。

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クラスを使った方が良いでしょう。

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"}

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

Ruby 3.2ではTime#inspectが返す文字列の形式や、ISO-8601風の文字列をTime.newに渡すと、その文字列をパースしてTimeクラスのインスタンスを作れるようになりました。

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.parseTime.iso8601メソッドを使って日時文字列のパースができましたが、require 'time'が必要でした。

# 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によると、requireが不要になる以外にも以下のようなメリットがあるようです。

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

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

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

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

# 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

標準ライブラリ

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

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

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ではなく+に変換される点が異なります。

require 'uri'

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

URIに含まれるスペースは+ではなく%20に変換するのがRFC的には正しいようです(RFC 3986を参照)。

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

まとめ

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

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から導入されたパターンマッチについては、新しく章を追加して基本から発展的な内容まで詳細に説明しています。

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

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

91
45
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
91
45

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?