STORES Product Blog

こだわりを持ったお商売を支える「STORES」のテクノロジー部門のメンバーによるブログです。

プロと読み解くRuby 3.4 NEWS

プロと読み解くRuby 3.4 NEWS

テクノロジー部門技術基盤グループの笹田(ko1)と遠藤(mame)です。Ruby (MRI: Matz Ruby Implementation、いわゆる ruby コマンド) の開発をしています。お金をもらって Ruby を開発しているのでプロの Ruby コミッタです。

本日 12/25 に、恒例のクリスマスリリースとして、Ruby 3.4.0 がリリースされました(Ruby 3.4.0 リリース )。今年も STORES Product Blog にて Ruby 3.4 の NEWS.md ファイルの解説をします(ちなみに、STORES Advent Calendar 2024 の記事になります。他も読んでね)。NEWS ファイルとは何か、は以前の記事を見てください。

本記事は新機能を解説することもさることながら、変更が入った背景や苦労などの裏話も記憶の範囲で書いているところが特徴です。

Ruby 3.4 は、言語(文法)の変更がほぼありませんでした。というのも、文法を解釈するパーサーを Prism に変更するという大きな仕事をするために、文法変更をやめとこう、となったためです。

Ruby 3.4 の代表的な変更は次のようなものになります(リリースノートから抜粋)。

  • it の追加
  • デフォルトのパーサをPrismに変更
  • Socket ライブラリの Happy Eyeballs Version 2 (RFC 8305) 対応
  • YJIT の更新
  • Modular GC

本記事では、これらを含めて NEWS ファイルにあるものをだいたい紹介していきます。

言語の変更

ブロックパラメータ it が利用可能に

ブロックパラメータに名前をつけずに参照する it がついに導入されました。

ary = ["foo", "bar", "baz"]

p ary.map { it.upcase }  #=> ["FOO", "BAR", "BAZ"]

かんたんなブロックをよりかんたんに、より読みやすく書けるようになります。

numbered parameter との違い

it はおおよそ numbered parameter の _1 と同じです。ただし、書ける位置の制限が少し緩和されています。

"foo".then do
  p "bar".then { _1.upcase } #=> "BAR"
  p _1 # ここで _1 を書くと syntax error
end

"foo".then do
  p "bar".then { it.upcase } #=> "BAR"
  p it.upcase #=> "FOO" # it ではこれが許される
end

許されているとはいえ、it が何を指しているかわかりにくくなるので、ワンラインのブロック以外で it を使うのは避けたほうが賢明かと思います。

また、numbered parameter は _2_3 で n 番目の引数を読み出すことができますが、it にはそのような機能はありません。

ちなみに、it と numbered parameter を同一ブロック内で参照することは禁止されています。

[1, 2, 3].then { p [it, _1, _2] } # これは syntax error

it 導入までの背景

numbered parameter が導入される際、その記法がいくつも提案され、その中に it もありました [Feature #15897] 。というか自分が提案していました。

it という名前が好きな人は結構いたのですが、互換性の問題がありました。it というメソッド名は既存のコード(主に RSpec)で多用されているので、単純にキーワードにすると、致命的な非互換になります。これを避けるため、「無引数の it の呼び出しだけキーワードにする」というトリッキーな工夫を発明しました。

が、it には n 番目の引数を読む機能がなく、それでよいのか確信が持てなかったことなどで、結局 _1 という記法が採用されました。

その後、人々が numbered parameter を実用し始めると、2 番目以降の引数を読む機能はあまり使われず、_1 を使うことが圧倒的に多いことがわかり始めました。

_1 しか使わないのに 1 という数字を見ることは認知負荷があって辛い。ということで、より目に優しい _1 の言い換えとして、it が導入されることが決定しました。Ruby 3.3 では移行のためメソッド it の無引数の呼び出しを警告し(通称「地上げ」)、今回 Ruby 3.4 で使えるようになりました。

(mame)

frozen_string_literal がデフォルトの方向に

  • String literals in files without a frozen_string_literal comment now emit a deprecation warning when they are mutated. These warnings can be enabled with -W:deprecated or by setting Warning[:deprecated] = true. To disable this change, you can run Ruby with the --disable-frozen-string-literal command line argument. [Feature #20205]

文字列リテラルを freeze しておく frozen_string_literal という機能がありましたが、これがデフォルトになります。といっても Ruby 3.4 でなるのではなく、もっと後のバージョンでデフォルトになる予定です(なので「方向に」)。

Ruby 3.4 での具体的な変更としては、-w もしくは -w:deprecated 指定で実行すると(deprecated 警告を有効にすると)、文字列リテラルに対する変更(破壊的操作)に警告が出るようになりました。

$ ruby -W:deprecated -e '"" << ?a'
-e:1: warning: literal string will be frozen in the future (run with --debug-frozen-string-literal for more information)

その警告メッセージにもあるように、--debug-frozen-string-literal を追加することで、その破壊的操作を行った対象の文字列がどこで生成したか表示してくれます。

$ ruby -W:deprecated --debug-frozen-string-literal -e 's = ""
s << ?a'
-e:2: warning: literal string will be frozen in the future # 2行目ので破壊的操作を行っているので警告
-e:1: info: the string was created here                    # 1行目で作られた文字列に対してであることを表示

これまで通り、frozen_string_literal プラグマは利用可能です。

$ ruby -W:deprecated -e '# frozen_string_literal: true
> "" << ?a'
-e:2:in '<main>': can't modify frozen String: "", created at -e:2 (FrozenError)

そして、これまでデフォルトであったため、おそらく誰も適用しなかったであろう # frozen_string_literal: false を指定しておくと、この警告が出なくなります。つまり、明確に文字列リテラルは frozen ではない、という宣言になります。

$ ruby -W:deprecated -e '# frozen_string_literal: false
"" << ?a'

アプリ全体についてこの挙動を指定したい場合は、--enable-frozen-string-literal、もしくは --disable-frozen-string-literal を指定します。

$ ruby -W:deprecated --enable-frozen-string-literal -e 'a = ""
a << ?a'
-e:2:in '<main>': can't modify frozen String: "" (FrozenError)

$ ruby -W:deprecated --disable-frozen-string-literal -e 'a = ""
a << ?a'
# 警告は出ない

つまり、--disable-frozen-string-literal を指定しておけば、これまでと全く同じように警告は出なくなります。

警告 "warning: literal string will be frozen in the future" が示す通り、この警告が出ているところは frozen な文字列に対する操作となるため、エラーになる予定です。チケットを見ると、Ruby 4.0 からエラーにすっか、って書いてあります。まぁ、予定は予定なので、どうなるかわかりません。

内部的には、破壊的操作を行ったら警告を出す(そして将来的に frozen になる)文字列のことを Chilled String と呼んで区別していますが、まぁ普通に使っているなら必要ない知識です。

この話の背景:

そもそも、文字列リテラルを freeze しておこう、というのは、Ruby 3.0 から導入される予定で、Ruby 2.x で色々議論されました。

その動機は主に (1) 文字列リテラルが freeze されていれば、その箇所で同じ文字列を返すことができるため、性能上のメリットがある、(2) なるべく freeze されていたほうがプログラミングがしやすい、という2点がメリットとされていると思いますが、よく聞くのは主に (1) だと思います。

ただ、その恩恵に比べて非互換などのデメリットが大きい、という理由でこれまで適用されていませんでした。ただ、この機能が好きな人は多くて、スクリプトの冒頭に # frozen_string_literal: true と書く人がいっぱい出てきました。エディタの支援で勝手に挿入されることも多いですね。

そこで、そもそもみんな書いているなら、この挙動をデフォルトにして、この面倒なプラグマを書かなくてもよくしようではないか、というのが、今回改めてデフォルト化しようとした理由になります。

ちなみに、Feature #20205: Enable frozen_string_literal by default - Ruby master - Ruby Issue Tracking System のコメントによると、公開された RubyGems のソースコードのファイル中で、# frozen-string-literal: true を指定しているのは下記の割合のようです。

Number of gems that have `# frozen-string-literal: true` in all .rb files

  among all public gems: 14254 / 175170 (8.14%)
  per-file basis: 445132 / 3051922 (14.59%)

  among public gems with the latest version released in 2015-present: 14208 / 101904 (13.94%)
  per-file basis: 443460 / 1952225 (22.72%)

  among public gems with the latest version released in 2020-present: 11974 / 41205 (29.06%)
  per-file basis: 389559 / 1136445 (34.28%)

  among public gems with the latest version released in 2022-present: 9121 / 26721 (34.13%)
  per-file basis: 329742 / 848061 (38.88%)

全体では 15% くらい。最近リリース(2022年以降)したファイルだと 35% くらいのファイルに入っているそうです。どうでしょう。多かった? 少なかった?

これまで s = ""; s << s2 のように気軽に変更する文字列(バッファ)を用意できていたのが、s = "".dup; s << s2s = +""; s << s2 など、一工夫必要になるので、Ruby 的にはかなり大きなマインドチェンジになるんじゃないかと思います。

でも、# frozen_string_literal: true と書いてきた人にはそうでもないかな?

ちなみに、--enable-frozen-string-literal は、Ruby 2.x で議論をしているとき、デフォルトで文字列リテラルを freeze したらどうなるか、試すために導入されました(大昔にパッチ書きました)。ついでに導入された --disable-frozen-string-literal は、ついに日の目を浴びた格好です。

(ko1)

ちなみに遠藤はこの変更がとても嫌いです。Ruby のオブジェクトは原則 mutable であるべき。

(mame)

String#+@ が dup するかどうかの条件が変わった

  • String#+@ now duplicates when mutating the string would emit a deprecation warning, offered as a replacement for the str.dup if str.frozen? pattern.

frozen_string_literal のデフォルト化のついでの話です。

これまで、String#+@、つまり +"foo" は mutable な文字列の場合は何もしませんでしたが、破壊的操作をすると警告するような文字列については dup するようになりました。

a = "str"
b = +a
p a.equal?(b) #=> false、つまり別オブジェクトになっている

c = "str".dup # c に破壊的変更をすると警告は出ない
d = +c
p c.equal?(d) #=> true、つまり dup せず同じオブジェクト

これまでは c に対するような挙動だけでしたが、a に対するような挙動が追加されたということになります。

つまり、例の警告を見たら + 付けとこうかな、という判断ができるようになったということです。

(ko1)

キーワード引数の **nil を渡せるようになった

  • Keyword splatting nil when calling methods is now supported. **nil is treated similarly to **{}, passing no keywords, and not calling any conversion methods. [Bug #20064]

ハッシュをキーワード引数として渡すための **nil を渡せるようになりました。

def hello(name: "default")
  puts "Hello, #{ name }"
end

# ハッシュをキーワード引数として渡せる
h = { name: "mame" }
foo(**h) #=> Hello, mame

# ハッシュではなく nil を渡すと、空ハッシュを指定したのとおなじ
h = nil
foo(**h) #=> Hello, default

これが許されるようになったのは、次のようなコード例で matz が説得されたからです。

h = if params.key?(:name)
  { name: params[:name] }
end

User.new(**h)

個人的には、if が暗黙的に返した nil を利用するのはそんなにおすすめしないけどなあ。

(mame)

[]= でブロックを渡すことができなくなった

  • Block passing is no longer allowed in index assignment (e.g. a[0, &b] = 1). [Bug #19918]

誰もやらないと思うんですが、これまで a[0, &b] = 1 としてブロックを渡せたのができなくなりました。 真面目にやると評価順とか気にするところが多かったようです。

(ko1)

[]= でキーワード引数を渡すことができなくなった

  • Keyword arguments are no longer allowed in index assignment (e.g. a[0, kw: 1] = 2). [Bug #20218]

これも同様ですが、a[0, kw: 1] = 2 のような書き方は禁止されました。 実際に使う人がいたようですが、禁止という方向になったようです。

(ko1)

トップレベル定数 Rubyが予約された

  • The toplevel name ::Ruby is reserved now, and the definition will be warned when Warning[:deprecated]. [Feature #20884]

トップレベルの定数 Ruby が地上げされることになりました。これを定義すると警告されます。

# ruby -w で実行すること

# Rubyモジュールを自分で定義すると警告が出る
module Ruby #=> warning: ::Ruby is reserved for Ruby 3.5
end

# Ruby定数への代入でも警告が出る
Ruby = 1 #=> warning: ::Ruby is reserved for Ruby 3.5

Ruby 3.5 では Ruby モジュールが定義され、Ruby に関するメタ的な機能で Ruby 処理系で共通に提供されるべきものが置かれる予定です。具体的に何が置かれるかは決まっていませんが、Ruby::VERSIONRUBY_VERSION の別名として定義されるとか。

(mame)

組み込みクラスのアップデート

Array#fetch_values が追加された

Hash#fetch_values があるので Array#fetch_values があってもいいじゃない、ということで追加されました。

["foo", "bar", "baz"].fetch_values(1, 2)         #=> ["bar", "baz"]
["foo", "bar", "baz"].fetch_values(1, 4)         #=> index 4 outside of array bounds: -3...3 (IndexError)
["foo", "bar", "baz"].fetch_values(1, 4) { 42 }  #=> ["bar", 42]

Array#values_at は境界外アクセスに対して nil を返すのに対して、Array#fetch_valuesは IndexError になる(ブロックがあったらそれを呼ぶ)ということのようです。

(mame)

Exception#set_backtraceThread::Backtrace::Location の配列が渡せるようになった

  • Exception#set_backtrace now accepts arrays of Thread::Backtrace::Location. Kernel#raise, Thread#raise and Fiber#raise also accept this new format. [Feature #13557]

背景の説明がとても長いです。あまり使ってほしい機能でもないので、どうしても読みたい人だけ読んでください。

まず、Ruby の例外のバックトレースは Exception#backtrace というメソッドでプログラム的に取り出すことができます。

def foo
  raise RuntimeError, "exception"
end

begin
  foo
rescue
  pp $!.backtrace #=> ["test.rb:2:in 'Object#foo'", "test.rb:6:in '<main>'"]
end

しかし、バックトレースの各行からファイル名だけを取り出したいとなったら、正規表現で切り出すなどする必要がありました。そこで、Exception#backtrace_locations というメソッドが導入されました。これは Thread::Backtrace::Location のインスタンスの配列を返すので、かんたんにファイル名や行番号などを取り出せるようになりました。

def foo
  raise RuntimeError, "exception"
end

begin
  foo
rescue
  loc = $!.backtrace_locations.first
  
  # 一見文字列っぽいけど、実際には Thread::Backtrace::Location のインスタンス
  p loc        #=> "test.rb:2:in 'Object#foo'"
  p loc.class  #=> Thread::Backtrace::Location
  
  # ファイル名や行番号をかんたんに取り出せる
  p loc.path   #=> "test.rb"
  p loc.lineno #=> 2
end

また、Kernel#caller_locations でも、呼び出し時点のバックトレースを取り出すことができます。

def foo
  loc = caller_locations.first
  p loc         #=> "test.rb:8:in '<main>'"
  p loc.path    #=> "test.rb"
  p loc.lineno  #=> 8
end

foo

ところで、例外のバックトレースはraise の第3引数で差し替えることができます。

def foo
  raise RuntimeError, "exception", ["foo", "bar"]
end

begin
  foo
rescue
  pp $!.backtrace #=> ["foo", "bar"]
  pp $!.backtrace_locations #=> nil
end

このように文字列の配列でバックトレースを指定されてしまった場合、インタプリタは実際のファイル名や行番号を知ることができないので、Thread::Backtrace::Locationを作れなくなります。よって、Exception#backtrace_locationsnil を返しています。

さて、raiseの第3引数に文字列の配列ではなく、caller_locationsを指定するとどうなるでしょうか。

# Ruby 3.3
def foo
  raise RuntimeError, "exception", caller_locations
    #=> in `set_backtrace': backtrace must be Array of String (TypeError)
end

foo

raiseが内部的に呼び出すException#set_backtraceの中で、「文字列の配列でなければダメ」というバリデーションがあるため、エラーになります。

ここで今回の変更です。Exception#set_backtraceThread::Backtrace::Location の配列を受け入れるようになりました。

# Ruby 3.4
def foo
  raise RuntimeError, "exception", caller_locations
end

begin
  foo
rescue
  pp $!.backtrace_locations  #=> ["test.rb:6:in '<main>'"]
end

バックトレースを切ったりほかの例外からコピーして使ったりしたいときに使うらしいです。ただ、長々と説明しましたが、バックトレースを下手に詐称するとデバッグが困難になるので、やっぱりやらないほうがいいと思います。

(mame)

Fiber Scheduler で時間のかかる処理をオフロードする仕組みが導入された

  • An optional Fiber::Scheduler#blocking_operation_wait hook allows blocking operations to be moved out of the event loop in order to reduce latency and improve multi-core processor utilization. [Feature #20876]

Fiber scheduler が blocking_operation_wait というコールバックを受けることができるようになりました。これは、時間のかかる処理で GVL を放そうとしたとき、具体的には zlib の処理などですが、このコールバックを受け取って、Fiber scheduler が別の Thread を作ってオフロードするために用意されました。

実際、使うのは難しいと思うので、フレームワークに任せるのがいいと思います。

(ko1)

IO::Buffer#copy が GVL を開放するようになった

  • IO::Buffer#copy can release the GVL, allowing other threads to run while copying data. [Feature #20902]

IO::Buffer#copy を実行中、GVL を開放して別のスレッドが実行できるようになりました。また、先ほどの blocking_operation_wait でも扱うことができます。

(ko1)

GC.config で GC の設定情報にアクセスできるようになった&世代別GCの新しい制御ができるようになった

  • GC.config added to allow setting configuration variables on the Garbage Collector. [Feature #20443]
  • GC configuration parameter rgengc_allow_full_mark introduced. When false GC will only mark young objects. Default is true. [Feature #20443]

GC は色々設定ができますが、その設定値にアクセスするための GC.config メソッドが追加されました。

$ ruby -e 'pp GC.config'
{rgengc_allow_full_mark: true, implementation: "default"}

変更できる設定は、このように変更できます。

$ ruby -e 'p GC.config(rgengc_allow_full_mark: false)'
{rgengc_allow_full_mark: false}

変更後の設定が返ります。あれ、変更前の値を返さないでいいのかな...。

現状では、rgengc_allow_full_mark という設定と、implementation の設定の2種類が取れるようになっています。今後、ますます増えていくと思います。

implementation 設定は、この記事の後半で紹介しますが、GC の実装を入れ替えることができるようにしよう、という変更が進められており、GC の実装の名前がここに入っています(もちろん読み取り専用です)。例での値は "default" ということで、これまでと同様の GC 実装である、ということがわかります。

rgengc_allow_full_mark 設定は、世代別 GC において、古い世代のオブジェクトも対象とする時間のかかるメジャー GC を許すかどうか、という設定であり、デフォルトは true になっています。

これを false にすると、古い世代のオブジェクトが多くなってもメジャー GC が起きません(より正確には、この設定が false の間、古い世代のオブジェクトを増やしません)。昔流行った OoB GC の現代版みたいなものを作るために導入されました。つまり、リクエストをうけているときは時間のかかるようなメジャー GC を禁止しておき、余裕ができたらメジャー GC もやろう、みたいな制御ができるようになりました。よく知らないけど Rails で入るんですかね(Ruby on Rails で OOB GC をデフォルトにしようぜ、って議論(Run GC out-of-band by default · Issue #50449 · rails/rails)からこの機能が入りました)。

ちょっと試してみましょう。

$ ruby -e 'GC.config(rgengc_allow_full_mark: false)
           pp GC.stat;
           a = []
           10_000_000.times{ a << [] }
           pp GC.stat'
{count: 4,
 minor_gc_count: 2,
 major_gc_count: 2,
 old_objects: 12017,
 (略)
}
{count: 17,
 minor_gc_count: 16,
 major_gc_count: 2,
 old_objects: 12017,
 (略)
}

major_gc_count、つまりメジャー GC は起こっていないことがわかります。また、old_objects の数も変わっていないため、古い世代のオブジェクトにそもそもならないことがわかります。

GC.latest_gc_info(:needs_major_by) をチェックすると、今メジャー GC が必要かどうかがわかります(truthy なら major GC をやりたがっている)。

扱いが難しい機能なので、基本的にはフレームワークに任せるのがいいと思います。

ちなみに、GC.config というメソッド自体、rgengc_allow_full_mark の設定をどうやって扱うか、という議論から、もっと設定増えそうだ、ということで、一般的な GC.config というメソッドが導入されました。

(ko1)

Hash 生成時に必要な容量を指定できるようになった

  • Hash.new now accepts an optional capacity: argument, to preallocate the hash with a given capacity. This can improve performance when building large hashes incrementally by saving on reallocation and rehashing of keys. [Feature #19236]
h = Hash.new
10_000_000.times{ h[it] = it }

10M要素のハッシュを作るとき、このプログラムでは繰り返し中にメモリ確保していきますが、最初から 10M 要素作ることが分かっていれば、最初にがばっとメモリを確保しておくと効率が良さそうです。

ということで、Hash.new(capacity: n) として、最初に n 要素分を確保するようになりました。ちょっとずつメモリ領域を確保するよりも速くなりそうです。やってみましょう。

$ time ruby -e 'n = 10_000_000; h = Hash.new; n.times{ h[it] = it }'

real    0m2.842s
user    0m2.485s
sys     0m0.294s

$ time ruby -e 'n = 10_000_000; h = Hash.new(capacity: n); n.times{ h[it] = it }'

real    0m2.122s
user    0m1.996s
sys     0m0.072s

capacity: を指定したほうが、ソコソコ速くなりました。

正直、この指定が効くようなケースはそんなに多くないと思うのですが、そういうケースがあったら使ってみてください。

(ko1)

整数の整数乗が Float::INFINITY を返さないようになった

  • Integer#** used to return Float::INFINITY when the return value is large, but now returns an Integer. If the return value is extremely large, it raises an exception. [Feature #20811]

  • Rational#** used to return Float::INFINITY or Float::NAN when the numerator of the return value is large, but now returns a Rational. If it is extremely large, it raises an exception. [Feature #20811]

整数の整数乗の結果がとても大きな数になるとき、Ruby 3.3 までは Float::INFINITY を返していましたが、Ruby 3.4 からは時間がかかっても素直に計算するようになりました。

# Ruby 3.3: "warning: in a**b, b may be too big" という警告を出しつつ Float::INFINITY を返す
p 10**10000000 #=> Infinity

# Ruby 3.4: 素直に計算する
p 10**10000000 #=> 10000000000000000000000000000.....

きっかけは、今年の10月に新たな最大の素数が発見されたというニュースでした。遠藤がRubyで計算してみようと 2**136279841-1 を実行したところ、Float::INFINITY が帰ってきてしまいました。がっかり体験。実は、6年前の最大素数の発見時にも同じがっかり体験をしており、このままでは次の最大素数の発見時にもまたがっかりしそうだったので、変えようという提案をしました。

もともとの挙動は時間のかかる計算を避けるためだったようですが、そこで INFINITY が返されてうれしい状況が想像しにくいこと、また、GMP の利用により大きい素数でもそれほど遅くならなくなったこと(自分のノート PC で 2**136279841-1 の計算は 1 秒程度、表示のための基数変換は 10 秒程度)などで、そのまま計算してよいだろうということに。

あと、Rationalの整数乗も同じように変わっています。

(mame)

MatchData#beginのバイト単位版が導入された

  • MatchData#bytebegin and MatchData#byteend have been added. [Feature #20576]

MatchData#beginのバイト単位版としてMatchData#bytebeginが導入されました。#byteendも同様です。

"あいう" =~ //

# "い" の位置は 1 文字目...2 文字目
p $~.begin(0) #=> 1
p $~.end(0)   #=> 2

# "い" の位置は 3 バイト目...6 バイト目
p $~.bytebegin(0) #=> 3
p $~.byteend(0)   #=> 6

(mame)

Object#singleton_method が extend したモジュールを見るようになった

  • Object#singleton_method now returns methods in modules prepended to or included in the receiver's singleton class. [Bug #20620]
  o = Object.new
  o.extend(Module.new{def a = 1})
  o.singleton_method(:a).call #=> 1
  # これまではエラー: singleton_method': undefined singleton method `a'

これまで、Object#singleton_method は extend されたモジュールのメソッドを返さなかったようなのですが、ちゃんと見るようになりました。

Ractor で require ができるようになった

  • require in Ractor is allowed. The requiring process will be run on the main Ractor. Ractor._require(feature) is added to run requiring process on the main Ractor. [Feature #20627]

これまで Ractor 上で require はできませんでした。というのも、

  1. $LOADED_FEATURES のような状態をどう管理するか不明
  2. main Ractor で実行することを期待している殆どすべてのライブラリの存在

などが理由です。おもちゃのようなアプリなら、Ractor を使う前にすべてのライブラリを require しておけば良いのですが、規模が大きくなると、メソッド中で require するようなもの(例えば pp メソッドが最初に実行されたとき pp ライブラリを require します)や、autoload の存在などが大きな問題になります。

そこで、別の Ractor で require すると、main Ractor で require 処理をするように処理を変更しました。

ただ、世の中には require を再定義するような けしからん さまざまなライブラリがあるため、そのような場合は自前で main Ractor であるか判断し、そうでなければ require を main Ractor で実行する Ractor._require(feature) メソッドを使ってもらうことになります。まぁ、読者の皆様はそんな require の再定義などしないと思うので、覚えておく必要はないと思います。ないよね?

で、現在動いている Ractor が main Ractor であるかを判断する便利メソッドとして、Ractor.main? が導入されました。

独自の require を定義するときは、これらを組み合わせて、次のように書くようにしてください。

  def require(feature)
    if !Ractor.main?
      Ractor._require(feature)
    else
      # オリジナルの require 処理を何かする
    end
  end

ちなみに、Kernel#require の再定義の場合は、Ractor.new を最初に行ったとき、上記のような挙動を行う無名 module が勝手に Kernel に prepend されます。

      Kernel.prepend Module.new{|m|
        m.set_temporary_name '<RactorRequire>'

        def require feature
          if Ractor.main?
            super
          else
            Ractor._require feature
          end
        end
      }

ので、例えば rubygems の定義する Kernel#require はとくに変更なしで大丈夫だと思うのですが、ほかにも prepend で拡張したり Object#require を勝手に定義する場合は、やっぱり Ractor._require を利用する必要があります。まぁ、そんなことしないよね?

(ko1)

Ractor local storage へのアクセス方法が増えた

  • Ractor.[] and Ractor.[]= are added to access the ractor local storage of the current Ractor. [Feature #20715]

Ractor ごとに用意された領域(Ractor local storage)にアクセスするため、Ractor#[key]/[key]= が用意されていましたが、実はレシーバを他の Ractor としても、自分の Ractor の local storage にアクセスしていました。警告もなく。

そこで、それならクラスメソッドでいいじゃろ、ということで Ractor.[]/[]= が導入されました。別の Ractor にアクセスできちゃうと色々まずいですからねえ。ただ、エラー処理などに使うかもしれないので、現在の Ractor#[] は仕様が曖昧なまま残りそうな気がします。

  • Ractor.store_if_absent(key){ init } is added to initialize ractor local variables in thread-safty. [Feature #20875]

Ractor local storage は、ある種のグローバル変数のような空間になります(Ractor ごとに異なるグローバル変数領域)。

さて、Ractor ごとに値を設定したいとき、Ractor の中でマルチスレッドプログラムが動いているとまずいです。

Ractor[:cnt] ||= 0
m = (Ractor[:m] ||= Mutex.new)
m.synchronize do
  Ractor[:cnt] += 1
end

このプログラムを見ると、一見問題なさそうですが、この部分をマルチスレッドで動かすと、m がそれぞれ別の Mutex を取ってしまう可能性が出てきてしまいます。また、:cnt の初期化を意図しない形で行ってしまう可能性があります。

そこで、設定を atomic に行う Ractor.store_if_absent(key){ init } というメソッドが導入されました。key に相当するキーがまだ初期化されていないとき、ブロックを実行して得られた値を、そのキーに対する値とします。これらの初期化は atomic に行われるため、他のスレッドに邪魔されることはありません。

先ほどのプログラムは、次のようにすることで Thread-safe になります。

m = Ractor.store_if_absent(:m){ Ractor[:cnt] = 0; Mutex.new }
m.synchronize do
  Ractor[:cnt] += 1
end

具体的には、とりあえず timeout.rb を Ractor 対応するために入れました。

(ko1)

each できない Range に対して Range#size が例外を投げるようになった

  • Range#size now raises TypeError if the range is not iterable. [Misc #18984]

(0.1 ... 1) のように each が呼び出せない Range に対して、Range#size が例外を投げるようになりました。

(0.1 ... 1).size  #=> 'Range#size': can't iterate from Float (TypeError)
# Ruby 3.3 までは 1 を返していた

他に、(...0) のような beginless range も同様です。

Range#step の意味が少し変わった

  • Range#step now consistently has a semantics of iterating by using + operator for all types, not only numerics. [Feature #18368]

かんたんに言えば、Time の Range に対して Range#step が使えるようになりました。

pp (Time.new(2000, 1, 1)...).step(86400).take(3)'
# => [2000-01-01 00:00:00 +0900,
#     2000-01-02 00:00:00 +0900,
#     2000-01-03 00:00:00 +0900]

Date など他の Range でもそれなりに動きます。

実際の挙動としては、Range#stepRange#begin の値に対して step の引数を "+" メソッドで足すことを繰り返し、Range#end になったらやめるようになりました。これまでは、Range#beginRange#endに入っている値に応じて作り込まれていたので、意味が整理されたと言えそうです(ただし、IntegerのRangeでは実際に"+"メソッドを呼ぶことはないとか、文字列のRangeの扱いとか、例外は残ります)。

これでTimeやDateなどを個別に対応しなくてもいい感じに動くようになりましたが、副作用として、"+" メソッドが「加算」っぽくない動きをするオブジェクトでは、若干奇妙な動きをすることになりました。注意する必要もないと思いますが、一応。

pp ([]...).step([1]).take(10)
[[],
 [1],
 [1, 1],
 [1, 1, 1],
 [1, 1, 1, 1],
 [1, 1, 1, 1, 1],
 [1, 1, 1, 1, 1, 1],
 [1, 1, 1, 1, 1, 1, 1],
 [1, 1, 1, 1, 1, 1, 1, 1],
 [1, 1, 1, 1, 1, 1, 1, 1, 1]]

(mame)

RubyVM::AbstractSyntaxTree::Node#locations が導入された

  • Add RubyVM::AbstractSyntaxTree::Node#locations method which returns location objects associated with the AST node. [Feature #20624]
  • Add RubyVM::AbstractSyntaxTree::Location class which holds location information. [Feature #20624]

RubyVM::AbstractSyntaxTree::Node のコード中の位置情報を返すメソッド #locations が導入されました。

# "1 + 1" をパースして得られるノードから位置情報を得る
loc = RubyVM::AbstractSyntaxTree.parse("1 + 1").locations.first
=> #<RubyVM::AbstractSyntaxTree::Location:@1:0-1:5>

# 1行目の0カラム目から、1行目の5カラム目までに広がっていることがわかる
p loc.first_lineno #=> 1
p loc.first_column #=> 0
p loc.last_lineno  #=> 1
p loc.last_column  #=> 5

#locations は、ノードによっては、複数の位置情報を返します。たとえば、alias foo bar というコードのノードは、全体の位置情報と、"alias"キーワードの位置情報の2つが返されるようです。Prism のノードはすでにこのような位置情報を持っているので、RubyVM::AbstractSyntaxTree::Node から Prism のノードへの変換を作るための準備ということです。

(mame)

String#append_as_bytes が導入された

  • String#append_as_bytes was added to more easily and efficiently work with binary buffers and protocols. It directly concatenate the arguments into the string without any encoding validation or conversion. [Feature #20594]

文字列をとにかく破壊的に結合していくメソッド String#append_as_bytes が導入されました。

str = "".b
str.append_as_bytes(255, "")

# "\xff" + "あ" に相当するバイト列になる
p str #=> "\xFF\xE3\x81\x82"

str << 255 << "あ" と何が違うのか? というと、こちらでは例外になるのでした。

str = "".b
str << 255 << ""
  #=> incompatible character encodings: BINARY (ASCII-8BIT) and UTF-8 (Encoding::CompatibilityError)

str はバイナリエンコーディングなのだから黙って結合されてくれよ、と思わなくもないのですが、引数の文字列 "あ" を勝手に壊れた文字列にするのは良くないということで、as_bytes という、より明示的な名前のメソッドで新規導入されることになりました。

ちなみに、String#append_as_bytes はレシーバがバイナリエンコーディングに限らず、どんなエンコーディングでもバイト列として無理やり結合します。そのユースケースはよくわかっていませんが。

(mame)

Symbol#to_s で返された文字列に破壊的変更をしようとすると deprecation warning が出るようになった

  • The string returned by Symbol#to_s now emits a deprecation warning when mutated, and will be frozen in a future version of Ruby. These warnings can be enabled with -W:deprecated or by setting Warning[:deprecated] = true. [Feature #20350]
$ ruby -we ':sym.to_s << ?a'
-e:1: warning: warning: string returned by :sym.to_s will be frozen in the future

冒頭で frozen-string-literal のデフォルト化のための仕組み(破壊的変更で警告がでる文字列)をご紹介しましたが、それを援用して、Symbol#to_s が返す文字列に対して破壊的変更を加えようとすると、deprecated 警告が有効なとき(-w-W:deprecated などで Ruby を起動したときなど)に警告が出るようになりました。

つまり、この返り値も将来 freeze したいってことなんでしょうねえ。Symbol#name はそのために入ったのに、#to_s もやらんといかんのかな。

(ko1)

Windows で Time#zone の文字列エンコーディングがちょっと変わった

  • On Windows, now Time#zone encodes the system timezone name in UTF-8 instead of the active code page, if it contains non-ASCII characters. [Bug #20929]

よくわかってないですが変わったそうです。Windowsネイティブ環境はむずかしいですね。

(mame)

Time#xmlschema が本体組み込みになった

  • Time#xmlschema, and its Time#iso8601 alias have been moved into the core Time class while previously it was an extension provided by the time gem. [Feature #20707]

time gem が提供していた Time#xmlschemaTime#iso8601 が、Ruby インタプリタ本体の組み込みになりました。

Time.new(2024, 12, 25).xmlschema #=> "2024-12-25T00:00:00+09:00"

組み込んだ理由は、C 実装のほうが速いからだそうです。それはそう。

(mame)

Warning.categories が追加された

  • Add Warning.categories method which returns a list of possible warning categories. [Feature #20293]

最近導入された警告にはカテゴリがあり、カテゴリごとに出力をオン・オフできるのですが、このカテゴリが徐々に増えてきているので、一覧を得るメソッドが導入されました。

# 廃止(deprecated)に関する警告をオンにする
Warning[:deprecated] = true

# 指定できるカテゴリ一覧を返す
Warning.categories
  #=> [:deprecated, :experimental, :performance, :strict_unused_block]

用途は主に Ruby 本体のテストです。ある種のテストを実行するときに警告を抑制したく、全カテゴリの状態を保存した上で抑制し、あとで復元するのですが、カテゴリが増えるたびにそのテストを修正するのが大変ということで。他の用途はないんじゃないかな。

(mame)

stdlib のアップデート

いろいろアップデートされていますが、NEWS 記事にコメントがかいてあるものだけ。

RubyGems で sigstore.dev に対応した

  • Add --attestation option to gem push. It enabled to store signature of build artifact to sigstore.dev.

RubyGems がソフトウェア・サプライチェーンのセキュリティ向上を目指す sigstore.dev に対応しました。 sigstore は、ソフトウェアサプライチェーンに対して自動化された署名を実現する一連の仕組みです。--attestationcosignsigstore-ruby を使って生成した Sigstore Bundle へのファイルパスを渡すと、Gem の署名を RubyGems にアップロードすることが出来ます。

(atpons)

Bundler でチェックサムの対応

  • Add a lockfile_checksums configuration to include checksums in fresh lockfiles.
  • Add bundle lock --add-checksums to add checksums to an existing lockfile.

RubyGems で Gem のチェックサムを付与することが出来るようになりました。ロックファイルを生成する時に Gem に対してチェックサムを付与し、そのロックファイルを使って bundle install を実行する時に検証することで、各々の実行環境におけるライブラリの一貫性が保証され、ライブラリの提供元の改ざんや、中間者攻撃を検知することができるようになります。

(atpons)

JSON の性能が向上

  • Performance improvements JSON.parse about 1.5 times faster than json-2.7.x.

JSON の処理がすごく速くなったそうです。 細かい話はこの辺に詳しそう。

(ko1)

ファイルを作らないTempfileが作れるようになった

  • The keyword argument anonymous: true is implemented for Tempfile.create. Tempfile.create(anonymous: true) removes the created temporary file immediately. So applications don't need to remove the file. [Feature #20497]

Tempfile.create(anonymous: true) とすると、実ファイルを作らずに一時ファイルを作れるようになりました。 正確には、O_TMPFILE が利用可能なLinuxでは実ファイルを作らず、Windowsを含む他の環境では一瞬作ってIOハンドルを得たらすぐにファイルを削除する、という実装のようです。

ユーザメリットはよくわかってないんですが、Ruby インタプリタが segfault で異常終了などしても一時ファイルが残らないとか、乱数生成したファイル名が既存ファイルと衝突してしまうという(気にしなくていいほど確率の低そうな)不幸を回避できるとかでしょうか。

(mame)

win32/sspi.rb が Ruby リポジトリから削除された

  • win32/sspi.rb

    • This library is now extracted from the Ruby repository to [ruby/net-http-sspi]. [Feature #20775]

これが何か良く知らないんですが、削除されたそうです。そもそも動いてなかったっぽい?

(ko1)

default gemがいろいろ更新された

win32-registry 0.1.0 が追加され、その他のライブラリがたくさん更新されました。

bundled gemsがいろいろ更新された

色々更新されています。

repl_type_completor が追加された

The following bundled gem is added.

  • repl_type_completor 0.1.9

RubyKaigi 2023 で発表された katakata_irb の名前が変わって、標準添付(bundled gem)になりました。IRB の補完をサポートするライブラリで、Prism と RBS を使って従来より賢い補完候補を出します。IRB のデフォルトでは正規表現で補完対象を解析するので、呼び出せないメソッドを候補に出すことがあるといった問題がありましたが、repl_type_completor によってより正確な候補が出せるようになりました。RBS を書いていなくても、組み込みクラスについては RBS が標準添付なのと解析機能自体がパワフルになっているので自動で賢くなります。 IRB では実行環境に repl_type_completor があると優先的に使われます。Bundler 環境では bundle add repl_type_completor が必要です。

(ima1zumi)

default gems -> bundled gems への移籍

The following bundled gems are promoted from default gems.

これらのライブラリが default gems から bundled gems になりました。 Gemfile を使ってアプリを開発している場合は、これらのライブラリを Gemfile に書くようにしてください。

(ko1)

互換性の問題

エラーメッセージやバックトレースがいろいろ変わった

  • Error messages and backtrace displays have been changed.
    • Use a single quote instead of a backtick as an opening quote. [Feature #16495]
    • Display a class name before a method name (only when the class has a permanent name). [Feature #19117]
    • Extra rescue/ensure frames are no longer available on the backtrace. [Feature #20275]
    • Kernel#caller, Thread::Backtrace::Location’s methods, etc. are also changed accordingly.

地味ですが、いろいろ変わりました。間違い探しです。

古い表示例:

test.rb:1:in `foo': undefined method `time' for an instance of Integer
        from test.rb:2:in `<main>'

新しい表示例:

test.rb:1:in 'Object#foo': undefined method 'time' for an instance of Integer
        from test.rb:2:in '<main>'

ひとつは、メソッド名の表示が foo から Object#foo に変わったことです。どのメソッドなのか、わかりやすくなりました(逆に冗長になった可能性もあるので、もし不満を感じることが多かったら今からでも声を上げるとよいかもしれません)。 もうひとつは、バックティックがシングルクォートに変わったことです。エラーメッセージをコピペしたら markdown で変なことになる、という体験が改善します。

この変更をやったのは自分なんですが、この程度でも意外と非互換の影響はあって、地味に大変でした。エラーメッセージやバックトレースをパースしているやんちゃなライブラリがたまにありました。たとえば irb とか test-unit とか。大多数のライブラリには影響ないと思ってますが、もしなにかあったら検討はするので気軽にご報告ください。

(mame)

Hash#inspect の結果が変わった

  • Hash#inspect rendering have been changed. [Bug #20433]

    • Symbol keys are displayed using the modern symbol key syntax: "{user: 1}"
    • Other keys now have spaces around =>: '{"user" => 1}', while previously they didn't: '{"user"=>1}'

Hash を p (inspect) したときの見た目が変わりました。

h = { user: 1 }
p h #=> Ruby 3.3 までは {:user=>1}
    #=> Ruby 3.4 からは {user: 1}

現代でこのようなハッシュを書くときにわざわざ { :user => 1 } のように書いている人はほとんどいないと思うので、それに合わせた変更です。

おそらく、frozen_string_literal デフォルト化のための警告を除けば、これが Ruby 3.4 で一番大きい非互換なのではないかと思います。テストで次のようなコードを書いていると、失敗します。

str = "result: #{ h }"

expect(str).to eq("result: {:user=>1}")

直し方は、Ruby 3.4 以降だけで動くので良ければ期待値を書き換えてしまえばいいですが、Ruby 3.3 と 3.4 の両方で動かしたいライブラリのテストなどでは、次のように書き換えると良いかもしれません。

expect(str).to eq("result: #{ { user: 1 } }")

まあ、inspect の文字列は今回のように変化することもあるので、テストの期待値にするのはあまりおすすめしません。

ちなみに、Symbol キーと非 Symbol キーが混ざっている Hash に対しては、Symbol キーのみがコロン表示になります。

h = { :sym => 1, "str" => 2 }

p h #=> {sym: 1, "str" => 2}

余談ですが、この変更に至る経緯はちょっとおもしろいです。最初は「{ :< => 1 }.inspect などが {:<=>1} という出力になっていて、eval しても元に戻らない」というバグ報告がきっかけでした。inspectの結果がevalで元に戻る保証はないのですが、{:<=>1} は見た目にもよくわからないので直したい。ということで、{:< => 1} のようにスペースを入れることが検討されました。すると {:key=>1} も合わせて {:key => 1} にすべき?となりました。{:< => 1} はほとんどの人に影響ないと思いますが、{:key => 1 } に変わるのはそれなりの非互換です。そこで引きさがる選択肢もありましたが、以前から「いつか Hash#inspect{key: 1} の形式にしたい」という話がくすぶっては消えていたことがあり、「どうせ非互換になるならここで一気にやってしまおう」という機運になり、今回の変更になりました。

(mame)

小数から文字列への変換が、小数部なし文字列を解釈するようになった

  • Kernel#Float() now accepts a decimal string with decimal part omitted. [Feature #20705]

  • String#to_f now accepts a decimal string with decimal part omitted. [Feature #20705] Note that the result changes when an exponent is specified.

要するに、"1." みたいな中途半端な小数文字列を解釈するようになりました。

Float("1.") #=> 1

以前は、Kernel#Float はこのような文字列に対して例外を投げていました。String#to_fは、解釈範囲で評価していた(上の例ではピリオド以降を無視した)ので、結果的に挙動は変わりません。

ピリオドと指数表記の間の小数部も省略できます。

Float("1.E-1") #=> 0.1

これも、以前は Kernel#Float は例外でした。String#to_f はピリオド以降を無視していたので 1 を返していましたが、Ruby 3.4 以降では 0.1 を返すので、微妙に非互換です。踏む人はいないと思いたいですが。

ちなみに 1. を小数と解釈する言語は、C 言語を始め、Python など、意外と結構あります。もちろん、Ruby 言語ではピリオドはメソッド呼び出しなので、1. を小数と解釈することはできません。

(mame)

Refinement#refined_class が削除された

  • Removed deprecated method Refinement#refined_class. [Feature #19714]

削除されました。これからは Refinement#target を使ってくださいとのことです。

(mame)

Stdlib の非互換

標準添付ライブラリにもいくつか変更があったようです。

  • DidYouMean

    • DidYouMean::SPELL_CHECKERS[]= and DidYouMean::SPELL_CHECKERS.merge! are removed.
  • Net::HTTP

    • Removed the following deprecated constants: Net::HTTP::ProxyMod Net::NetPrivate::HTTPRequest Net::HTTPInformationCode Net::HTTPSuccessCode Net::HTTPRedirectionCode Net::HTTPRetriableCode Net::HTTPClientErrorCode Net::HTTPFatalErrorCode Net::HTTPServerErrorCode Net::HTTPResponseReceiver Net::HTTPResponceReceiver

      These constants were deprecated from 2012.

  • Timeout

    • Reject negative values for Timeout.timeout. [Bug #20795]
  • URI

    • Switched default parser to RFC 3986 compliant from RFC 2396 compliant. [Bug #19266]

C API updates

  • rb_newobj and rb_newobj_of (and corresponding macros RB_NEWOBJ, RB_NEWOBJ_OF, NEWOBJ, NEWOBJ_OF) have been removed. [Feature #20265]

これらの C API が削除されました。

この C API が削除されました。これまで nop だったので、単に使っているところがあれば削除するだけでいいと思います。

そもそも、これは何をやっていたかというと、GC を走らせる前に、「このオブジェクトはもう要らないオブジェクトだ」と手動 free するものでした。ただ、これ使うのとても大変で、間違って生きているオブジェクトに対して使うとおかしなことになるし、GC に特殊処理が必要になるし、と、最近では逆にあると困る存在でした。というわけで、すっきり消せたね、という話。

(ko1)

実装の改善

Prismがデフォルトパーサになった

  • The default parser is now Prism. To use the conventional parser, use the command-line argument --parser=parse.y. [Feature #20564]

Prism が Ruby のデフォルトのパーサになりました。

ここ数年、Ruby のパーサは RubyKaigi などでなぜか非常に熱い話題ですが、そうは言ってもインタプリタ内部の話なので、普通のユーザが意識することは特にないかなと思います。強いて言えば、文法エラーのエラーメッセージが変わったくらいでしょうか。

$ ruby -e 'foo('
-e: -e:1: syntax error found (SyntaxError)
> 1 | foo(
    |     ^ unexpected end-of-input; expected a `)` to close the arguments

snippet が出るのは便利? ちなみに従来はこんな感じでした。

$ ruby --parser=parse.y -e 'foo('
-e:1: syntax error, unexpected end-of-input, expecting ')'
ruby: compile error (SyntaxError)

なお、Prism にはコーナーケースでまだ非互換がいくつか見つかっている(今後も見つかるんじゃないかな)ので、ちらほらと変わることはあるかもしれません。

(mame)

Happy Eyeballs v2 が実装された

  • Happy Eyeballs version 2 (RFC8305), an algorithm that ensures faster and more reliable connections by attempting IPv6 and IPv4 concurrently, is used in Socket.tcp and TCPSocket.new. To disable it globally, set the environment variable RUBY_TCP_NO_FAST_FALLBACK=1 or call Socket.tcp_fast_fallback=false. Or to disable it on a per-method basis, use the keyword argument fast_fallback: false. [Feature #20108] [Feature #20782]

TCPSocket.newSocket.tcp が、デフォルトで Happy Eyeballs v2 に基づいて接続するようになりました。Happy Eyeballs v2 とは、指定ホストに対して IPv4 と IPv6 の両方に同時並行で接続しにいってみて、先に応答があったほうで接続を確立する、という仕組みです。

従来は、IPv6 や IPv4 に逐次で接続を試みていました。この方法だと、何らかの理由で IPv6 の通信に問題がある環境で先に IPv6 で接続をしようとすると、それがタイムアウトするまで IPv4 の接続試行に移らないので、とても時間がかかっていました。Ruby 3.4 は IPv6 と IPv4 に同時並行で接続するので、IPv6 がダメでもすぐに IPv4 が反応してくれて、タイムアウトを待つことなく接続確立するはずです。

まあ実際のところ、ユーザメリットよりは、社会が IPv4 から IPv6 に徐々に移行していくために必要な移行措置という印象が個人的には強いです。Happy Eyeballs は社会的責任。Ruby は社会的責任を果たしていてえらい。

Happy Eyeballs は同時並行に接続するため、わずかにオーバーヘッドがあるようです。もしどうしても Happy Eyeballs を止めたかったら、Socket.tcp_fast_fallback = false とするか、または環境変数で RUBY_TCP_NO_FAST_FALLBACK=1 としてください。オーバーヘッド以外に、接続トラブル発生時の問題切り分けに役立つこともあるかも。

(mame)

GC の実装を切り替えられる仕組みが導入された

  • Alternative garbage collector (GC) implementations can be loaded dynamically through the modular garbage collector feature. To enable this feature, configure Ruby with --with-modular-gc at build time. GC libraries can be loaded at runtime using the environment variable RUBY_GC_LIBRARY. [Feature #20351]

Ruby のビルド時の configure--with-modular-gc を指定することで、GC 実装を Ruby プロセス起動時に切り替えられる Ruby をビルドできるようになりました。RUBY_GC_LIBRARY 環境変数で変更することができます。

デフォルトではこのオプションを指定してビルドされないため、今のところ、GC の実装を差し替える実験のために導入されたと言っていいと思います。

  • Ruby's built-in garbage collector has been split into a separate file at gc/default/default.c and interacts with Ruby using an API defined in gc/gc_impl.h. The built-in garbage collector can now also be built as a library using make modular-gc MODULAR_GC=default and enabled using the environment variable RUBY_GC_LIBRARY=default. [Feature #20470]

で、GC の実装を色々変えられるように、GC 関連のディレクトリ構成が変更されました。

  • An experimental GC library is provided based on MMTk. This GC library can be built using make modular-gc MODULAR_GC=mmtk and enabled using the environment variable RUBY_GC_LIBRARY=mmtk. This requires the Rust toolchain on the build machine. [Feature #20860]

で、その GC 実装を変更する機能を使って、MMTk という、GC の実装をたくさん集めたものを使う GC 実装が実験的に提供されました。MMTk には研究の粋が詰まった GC 実装が入っているのですが、CRuby の制限から、現在サポートされているのはごくシンプルなもののみで、今後発展していくとのことです。Rust で書いてあるんですね。

(ko1)

YJIT

New features

  • Add unified memory limit via --yjit-mem-size command-line option (default 128MiB) which tracks total YJIT memory usage and is more intuitive than the old --yjit-exec-mem-size.
  • More statistics now always available via RubyVM::YJIT.runtime_stats
  • Add compilation log to track what gets compiled via --yjit-log
    • Tail of the log also available at run-time via RubyVM::YJIT.log
  • Add support for shareable consts in multi-ractor mode
  • Can now trace counted exits with --yjit-trace-exits=COUNTER

New optimizations

  • Compressed context reduces memory needed to store YJIT metadata
  • Improved allocator with ability to allocate registers for local variables
  • When YJIT is enabled, use more Core primitives written in Ruby:
    • Array#each, Array#select, Array#map rewritten in Ruby for better performance [Feature #20182].
  • Ability to inline small/trivial methods such as:
    • Empty methods
    • Methods returning a constant
    • Methods returning self
    • Methods directly returning an argument
  • Specialized codegen for many more runtime methods
  • Optimize String#getbyte, String#setbyte and other string methods
  • Optimize bitwise operations to speed up low-level bit/byte manipulation
  • Various other incremental optimizations

YJIT もいろいろ良くなったようです。多分解説が出るだろうからここではスキップ。

(ko1)

Miscellaneous changes

ブロックを使わないメソッドにブロックを渡すと警告する機能が付いた

  • Passing a block to a method which doesn't use the passed block will show a warning on verbose mode (-w). [Feature #15554]

ブロックを利用しないメソッドにブロックを渡すと、-w を指定していると警告を出すようになりました。

def f = nil
f{}
#=> warning: the block passed to 'Object#f' defined at t.rb:1 may be ignored

ただし、これを素直に実行すると、例えば次のような duck typing で意図して無駄にブロックを渡している場合でも警告が出てしまい、それが結構あることがわかりました。

class C
  def f = yield # こっちはブロックを使う
end

class D
  def f = nil # こっちはブロックを使わない
end

[C.new, D.new].each{ it.f{} }
#=> f というメソッド名でブロックを利用するものがあるので、警告を出さない

そこで、このように、「同名のメソッドでブロックを使うものがある場合」は警告をださないようになりました(-w でも警告は出ない)。

条件が緩くなってしまうので、それが嫌な場合は -W:strict_unused_block をつけて実行してみてください。

$ ./miniruby -W:strict_unused_block -e '
class C
  def f = yield # こっちはブロックを使う
end

class D
  def f = nil # こっちはブロックを使わない
end

[C.new, D.new].each{ it.f{} }
'
-e:10: warning: the block passed to 'D#f' defined at -e:7 may be ignored

警告が出るようになります。

実際、弊社のアプリで試してみると、rspec のテストで、1件だけブロックを渡すべきではないところで渡している例が見つかりました。strict 版でなければ見つからなかったので、たくさん false positive が出るのを覚悟して試してみるといいかもしれません。

(ko1)

再定義するとは思わないようなメソッドを再定義するようなけしからん操作に警告が出るようになった

  • Redefining some core methods that are specially optimized by the interpreter and JIT like String#freeze or Integer#+ now emits a performance class warning (-W:performance or Warning[:performance] = true). [Feature #20429]

String#freezeInteger#+ など、まぁ再定義されんよな、というメソッドが再定義された場合、インタプリタの性能上悪い影響が出ます(再定義されないことを前提に最適化がされているため)。そこで、そういうのが再定義されたら -W:performance を指定していると、警告が出るようになりました。

(ko1)

おわりに

Ruby 3.4 の新機能や改善を紹介してきました。ここで紹介した以外でも、バグの修正や細かな改善が行われています。お手元の Ruby アプリケーションでご確認いただければと思います。

Ruby 3.4 は目立つ新機能はないのですが、Prism にパーサが代わったり、it という新しい文法が導入されたりと、たくさんの実装の改善が行われています。また、frozen-string-literal のデフォルト化など、今後の Ruby の展開に大きな影響を与える決断もありました。ぜひ、お手元にセットアップして新しい Ruby を楽しんでください。

Enjoy Ruby programming!

(ko1/mame, guest: ima1zumi, atpons)