LoginSignup
43

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

Last updated at Posted at 2023-12-24

はじめに

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

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

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

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

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

$ ruby -v
ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin23]

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

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

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

言語仕様の変更→なし

Ruby 3.3では言語仕様の変更点はありません。

ただし、Ruby 3.4では it が最初のブロック引数を参照する変更が導入される予定です。
Ruby 3.3ではこれに関連する警告表示が導入されます(後述)。

互換性に関する変更

it という名前のメソッドの呼び出しで警告が出る場合がある

2024年にリリースされる(2023年じゃないよ!)のRuby 3.4ではデフォルトのブロックパラメータとして _1 の代わりに it が使えるようになる予定です。

# Ruby 2.7 or higher
[1, 2, 3].map { _1 * 10 }
#=> [10, 20, 30]

# Ruby 3.4(2024年12月25日リリース予定)
[1, 2, 3].map { it * 10 }
#=> [10, 20, 30]

この前準備として、2023年リリースのRuby 3.3では、ブロックの内部で it という名前のメソッドがレシーバも引数もブロックもなしに呼ばれた場合に警告を出すようになります(この警告はRubyの実行オプションによらず常に出力されます - 参考)。

def it = 10

# Ruby 3.3
[1, 2, 3].map { it * 10 }
#=> warning: `it` calls without arguments will refer to the first block param in Ruby 3.4; use it() or self.it
#=> [100, 100, 100]

こういうケースではレシーバを付けたり、メソッド呼び出しの括弧を付けたりすると警告を無くせます。

# レシーバを付けたり括弧を付けたりすれば警告は出ない
[1, 2, 3].map { self.it * 10 }
[1, 2, 3].map { it() * 10 }

it が変数だった場合は警告は出ません。

it = 10

# itが変数なら警告は出ない
[1, 2, 3].map { it * 10 }
#=> [100, 100, 100]

また、it がメソッドであっても、明示的にブロックパラメータを受け取るようになっていれば警告は出ません。

def it = 10

# ブロックパラメータ(ここではn)があるので警告が出ない
[1, 2, 3].map { |n| n + it }
#=> [11, 12, 13]

RSpecで使う it は、it "speaks English" のように文字列の引数を渡したり、it do ... end のようにブロックを引数として渡して使う場合がほとんどなので、警告が出ることは滅多にないと思います。

# itに文字列やブロックの引数を渡す場合は警告が出ない
it "speaks English" do
  expect(parrot.say).to eq "Hello"
end

この警告に関する詳しい情報は以下の記事にまとめてあります。

わざわざ it という名前のメソッドを定義しているのはRSpecぐらいしか思いつきませんし、そのRSpecも標準的なユースケースであれば警告は出ないので、この警告に出くわすことは滅多にないのでは?と考えています。

NoMethodErrorやNameError発生時のメッセージ形式が少し変わった

Ruby 3.2まではオブジェクトをinspectした結果が表示されていましたが、Ruby 3.3では"instance of (class name)"のような形式で表示されます。これは処理効率の改善を目的とした変更だそうです。

# Ruby 3.2
'-1'.abs  
#=> undefined method `abs' for "-1":String (NoMethodError)


# Ruby 3.3
'-1'.abs 
#=> undefined method `abs' for an instance of String (NoMethodError)

レシーバがnilやtrue/falseの場合は以下のように変わります。

# Ruby 3.2
nil.upcase
#=> undefined method `upcase' for nil:NilClass (NoMethodError)
true.upcase
#=> undefined method `upcase' for true:TrueClass (NoMethodError)

# Ruby 3.3
nil.upcase
#=> undefined method `upcase' for nil (NoMethodError)
true.upcase
#=> undefined method `upcase' for true (NoMethodError)

NameErrorも同じように変わります。

class Foo
  def bar
    x
  end
end

# Ruby 3.2
Foo.new.bar
#=> undefined local variable or method `x' for #<Foo:0x000000010343aa40> (NameError)

# Ruby 3.3
Foo.new.bar
#=> undefined local variable or method `x' for an instance of Foo (NameError)

あまりないと思いますが、エラーメッセージをパースするような処理やテストを書いていた場合は修正が必要になるかもしれません。

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

MatchData#named_captures が symbolize_names キーワードを受け取るようになった

Ruby 3.3では MatchData#named_captures メソッドが symbolize_names キーワードを受け取るようになりました。このキーワードを指定すると、ハッシュのキーが文字列からシンボルに変わります。

str = '2016-05-08'
match_data = str.match(/(?<year>\d+)-(?<month>\d+)-(?<day>\d+)/)

# Ruby 3.2までは常にキーが文字列
match_data.named_captures
#=> {"year"=>"2016", "month"=>"05", "day"=>"08"}

# Ruby 3.3ではキーをシンボルに変えられる
match_data.named_captures(symbolize_names: true)
#=> {:year=>"2016", :month=>"05", :day=>"08"}

Rangeクラスに overlap? メソッドが追加された

Ruby 3.3ではRangeクラスに overlap? メソッドが追加されました。このメソッドは2つの範囲オブジェクトに値の重なりがあれば true を返します。

# 5から10までが重なっているのでtrue
(0..10).overlap?(5..15)  #=> true

# 重なりがないのでfalse
(0..10).overlap?(20..30) #=> false

..... の違いも overlap? メソッドの挙動に反映されます。

# 0..1 は終端の1を含むので、1..2 と重なる
(0..1).overlap?(1..2)  #=> true

# 0...1 は終端の1を含まないので、1..2 と重ならない
(0...1).overlap?(1..2) #=> false

なお、Rails(ActiveSupport)では overlaps? というほぼ同じ仕様のメソッドが定義されています。

# Railsでは以前からoverlaps?メソッドが使える
(0..10).overlaps?(5..15)  #=> true
(0..10).overlaps?(20..30) #=> false

Rails 7.1ではRuby 3.3との互換性を考慮して overlap? メソッドが定義され(加えて overlaps?overlap? のエイリアスメソッドになった)、Ruby 3.3以降でRailsを実行した場合はRuby本体の overlap? メソッドが呼び出されるようになっています。

# Rails 7.1だとoverlap?メソッドも使える
# また、Ruby 3.3で動かした場合はRuby側のoverlap?メソッドが呼び出される
(0..10).overlap?(5..15)  #=> true
(0..10).overlap?(20..30) #=> false

ちなみに、このメソッドはシンプルに見えて意外といろんなケースを考慮しないといけなかったりします。興味がある方は overlap? メソッドのテストコードをチェックしてみてください。

先読み・後読み・アトミックグループを含む正規表現が最適化された

Ruby 3.2では10秒以上かかるような以下の正規表現がRuby 3.3では一瞬で実行できるようになりました(以下のコード例はこちらから抜粋しました)。

# 先読みを含む
/^a*?(?=a*a*)$/ =~ "a" * 1000000 + "x"
#=> nil

# 否定の先読みを含む
/^a*?(?!a*a*)$/ =~ "a" * 1000000 + "x"
#=> nil

# 後読みを含む
/(?<=abc|def)(a|a)*$/ =~ "abc" + "a" * 1000000 + "x"
#=> nil

# 否定の後読みを含む
/(?<!x)(a|a)*$/ =~ "a" * 1000000 + "x"
#=> nil

# アトミックグループを含む
/^a*?(?>a*a*)$/ =~ "a" * 1000000 + "x"
#=> nil

ただし、先読み・後読み・アトミックグループ内でキャプチャを同時に行おうとした場合は最適化されないので注意してください。

# Ruby 3.3なら速い
/^a*?(?=a*a*)$/ =~ "a" * 1000000 + "x"

# Ruby 3.3でも遅い(先読みの中でキャプチャを使うと最適化されないため)
/^a*?(?=a*(a)a*)$/ =~ "a" * 1000000 + "x"

Time.new に渡す日時文字列の制限が厳しくなった

Ruby 3.2ではTime.newが日時文字列をパースできるようになりました(参考)。

# Ruby 3.2以降
Time.new("2020-12-24T15:56:17Z")
#=> 2020-12-24 15:56:17 UTC

しかし、Ruby 3.2では "2020-12" のような日時として不完全な文字列でもパースできてしまいました。

# Ruby 3.2だと日時として不完全な文字列でもパースできてしまう
Time.new("2020-12")
#=> 2020-12-01 00:00:00 +0900

Ruby 3.3ではこのような不完全な文字列をパースしようとするとArgumentErrorが発生するようになりました。

# Ruby 3.3
Time.new("2020-12")
#=> no time information (ArgumentError)

Ruby 3.3でエラーが発生するようになったのは以下のようなケースです(参考)。

# 年月のみ
Time.new("2020-12")
# 年月日のみ(時間の情報がない)
Time.new("2020-12-02")
# 先頭にホワイトスペースが含まれる
Time.new(" 2020-12-02 00:00:00")
# 末尾にホワイトスペースが含まれる
Time.new("2020-12-02 00:00:00 ")

ただし、年だけを指定するケースはRuby 1.9.2から存在する仕様であるため、エラーになりません。

# 年だけ指定するのは有効(Ruby 1.9.2から存在する仕様)
Time.new("2000")
#=> 2000-01-01 00:00:00 +0900

また、この仕様変更はRuby 3.2へもバックポートされているため、次回のリリース(Ruby 3.2.3?)で同じ制限が適用される可能性があります。

標準ライブラリのアップデート

特定のライブラリをGemfileへの追加なしにrequireすると警告が出るようになった

Ruby 3.3では将来的にbundled gem(後述)に移行する予定がある標準ライブラリをGemfileに追加せずrequireすると、警告が出るようになりました。Ruby 3.3では以下のライブラリがこの警告の対象になります。

  • abbrev
  • base64
  • bigdecimal
  • csv
  • drb
  • getoptlong
  • mutex_m
  • nkf
  • observer
  • racc
  • resolv-replace
  • rinda
  • syslog

たとえば以下のように bigdecimal ライブラリをrequireするRubyスクリプトがあったとします。

require 'bigdecimal'

a = BigDecimal("0.1")
b = BigDecimal("0.2")
puts (a + b).to_f

bigdecimal はRuby 3.4でbundled gemに移行する予定になっています。そのため、Gemfileに"bigdecimal"を追加せずに、Bundler経由でこのスクリプトを実行すると、警告が出力されます。

$ bundle exec ruby sample.rb 
sample.rb:1: warning: bigdecimal was loaded from the standard library, but will no longer be part of the default gems since Ruby 3.4.0. Add bigdecimal to your Gemfile or gemspec.
0.3

Gemfileにgem 'bigdecimal'を追加してから実行すると、この警告は出なくなります。

# Gemfileに gem 'bigdecimal' を追加してから実行すると、警告は出ない
$ bundle exec ruby sample.rb 
0.3

Bundlerを使用せずに実行する場合は警告は出ません。

# Bundlerを使用せず、直接スクリプトを実行する場合は警告が出ない
$ ruby sample.rb
0.3

ただし、Bundlerを使用する場合でも、Railsアプリのようにbootsnap gemを利用しているとこの警告が表示されません。その場合は DISABLE_BOOTSNAP=1 という環境変数を付けてアプリケーションを実行してください。

# Railsアプリのようにbootsnap gemを利用している場合はDISABLE_BOOTSNAP=1を付けて実行する
$ DISABLE_BOOTSNAP=1 rails c
/Users/jnito/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/activesupport-7.0.2.3/lib/active_support/message_verifier.rb:4: warning: base64 was loaded from the standard library, but will no longer be part of the default gems since Ruby 3.4.0. Add base64 to your Gemfile or gemspec. Also contact author of activesupport-7.0.2.3 to add base64 into its gemspec.
/Users/jnito/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/activesupport-7.0.2.3/lib/active_support/xml_mini.rb:5: warning: bigdecimal was loaded from the standard library, but will no longer be part of the default gems since Ruby 3.4.0. Add bigdecimal to your Gemfile or gemspec. Also contact author of activesupport-7.0.2.3 to add bigdecimal into its gemspec.
/Users/jnito/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/activesupport-7.0.2.3/lib/active_support/notifications/fanout.rb:3: warning: mutex_m was loaded from the standard library, but will no longer be part of the default gems since Ruby 3.4.0. Add mutex_m to your Gemfile or gemspec. Also contact author of activesupport-7.0.2.3 to add mutex_m into its gemspec.
Loading development environment (Rails 7.0.2.3)
irb(main):001:0> 

なお、この警告はBundler 2.5.0以上であることも必須です。Bundlerのバージョンが古い場合はアップデートしてください。

# 備考:上記の警告を出すためにはBundler 2.5.0以上であることも必須
$ bundle -v
Bundler version 2.5.3

# バージョンが古い場合はBundler自体をアップデートする
$ bundle update --bundler

また、Rails 7.1ではすでに対応が完了しているようで、DISABLE_BOOTSNAP=1 rails cを実行しても警告は表示されません。

$ DISABLE_BOOTSNAP=1 rails c
Loading development environment (Rails 7.1.2)
irb(main):001>

豆知識:bundled gem(とdefault gem)って何?

Rubyの標準ライブラリ(Rubyをインストールしたら必ず使えるライブラリ)はgemとして提供されているものがあります。標準ライブラリのgemはそこからさらに bundled gemdefault gem の2種類にわかれます。

gem list (gem name) で調べるとどちらのタイプかがわかります。

$ ruby -v
ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin23]

# bigdecimalは (Ruby 3.3の時点では) default gem
$ gem list bigdecimal
*** LOCAL GEMS ***
bigdecimal (default: 3.1.5)

# minitestはbundled gem (defaultの表示がない)
$ gem list minitest
*** LOCAL GEMS ***
minitest (5.20.0)

bundled gemはRubyインストール時に同時にインストールされるふつうのgemです。ふつうのgemなので、やろうと思えばアンインストールすることもできます。

$ gem uninstall minitest
Successfully uninstalled minitest-5.20.0

一方、default gemはアンインストールできません。

$ gem uninstall bigdecimal
Gem bigdecimal-3.1.5 cannot be uninstalled because it is a default gem

Bundlerを利用する場合、bundled gemはそのgemを必ずGemfileに追加する必要があります。

Gemfile
# bundled gemはGemfileへの追加が必須
gem 'minitest'

一方、default gemはGemfileに追加しなくても使用できます。Gemfileにdefault gemを明示的に追加した場合は、そこで指定したバージョンのgemが優先的に使われます。

Gemfile
# default gemはGemfileに追加してもしなくても良い
# (しなくても良いが、追加するとそのgemのバージョンを固定できるようになる)
gem 'bigdecimal'

上記の内容を表にまとめると以下のようになります。

default bundled
Rubyをインストールすれば使える? Y Y
アンインストールできる? N Y
Gemfileにgemとして追加できる? Y Y
Gemfileへの追加が必須? N Y(注意!!)

参考文献:bundled gem と default gem の違い - @znz blog

default gemがbundled gemに変わったときによく起きる問題

ところで、今までdefault gemだったものが、あるRubyのバージョンからbundled gemに変わると、Bundlerを利用しているアプリケーションが動かなくなる場合があります。なぜなら、前述の通り、default gemと違ってbundled gemはGemfileにそのgemを追加しないとライブラリが利用できないからです(前述の表を参照)。

みなさんももしかすると、net-smtpやwebrickをGemfileに追加しないとネット上のサンプルコードが動かない、もしくはRubyのバージョンを上げた途端にアプリケーションが動かなくなった、といった問題に遭遇したことがあるかもしれません。これはnet-smtpやwebrickがbundled gemに変更された影響です。

# net-smtpがbundled gemに変わったことで発生するエラーの例
# (Gemfileに gem 'net-smtp' を追加すると直る)
cannot load such file -- net/smtp

default gemがbundled gemに変わるとこうした問題が起きがちなので、Ruby 3.3からは突然bundled gemに変更するのではなく、bundled gemに変わることを事前に予告し、対象のgemを予めGemfileに追加してもらうようにするため、警告が出力されるようになりました。

# bigdecimalがもうすぐbundled gemに変わるから、予めGemfileに追加しておいてね!!
$ bundle exec ruby sample.rb 
sample.rb:1: warning: bigdecimal was loaded from the standard library, but will no longer be part of the default gems since Ruby 3.4.0. Add bigdecimal to your Gemfile or gemspec.
0.3

ちなみに余談ですが、default gemはメンテナンスがかなり大変であるため、Rubyコア開発チームとしては積極的にbundled gems化を推し進めていきたいそうです(ruby-jpで聞いた話)。

optparse の getopts メソッドで symbolize_names キーワードを受け取るようになった

Ruby 3.3では optparse の getopts メソッドで symbolize_names キーワードを受け取るようになりました。このキーワードを指定すると、ハッシュのキーが文字列からシンボルに変わります。

require 'optparse'

params = ARGV.getopts("arl", symbolize_names: true)
p params
# symbolize_names: trueを指定したので、キーが文字列ではなくシンボルになっている
$ ruby sample.rb -al
{:a=>true, :r=>false, :l=>true}

なお、この変更点はNEWS.mdには書かれていませんが、僕自身がこのプルリクエストを作ったので自慢ついでに紹介したかったのでした :sweat_smile:

irbに関する変更点

型ベースの入力補完ができるようになった(実験的機能)

Ruby 3.3(正確にはirb 1.9.0以降)では、実験的機能として型ベースの入力補完ができるようになりました。

この機能を使うためにはまず repl_type_completor gem をインストールします。

gem install repl_type_completor

それから --type-completor オプションを付けてirbを起動します。

irb --type-completor

型ベースの入力補完を有効にすると、ブロックパラメータのメソッド等も型定義に基づいて候補をリストアップしてくれるようになります。

たとえば以下の例は n がIntegerであると判定されるので up まで入力した時点で upto を候補として表示しています。

Screenshot 2023-12-23 at 18.46.52.png

Ruby 3.2では常に正規表現ベースで候補を表示するため、upto 以外のメソッドも候補に挙がっていました。

Screenshot 2023-12-23 at 18.46.17.png

ただし、型ベースの入力補完を有効にするトレードオフとして、候補を表示するまでの時間が遅くなるようです。

より詳しい内容はirbのREADMEを参照してください。

2023.12.27追記
Ruby 3.3におけるirbの新機能については以下の記事にも詳しくまとめられています。

その他の変更点

Ruby 3.3.0のリリースノートの中から、注目すべき内容をいくつかピックアップします。

  • 新しいRubyパーサーであるPrismパーサーがdefault gemとして導入された
  • 新しいパーサージェネレータであるLrama(リャマ)によって、Bisonが置き換えられた
  • Rubyで実装されたJITコンパイラであるRJITが実験的に導入された(これに伴いMJITは削除された)
  • YJITがさらに高速化された
  • スレッドの生成管理コストを抑える M:N スレッドスケジューラが導入された
  • Socket.getaddrinfo の名前解決中断機能や、GC周りの改修などにより、パフォーマンスが改善された
  • default gemやbundled gemのバージョンが上がった(詳細はリリースノートを参照)

参考情報:Ruby 3.3を使うとRailsアプリでYJITが自動的に有効になる?

先日、RailsのリポジトリでRuby 3.3でRailsを実行したときに自動的にYJITが有効になるPRがマージされました。

本記事の執筆時点ではこの修正はまだ公式リリースされていませんが、近い将来、Ruby 3.3とRailsを組み合わせるとデフォルトでYJITが有効になる世界が待ってそうです。

まとめ

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

Ruby 3.3は表面的な言語仕様の変更や便利メソッドの追加よりも、新しいパーサーの導入やYJITのパフォーマンス改善など、内部的なブラッシュアップが中心になっている印象を受けます。
Rubyの土台が改善されることで、新しいライブラリの登場や処理速度の向上といった恩恵が受けられるのではないかと期待しています。

また、僕のように普段はRailsアプリケーションの開発をメインとしている開発者は、特に違和感なくすっとRuby 3.2から3.3へ移行できそうな雰囲気があります。実際、小さなRails 7.0アプリケーションをRuby 3.3で動かしたところ、コードをいじることなく自動テストを全部パスさせることができました 💯

あと、bundled gemに移行する予定のあるライブラリを事前に教えてくれる機能もありがたいですね。この問題は実務でもよく遭遇していたので。

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

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以降で発生する記述内容との差異は、それぞれ以下の記事にまとめてあります。なので、多少バージョンが古くても安心して読んでいただけます😊

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
43