はじめに
2022年12月25日に、Rubyの新しいバージョンであるRuby 3.2がリリースされました。
一方、2021年12月2日に出版した書籍「プロを目指す人のためのRuby入門 改訂2版」(通称・チェリー本。以下、本書)は執筆当時最新だったRuby 3.0を対象にしています。
本書は紙の本であるため、簡単に内容をアップデートすることができません。しかし、何もしないとどんどん内容が古くなってしまい、「本の通りやってみたけど、今使っているRubyとなんか動きが違う」ということになってしまいます。
そこで新しいRubyのバージョンがリリースされて、本書の説明と異なる部分が出てきたときは、毎回ネット上でその差異を説明するようにしています。その説明を読めば、動きが違う部分があってもきっと落ち着いて対処できるはず、という算段です。
というわけで、この記事ではRuby 3.2で発生する「プロを目指す人のためのRuby入門 改訂2版」との差異について説明します(第1版との差異ではないのでご注意ください)。
また、「プロを目指す人のためのRuby入門 改訂2版」を持っていない人でも役に立ちそうなRuby 3.2の新機能や変更点もあわせて紹介します!
なお、本文に出てくる章番号や項番号は、書籍の中で使われている番号です。
参考: もう読みましたか?Ruby 3.1との差異はこちら
Ruby 3.1で発生する差異については以下の記事にまとめてあります。
上記の記事と重複する内容はこの記事では説明しません。
ですので、まだ読まれていない方は先に上の記事を読んでから、この記事に戻ってくることをお勧めします。
それでは以下が本編です。
本文の説明と実行結果が異なるもの
以下で説明する内容は、Ruby 3.2で実行した場合に本書(つまりRuby 3.0)で説明している内容と実行結果が異なる部分です。
TypeErrorとArgumentError発生時の表示がわかりやすくなった(本書全般、12.3.3、12.3.4)
Ruby 3.2ではTypeErrorやArgumentErrorが発生した場合に、問題の発生箇所をわかりやすく下線付きで教えてくれるようになりました。
たとえば、第1章のコラム「数値と文字列は暗黙的に変換されない」のサンプルコードを実行すると以下のように表示されます(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>'
同じく、12.3.4項のサンプルコードを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キーワードに過不足がある場合のエラー表示が改善された(9.6.7、12.3.8)
Ruby 3.2ではendキーワードに過不足によってSyntaxErrorが発生した場合に、問題の発生箇所をわかりやすく教えてくれるsyntax_suggestという機能が入りました。
たとえば、9.6.7項のサンプルコードを実行すると以下のように表示されます(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)
開発の効率が上がりそうな便利機能ですね。
Setクラスが組み込みクラスになった(4.7.4)
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}>
パターンマッチのfindパターンが実験的機能ではなくなった(11.3.7)
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
に代入する」という意味のパターンマッチです。
パターンマッチの詳しい説明は本書の第11章をご覧ください。
Minitestの型情報の追加方法が変わった(13.10.2)
※Steep自体はRuby 3.2のリリースとは無関係ですが、ここでついでに紹介します
Steep 0.51.0を使っている場合、最初に steep check
を実行したときのエラー表示が本書と異なります(Ruby::UnknownConstantのエラーが最初に表示される)。
$ steep check
# Type checking files:
.........................................................................F...........
test/fizz_buzz_test.rb:4:21: [warning] Cannot find the declaration of constant: `Minitest`
│ Diagnostic ID: Ruby::UnknownConstant
│
└ class FizzBuzzTest < Minitest::Test
~~~~~~~~
test/fizz_buzz_test.rb:6:4: [error] Type `::FizzBuzzTest` does not have method `assert_equal`
│ Diagnostic ID: Ruby::NoMethod
│
└ assert_equal '1', fizz_buzz(1)
~~~~~~~~~~~~
# 省略
また、その後に本書の指示に従ってMinitestの型情報を追加しても、まだ以下のエラーが残ったままになります。
$ steep check
# Type checking files:
................................................................................F..
test/fizz_buzz_test.rb:4:21: [warning] Cannot find the declaration of constant: `Minitest`
│ Diagnostic ID: Ruby::UnknownConstant
│
└ class FizzBuzzTest < Minitest::Test
~~~~~~~~
Detected 1 problem from 1 file
この問題が発生した場合は、sig/fizz_buzz.rbs
を以下のように修正してください。
# 省略
-class FizzBuzzTest
+class FizzBuzzTest < Minitest::Test
def test_fizz_buzz: -> untyped
end
さらに、Steepfileにlibrary "minitest"
も追記してください。
target :lib do
check 'lib/fizz_buzz.rb'
check 'test/fizz_buzz_test.rb'
signature 'sig'
+ library "minitest"
end
こうすればSteep 0.51.0以上でもsteep checkが成功するはずです。
$ steep check
# Type checking files:
...................................................................................
No type error detected. 🫖
より詳しい内容は以下の記事をご覧ください。
Ruby 3.2で追加された新しい言語仕様
このほかにもRuby 3.2では、本書執筆時点にはなかった新しい構文がいくつか追加されています。
引数名が付かない*
や**
を別のメソッドに渡せるようになった(第7章のコラム:引数名が付かない*
や**
)
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, **)}"
その他、Ruby 3.2で注目したい新機能
ここから下では「プロを目指す人のためのRuby入門」を読んだ方なら知っておいて損のない、注目の新機能や変更点を紹介していきます。
Webブラウザ上でもRubyが実行できるようになった(1.4節に関連)
Ruby 3.2ではWASI(The WebAssembly System Interface)ベースのWebAssembly(Wasm)へのコンパイルがサポートされたため、webブラウザ上でもRubyが実行できるようになりました。
以下はTryRuby playgroundでRuby 3.2を実行した例です。
TryRuby playgroundがあれば本書を読み進める上でローカルの実行環境は不要、というわけにはいきませんが、簡単なサンプルコードであればブラウザ上で動作確認できるようになります。
また、Rubyがブラウザ上で実行できるようになったことから、今後さらにRubyを活用できる場面が増えていくことも期待できます。
RubyのWebAssembly/WASIサポートに関する詳しい情報は下記の公式リリースノートをご覧ください。
商を切り上げしてくれるInteger#ceildiv
メソッドが追加された(2.4節に関連)
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
環境変数でirbの自動補完の有効・無効を制御できるようになった(第2章のコラム:「Ruby 2.7以降で使えるirbの便利機能」に関連)
Ruby 3.2のirbではIRB_USE_AUTOCOMPLETE
という環境変数で自動補完の有効・無効を制御できるようになりました。たとえば次のような形でirbを起動すると、自動補完が無効になります。
IRB_USE_AUTOCOMPLETE=false irb
これに関連してproduction環境でrails console(rails c)を起動するとデフォルトで自動補完が無効になる変更も採用されています(参考)。
参考文献: Ruby 3.2 のIRBの新機能 - Qiita
正規表現が高速化した(第6章に関連)
Ruby 3.2では正規表現が高速化し、ReDoS攻撃(正規表現の計算コストが膨大に発生する文字列を送り込んで、サーバーの計算リソースを占有する攻撃)の影響を受けにくくなりました。
たとえば "_a_____________________" =~ /(_+|\w+)*a/
というコードはRuby 3.1までは結果が得られるまで膨大な時間がかかりましたが、Ruby 3.2では一瞬で結果が返ります。
公式リリースノートによれば、この高速化によって90%程度の正規表現が線形時間でマッチ判定できるようになるとのことです(一部の正規表現では高速化が有効に働かないケースがあることに注意してください)。
このアルゴリズムの改善で、ほとんどの(我々の実験では90%程度の)正規表現が線形時間でマッチ判定できるようになります。
なお、高速化されるかどうかの判定はRegexp.linear_time?
メソッドで判定できます(true
なら高速化される)。
Regexp.linear_time?(/(_+|\w+)*a/) #=> true
Regexp.linear_time?(/^a*b?a*()\1$/) #=> false
この改善に関する技術的な説明は以下の記事を参照してください。
正規表現のタイムアウト時間を指定できるようになった(第6章に関連)
上記の高速化により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.new
にtimeout
キーワードを指定します。
# この正規表現オブジェクトにだけ、タイムアウト秒数を指定する
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)
Regexp.newのオプションを文字列でも指定できるようになった(6.5.3項に関連)
# 大文字小文字を無視する正規表現オブジェクトを生成する
/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 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
組み込みライブラリにDataクラスが新たに導入された(第7章に関連)
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
メソッドが実装されているので、パターンマッチで使うこともできます(deconstruct
メソッドとdeconstruct_keys
メソッドについては本書の11.5.4項を参照してください)。
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など)に変更してください。
Struct.newで明示的にkeyword_init: false
を指定しない限り、キーワード引数による初期化が有効になった(第7章に関連)
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: false
(nil
ではなく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入門」ではStructクラスは登場していません。Structクラスについて知りたい場合は、以下の記事を参考にしてみてください。
MatchDataオブジェクトがパターンマッチに対応した(第11章に関連)
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オブジェクトがパターンマッチに対応した(第11章に関連)
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"}
binding.irbからdebug.gemを起動できるようになった(第12章のコラム:その他のデバッグツールに関連)
Ruby 3.2ではbinding.irb
でプログラムを停止(irbを起動)してからdebug
コマンドを入力すると、debug.gemを起動してデバッグを開始できるようになりました。
このほかにもirb上で$ クラス名
や$ メソッド名
といったコマンドを入力することで、クラスやメソッドのコードを表示できる機能が追加されています。
その他、Ruby 3.2におけるirbの便利な新機能については以下の記事をご覧ください。
Time.newが日時文字列をパースできるようになった(13.2節に関連)
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.parse
やTime.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.iso8601
はTime#inspect
が返す文字列をパースできない、という問題を解消できる -
Time.parse
は予期しない結果になることがよくある、という問題を解消できる -
Time.new
はTime.iso8601
より約1.9倍速い
File.exists?とDir.exists?が削除された(13.3節に関連)
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
Bundlerが速くなった (13.9.2項に関連)
Ruby 3.2では以下のような改善によってBundlerが速くなりました。
本書とは直接関係ないが知っておくと良さそうな新機能や変更点
以下は特に本書と関連の深い項はないものの、Railsアプリケーション開発時に知っておくと良さそうなRuby 3.2の変更点です。
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
についてはこちらの記事でも紹介しています。
ERB::Util.html_escape
とERB::Util.url_encode
が速くなった
Ruby 3.2ではERB::Util.html_escape
がCGI.escapeHTML
よりも速くなりました。また、ERB::Util.url_encode
はCGI.escapeURIComponent
よりも速くなりました。
require 'cgi'
require 'erb'
# Ruby 3.2なら速い
ERB::Util.html_escape("is a > 0 & a < 10?")
#=> "is a > 0 & a < 10?"
# 同じ結果になるが遅い
CGI.escapeHTML("is a > 0 & a < 10?")
#=> "is a > 0 & a < 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"
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.
様々なパフォーマンス改善が行われた
Ruby 3.2では様々なパフォーマンス改善が行われています。
技術的に難しい内容になるためここでは詳しく説明しませんが、気になる方はNEWS.mdの"Implementation improvements"欄や"JIT"の欄を参照してください。
他にも魅力的な新機能や変更点がたくさん!!
この記事では「プロを目指す人のためのRuby入門」の読者のみなさんが興味を持ちそうな新機能や変更点をピックアップして紹介しましたが、Ruby 3.2にはこのほかにもたくさんの新機能や変更点があります。
ぜひ公式リリースノートやNEWS.md、Ruby 3.2アドベントカレンダーをチェックしてみてください!
そして、今年も2022年のクリスマスにRuby 3.2を届けてくれたMatzさんやコミッタのみなさんに感謝したいと思います。どうもありがとうございました!
みなさんもぜひRuby 3.2の新機能を試してみてください😉
まとめ
というわけで、この記事ではRuby 3.2で発生する「プロを目指す人のためのRuby入門 改訂2版」との差異をまとめました。
Ruby 3.1からの差分という観点では、本書の内容がガラッと古くなるような変更点はそれほど多くなかったと思います。
ですが、Dataクラスの導入や、MatchDataクラスやTimeクラスのパターンマッチ対応など、Ruby中級者や上級者ならうまく活用できそうな魅力的な新機能がたくさん導入されているように感じました。それから、正規表現が高速化し、ReDoS攻撃の影響を受けにくくなった点も素晴らしい改善点だと思います。
「プロを目指す人のためのRuby入門 改訂2版」とこの記事(と、Ruby 3.1との差異)をあわせて読めば、Ruby 3.2でもバリバリと実践的なコードが書けるはずです。まだ「プロを目指す人のためのRuby入門 改訂2版」を購入されていない方は、この機会にぜひ購入を検討してもらえると嬉しいです。みなさん、よろしくお願いします!
PR:「プロを目指す人のためのRuby入門 改訂2版」について
「プロを目指す人のためのRuby入門 改訂2版」は、他の言語での開発経験があり、これからRubyを始めたい人や、Rubyプログラミングの経験はある程度あるものの、まだまだ自信がない人に向けて、Rubyの言語仕様や開発の現場で役立つ知識を詳しく、ていねいに解説したRubyの入門書です。
改訂2版ではRuby 3.0に完全対応し、「パターンマッチ」の章を新たに追加するなど、第1版に比べてさらに内容が充実しています。
改訂2版の変更点については以下のブログ記事で詳しく説明していますので、こちらのぜひチェックしてみてください。