
テクノロジー部門技術推進グループの笹田(ko1)と遠藤(mame)です。Ruby (MRI: Matz Ruby Implementation、いわゆる ruby コマンド) の開発をしています。お金をもらって Ruby を開発しているのでプロの Ruby コミッタです。
本日 12/25 に、恒例のクリスマスリリースとして、Ruby 4.0.0 がリリースされました(Ruby 4.0.0 リリース | Ruby)。今年も STORES Product Blog にて Ruby 4.0 の NEWS.md ファイルの解説をします(ちなみに、STORES Advent Calendar 2025 の記事になります。他も読んでね)。NEWS ファイルとは何か、は以前の記事を見てください。
- プロと読み解く Ruby 2.6 NEWS ファイル - クックパッド開発者ブログ
- プロと読み解くRuby 2.7 NEWS - クックパッド開発者ブログ
- プロと読み解く Ruby 3.0 NEWS - クックパッド開発者ブログ
- プロと読み解く Ruby 3.1 NEWS - クックパッド開発者ブログ
- プロと読み解く Ruby 3.2 NEWS - クックパッド開発者ブログ
- プロと読み解くRuby 3.3 NEWS - STORES Product Blog
- プロと読み解くRuby 3.4 NEWS - STORES Product Blog
本記事は新機能を解説することもさることながら、変更が入った背景や苦労などの裏話も記憶の範囲で書いているところが特徴です。
Ruby は 1995 年に最初のリリースが発表されたので、今年はリリース30周年という記念の年になります。 というわけで、それを記念しての Ruby 4.0.0 というリリースになりました。
Ruby 4.0 の代表的な変更は次のようなものになります(リリースノートから抜粋)。
- (言語仕様)論理二項演算子を行頭に置けるようになった
- Ruby Box の試験的な導入
- ZJIT の試験的な導入
- Ractor の改善(まだ試験的機能)
(だいたい試験的だな?)
本記事では、これらを含めて NEWS ファイルにあるものをだいたい紹介していきます。
言語の変更
foo(*nil) と呼んだ時、nil.to_a を呼ばなくなった
*nilno longer callsnil.to_a, similar to how**nildoes not callnil.to_hash. [Feature #21047]
知らなかったんですが、 foo(*nil) とすると、nil.to_a が呼ばれていたそうです。そして、それは [] を返すので、結局引数がないことになります。
Ruby 3.3 で導入されたfoo(**nil) では、nil.to_hash は呼ばれないので、nil.to_a も呼ばないほうがいいのではないか、という提案がされ、無事アクセプトされました。メソッド呼び出し1回分、および空配列1個分の生成がなくなり、性能もよかろうということです。
p nil.to_a #=> [] def nil.to_a p :to_a [] end p(*nil) # Ruby 3.4 # => :to_a # Ruby 4.0 # => 何も表示されない
非互換といえば非互換ですが、これに依存するようなコードはないと信じたい。
(ko1)
論理演算子の前で改行できるようになった
- Logical binary operators (
||,&&,andandor) at the beginning of a line continue the previous line, like fluent dot. [Feature #20925]
if 文の条件式が複数行になったら、条件式と if 文の中身の区別がつきにくいという問題がありました。
if cond1 && cond2 # 条件式の一部 body1 # if 内の最初の文 body2 end
このコードは、cond2 が条件式の一部、 body1 が if 文の中身の最初の文となっていますが、インデントが同じなので区別がむずかしいですね。
cond2 を深くインデントしたり、 cond2 と body1 の間に空行を置いたり、 then を挟んだり、わかりやすくする工夫がありましたが、いずれも座りの悪いものでした。
# then を使ってみた if cond1 && cond2 then body1 body2 end
そこで Ruby 4.0.0 では、&& や || の前に空行を置くことができるようになりました。
if cond1 && cond2 body1 body2 end
&& cond2 なら、条件式の一部であることが明白なので、問題が解決します。
……ということなのですが、どうでしょうね? 座りの悪い書き方が 1 つ増えただけのような気もするような、慣れたら便利なような。
裏話
この変更の議論は結構盛り上がりました。
Ruby の式が次の行に続くかどうかは、原則として前の行の行末で判断できるのですが、メソッド呼び出しのドットが例外でした。
ary # ここで式として完結できるが .map { it.to_s } # 次の行がドットで始まるので 1 つの式として続く .select { it.size > 3 } # 複数行にわたるメソッドチェーンで便利
ary は単独で完結している式ですが、次の行が .map から始まっているので行継続します。今回はこの例外が && と || と and と or に広がった形です。
しかしこの例外を広げると、他の二項演算子でも行継続したくなるのでは? 例えば x + y の + の前で改行したくなるのでは? しかしそれは互換性の問題があるのでできない( + y は単項演算子の + として解釈できるので)。Ruby の文法がどこで改行できるかわかりにくくなるリスクを負ってまで && の前で改行することを許す価値があるのか?
という感じで抵抗を感じる人も少なくありませんでしたが、最終的に Matz が「やる」といったのでやることになりました。
余談ですが、個人的には && cond2 はインデントせず、 if cond1 と並列に書くのがかっこいいと思いました。
if cond1 && cond2 body1 body2 end
が、この記法の賛同者は全然いない(Ruby 開発者会議では「話にならない」と鼻であしらわれた)ので悲しい。
(mame)
組み込みクラスのアップデート
Array#rfind が導入された
Array#rfindhas been added as a more efficient alternative toarray.reverse_each.find[Feature #21678]Array#findhas been added as a more efficient override ofEnumerable#find[Feature #21678]
条件に合う要素の中で最後のものを探す Array#rfind メソッドが導入されました。最後の要素から逆順にイテレートして、条件に合う要素で最初に見つかったものを返します。
# 2 ではなく 4 を返す [1, 2, 3, 4, 5].rfind { it.even? } #=> 4
また、ついでに Array#find も導入されたようです。もともと Enumerable#find があったのですが、Array に特化したメソッドを定義することで高速になります。
(mame)
Binding#implicit_parameters が導入された
Binding#implicit_parameters,Binding#implicit_parameter_get, andBinding#implicit_parameter_defined?have been added to access numbered parameters and "it" parameter. [Bug #21049]
numbered parameter や暗黙的な it などを Binding から読み出すメタプログラミング API が導入されました。
["foo"].each do p it #=> "foo" # 暗黙的な引数 it を binding から読む p binding.implicit_parameters #=> [:it] p binding.implicit_parameter_get(:it) #=> "foo" end { foo: 42 }.each do p _1 #=> :foo p _2 #=> 42 # 暗黙的な引数 _1 と _2 を binding から読む p binding.implicit_parameters #=> [:_1, :_2] p binding.implicit_parameter_get(:_1) #=> :foo p binding.implicit_parameter_get(:_2) #=> 42 end
デバッガで _1 や it の内容を表示するために必要ということで導入されたものなので、普通は使う必要がないと思います。使わないでください。
Binding#local_variablesdoes no longer include numbered parameters. Also,Binding#local_variable_get,Binding#local_variable_set, andBinding#local_variable_defined?reject to handle numbered parameters. [Bug #21049]
なお、Ruby 3.4 までは binding.local_variable_get(:_1) で numbered parameter が読めていたのですが、Ruby 4.0 では NameError になるのでご注意ください。
この変更の理由は次のとおりです。Ruby 3.4 で導入された暗黙的な it は普通のローカル変数名としても使えるため、 binding.local_variable_get(:it) がどちらを指しているのか曖昧になってしまいます。it を禁止したのに合わせて _1 も禁止になりました。
そもそも numbered parameter や暗黙的な it は普通のローカル変数とはスコープの規則が異なるので、 Binding#local_variables などで一緒くたに扱うのがよくないのでは?という気づきもあり、implicit_parameters という別のメソッドで扱うことになりました。
(mame)
Enumerator.produce で size キーワードが指定できるようになった
Enumerator.producenow accepts an optionalsizekeyword argument to specify the size of the enumerator. It can be an integer,Float::INFINITY, a callable object (such as a lambda), ornilto indicate unknown size. When not specified, the size defaults toFloat::INFINITY. [Feature #21701]
Enumerator.produce に size というキーワードが追加されました。
Enumerator.produce は基本的に無限列の Enumerator を作るメソッドです。
# (1..) 相当の Enumerator enum = Enumerator.produce(1) {|n| n + 1 } p enum.take(5) #=> [1, 2, 3, 4, 5] p enum.size #=> Float::INFINITY
あまり知られていない技として、StopIteration 例外を投げることで produce を止めることができます。
# (1..10) 相当の Enumerator enum = Enumerator.produce(1) do |n| raise StopIteration if n > 10 n + 1 end p enum.to_a #=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] p enum.size #=> Float::INFINITY (?)
このとき、enum.size が Float::INFINITY なのはいまいちですね。ということで、size キーワードで長さを明示的に指定できるようになりました。
# (1..10) 相当の Enumerator enum = Enumerator.produce(1, size: 10) do |n| raise StopIteration if n > 10 n + 1 end p enum.to_a #=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] p enum.size #=> 10
ただ、この size キーワードを指定しない限り produce の返した Enumerator のサイズは Float::INFINITY と嘘をつきますし、size キーワードに嘘の値を指定したときも嘘をつくことにご注意ください。要するに、Enumerator#size の値はあまり信用しないほうがよさそうです。
(mame)
error_highlight で ArgumentError の表示が親切になった
- When an
ArgumentErroris raised, it now displays code snippets for both the method call (caller) and the method definition (callee). [Feature #21543]
次のようなコードを考えます。
def add(x, y) = x + y add(1)
add の引数の数が間違っているので、エラーが出ます。このとき、caller(呼び出し側)と callee (呼び出し先)のコードスニペットが出るようになりました。
# Ruby 4.0 の場合
$ ruby test.rb
test.rb:1:in 'Object#add': wrong number of arguments (given 1, expected 2) (ArgumentError)
caller: test.rb:3
| add(1)
^^^
callee: test.rb:1
| def add(x, y) = x + y
^^^
from test.rb:3:in '<main>'
Ruby 3.4 のときは、次のようなシンプルな表示でした。
# Ruby 4.0 の場合
$ ruby test.rb
test.rb:1:in 'add': wrong number of arguments (given 1, expected 2) (ArgumentError)
from test.rb:3:in '<main>'
このエラーを見たら、みんな反射的に test.rb:1 のソースを見ますよね。しかし、wrong number of arguments が出たときにメソッド定義側を修正することってほとんどないと思います。エラーを再度見て、test.rb:3 のソースを開き直すことになります。
みんながこの作業を繰り返しているのは無駄だと思ったので、スタックトレースの改善を提案して議論した結果、error_highlight で上記のように表示してみることになりました。
(mame)
Fiber::Scheduler についての変更
- Introduce
Fiber::Scheduler#fiber_interruptto interrupt a fiber with a given exception. The initial use case is to interrupt a fiber that is waiting on a blocking IO operation when the IO operation is closed. [Feature #21166]
Fiber::Scheduler#fiber_interrupt というメソッドが追加されました。
だいぶ具体的な話になってしまうのですが、利用シナリオを記述します。
- スレッド A がある
ioに対してio.readしているとき、スレッド B がio.closeしようとすると、A が無限に待ってしまうことがあるため(OSによります)、io.closeはそれに対してブロックしているスレッド一覧(この場合スレッドA)に対して例外を起こします。 - Fiber scheduler でも同じことをできるようにしたい、というリクエストがあり、それを実現するために
Fiber::Scheduler#fiber_interruptが導入されました。
実際のコードは、よく知らないので書けない…。
- Introduce
Fiber::Scheduler#yieldto allow the fiber scheduler to continue processing when signal exceptions are disabled. [Bug #21633]
Fiber::Scheduler#yield が導入されました(あれ、いつの間に...)。
シグナルなどを受けた後、もし通常のフローで受け取ることができなかったら、他のスケジュール可能な Fiber をスケジュールする、というものです。いいのかなぁ。
- Reintroduce the
Fiber::Scheduler#io_closehook for asynchronousIO#close.
Fiber::Scheduler#io_close が再度導入されました。とありますが、参照がないので何もわからない。
- Invoke
Fiber::Scheduler#io_writewhen flushing the IO write buffer. [Bug #21789]
IO#close や IO#fush を行うと、書き込みバッファにたまっていたデータをファイルに出力するのですが、これまで Fiber scheduler を気にしないで出力を行っていたのですが、これを Fiber scheduler 向けの io_write フックによって行うようにした、というものです。
(ko1)
File::Stat#birthtime が Linux でとれるようになった
File::Stat#birthtimeis now available on Linux via the statx system call when supported by the kernel and filesystem. [Feature #21205]
Linux でファイルの作成日時が取れるようになったとのことです。
statx(2) システムコールが利用可能な環境で、かつファイルシステムに作成日時が記録されている場合でのみ利用可能ですが、現代的な Linux ではだいたい大丈夫なのではないかと思います(が、詳しい条件は各自で調べて欲しい)。
(mame)
IO.select が timeout 値として Float::INFINITY を受け取れるようになった
IO.selectacceptsFloat::INFINITYas a timeout argument. [Feature #20610]
IO.select は複数の I/O について、ready(read してもブロックしない、などの状態)になるのを待つための API です。これにはタイムアウトが指定でき、nil (もしくは無指定)では無限に待つ、という挙動でした。この無限に待つタイムアウトとして、Float::INFINITY も指定できるようになった、という変更です。
(ko1)
オブジェクトの inspect で表示するインスタンス変数を選べるようになった
Kernel#inspectnow checks for the existence of a#instance_variables_to_inspectmethod, allowing control over which instance variables are displayed in the#inspectstring [Feature #21219]
オブジェクトを何気なく p したとき、思いがけず大量の出力が出て面食らったことがあると思います。
class Foo def initialize(id) @id = id @internal_data = "a" * 1000 end end p Foo.new("foo") #=> #<Foo:0x00007c495d699dd0 @internal_data="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...
@id は表示してほしいけれど @internal_data は見たくない、というとき、表示するインスタンス変数を指定できる機能が導入されました。次のように instance_variables_to_inspect メソッドを追加すると、 @id だけ表示されます。
class Foo def initialize(id) @id = id @internal_data = "a" * 1000 end # 表示したいインスタンス変数名をシンボルで列挙する private def instance_variables_to_inspect = [:@id] end p Foo.new("foo") #=> #<Foo:0x00007692c1a44f78 @id="foo">
すっきりしますね。
パスワード文字列のように気軽にログに出してほしくないものを除外するためにも使えます(というより、そちらのほうが重要なユースケースかも)。たとえば @password を除外したければ、次のようにしておくとよいでしょう。
class LoginInfo def initialize(user) @user = user @password = "secret" end # Kernel#instance_variable から表示したくないインスタンス変数名を除去して返す private def instance_variables_to_inspect = instance_variables - [:@password] end # @password は表示されない p LoginInfo.new("mame") #=> #<LoginInfo:0x000072b3b2426390 @user="mame">
仕組みとしては、 Kernel#inspect メソッドがオブジェクトの文字列化をする際、すべてのインスタンス変数を無条件で表示するのではなく、instance_variables_to_inspect を呼んで表示すべきインスタンス変数を特定するようになっています。
ちなみに、pp には以前から pretty_print_instance_variables という同様の仕組みがありました。これが p にも導入された形ですが、pretty_print_instance_variables という名前のまま導入するのはあんまりなので、instance_variables_to_inspect という名前になりました。例によって、このメソッド名で落ち着くまでが一番大変でした(Ruby では名前の議論がとても重要)。
(mame)
open(”| ls”) でプロセス起動できなくなった
- Kernel
- A deprecated behavior, process creation by
Kernel#openwith a leading|, was removed. [Feature #19630]
- A deprecated behavior, process creation by
- IO
- A deprecated behavior, process creation by
IOclass methods with a leading|, was removed. [Feature #19630]
- A deprecated behavior, process creation by
Ruby の Kernel#open はファイルを開くメソッドですが、ファイル名に "| ls" のようにパイプ記号で始まる文字列を与えると、プロセスを起動してその標準入出力をファイル的に扱える機能がありました。
# ls /home を実行し、その出力を read で得て出力する(Ruby 3.4 まで) open("| ls /home") { puts f.read } #=> Ruby 3.4: mame #=> Ruby 4.0: No such file or directory @ rb_sysopen - | ls /home (Errno::ENOENT)
しかしこの機能を知らないユーザが多く、脆弱性の原因になりがちだということで、2 年前の Ruby 3.3 のときに非推奨となり、このたびついに削除となりました。
drop-in replacement は用意されていないので、もしこの機能を意図的に使っていた人がいたら、IO.popen 等を使って適切に書き換えてください。おおよそ open("|command") を IO.popen("command") に置き換えればうまくいくと思います(が、そもそも危険な機能なので、慎重にご検討ください)。
(mame)
Math.log1p と Math.expm1 が導入された
Math.log1pandMath.expm1are added. [Feature #21527]
Math.log1p(x) とは、 の値を返すメソッドです。
「なんでこんな関数があるの? 素直に Math.log(x + 1.0) と書けばいいのでは?」と思うかも知れませんが、浮動小数点数の誤差を防ぐために必要なことがあるそうです。
というのも、 x がとても小さい( Float::EPSILON より小さい)とき、 x + 1.0 は丸められて 1.0 になってしまうので、 Math.log(x + 1.0) は常に 0 を返すことになります。
Math.log1p(x) はこのような小さい x に対しても動きます。
# とても小さい x x = 1.0e-16 p Math.log(x + 1) #=> 0.0 p Math.log1p(x) #=> 1.0e-16
まあ、 は
の付近では
なので、
Math.log1p は引数をそのまま返すだけなのですが。
Math.expm1 は の値を返すメソッドで、その必要性もほとんど同じ話です。
提案のきっかけは、自分が趣味で数値計算の論文を実装していたとき、log1p が無いことに気づいたことでした。C 言語が C99 の時代から提供している関数が欠けているのに誰も気づかなかったとは。
導入の議論では「 log1p と expm1 は名前が意味不明」というツッコミが入りましたが、調べた限り Python、Java、JavaScript、Go、PHP、.NET、R、MATLAB、Kotlin、Swift、Julia、Haskell、OCaml、Crystal がこの名前で導入していた(Rust は ln_1p という更に暗号めいた独自路線だった)ということで、そのままで良しとなりました。
(mame)
Pathname が組み込みクラスになった
- Pathname has been promoted from a default gem to a core class of Ruby. [Feature #17473]
Pathname クラスが組み込みになりました。 require "pathname" と書かなくても Pathname が使えます。
といっても Rails ではデフォルトで require "pathname" されているので、ほとんどのユーザにとっては今更というか、あまり影響のあるものではないでしょう。Ruby で書き捨てスクリプトやワンライナーをよく書く人にとっては、地味にうれしいかもしれません。
注意点として、次の 3 つのメソッドを使うには require "pathname" が必要です。
Pathname#findPathname#rmtreePathname#mktmpdir
なぜかというと、これらのメソッドは別のライブラリに依存しているためです。 Pathname を組み込むのに find 、 fileutils 、 tmpdir ライブラリまで組み込むのはちょっとやり過ぎだろうということで、これらだけ組み込みになりませんでした。
余談ですが、 Pathname の組み込みに際して、とても奇妙なバグが発生しました。そのデバッグの仮定は別の記事で詳説しているので、興味あればご覧ください。
(mame)
Proc#parameters の暗黙の it の返り値がちょっと変わった
Proc#parametersnow shows anonymous optional parameters as[:opt]instead of[:opt, nil], making the output consistent with when the anonymous parameter is required. [Bug #20974]
Proc#parameters は、そのブロックがどんな引数を持つかを調べるためのメソッドです。
p proc{}.parameters #=> [] p proc{|a|}.parameters #=> [[:opt, :a]] p proc{|a, b|}.parameters #=> [[:opt, :a], [:opt, :b]] p proc{|a, b, c=1|}.parameters #=> [[:opt, :a], [:opt, :b], [:opt, :c]] p proc{|a, b, c=1, *d|}.parameters #=> [[:opt, :a], [:opt, :b], [:opt, :c], [:rest, :d]] p proc{|a, b, c=1, *d, e|}.parameters #=> [[:opt, :a], [:opt, :b], [:opt, :c], [:rest, :d], [:opt, :e]] p lambda{}.parameters #=> [] p lambda{|a|}.parameters #=> [[:req, :a]] p lambda{|a, b|}.parameters #=> [[:req, :a], [:req, :b]] p lambda{|a, b, c=1|}.parameters #=> [[:req, :a], [:req, :b], [:req, :c]] p lambda{|a, b, c=1, *d|}.parameters #=> [[:req, :a], [:req, :b], [:req, :c], [:rest, :d]] p lambda{|a, b, c=1, *d, e|}.parameters #=> [[:req, :a], [:req, :b], [:req, :c], [:rest, :d], [:req, :e]]
すべて proc{|a|} などで [:opt, :a] (つまり optional 引数)なのは、ブロックの引数は、渡しても渡さなくてもいいからですね。lambda{|a|} だと、ちゃんと1引数渡さないとエラーなので、[:req, :a] になります。
さて、Ruby 3.4 で導入された it を使ったブロックでは、1 引数を受け取る proc{|a|} の省略になるので、1つの optional 引数を受け取るという意味になります。ただし、引数に it というものを指定できるので(proc{|it|p it}.parameters #⇒ [[:opt, :it]])、そのまま引数名として it を返すと、どっちかよくわからなくなります。そこで、これまでは、名前を nil にしていました。
ただ、procではなく lambda の場合は required parameter (必須引数)になりますが、その場合は単に [:req] だけだったんですよね。で、どっちかに揃えようって話になって、[:opt] にするように揃えました。
p lambda{p it}.parameters #=> [[:req]] p proc{p it}.parameters #=> Ruby 3.4: [[:opt, nil]] #=> Ruby 4.0: [[:opt]]
ちなみに、numbered parameters の場合は、変わらず、変数名に :_1 などが出ます。今は _1 が引数名(というか変数名)に利用できないので、紛らわしくないためですね。
p proc{p [_1, _2]}.parameters #=> [[:opt, :_1], [:opt, :_2]] p lambda{p [_1, _2]}.parameters #=> [[:req, :_1], [:req, :_2]]
(ko1)
Ractor間の通信を整理するため、Ractor::Port が導入された
Ractor::Portclass was added for a new synchronization mechanism to communicate between Ractors. [Feature #21262]Ractor::Portprovides the following methods:Ractor::Port#receiveRactor::Port#send(orRactor::Port#<<)Ractor::Port#closeRactor::Port#closed?As result,Ractor.yieldandRactor#takewere removed.
Ractor#joinandRactor#valuewere added to wait for the termination of a Ractor. These are similar toThread#joinandThread#value.Ractor#monitorandRactor#unmonitorwere added as low-level interfaces used internally to implementRactor#join.Ractor.selectnow only accepts Ractors and Ports. If Ractors are given, it returns when a Ractor terminates.Ractor#default_portwas added. EachRactorhas a default port, which is used byRactor.send,Ractor.receive.Ractor#close_incomingandRactor#close_outgoingwere removed.Ractor 間の通信について、
Ractor::Portという新しいもので整理しなおしました。これについては次の記事にまとまってますので、詳細はそちらを読んでみてください。
NEWS にある例ですが、こんな書き方ができるようになりました。
port1 = Ractor::Port.new port2 = Ractor::Port.new Ractor.new port1, port2 do |port1, port2| port1 << 1 port2 << 11 port1 << 2 port2 << 12 end 2.times{ p port1.receive } #=> 1, 2 2.times{ p port2.receive } #=> 11, 12
また、ある Ractor の終了を待つには、join もしくは value を使うようになりました。
r = Ractor.new{ task } r.join # task 終了をまつ p r.value # task 終了を待ち、task の返り値を得る。だいたいスレッドと同じ
終了待ちに使っていた Ractor#take がなくなったのは、大きな変更かも(Ractor 自体が experimental feature なので気軽に変更できる)。スレッドの API に近づけました。
(ko1)
Ractor 間で共有可能な Proc を作るための Ractor.sharable_proc の導入
Ractor.shareable_procandRactor.shareable_lambdais introduced to make shareable Proc or lambda. [Feature #21550], [Feature #21557]
Proc を Ractor 間で共有できるようにするには、いくつかの制約があります。一番厳しい制約は、そのブロックを実行しているときの self が shareable である、ということです(他の制約は下記)。これまで、Ractor.make_shareable(proc_obj) を利用することで、self が shareable であり、他の制約を満たしているときに限り、proc_obj を shareable とすることができました。
pr = proc{} Ractor.make_shareable(pr) #=> 'Ractor.make_shareable': Proc's self is not shareable: #<Proc:...> (Ractor::IsolationError) # main が shareable ではないため
この self を shareable にするというのが面倒で、nil.instance_exec{ proc{ … } } みたいなことをしないといけなくてダサかったのですが、それを解決するためのメソッドとして Ractor.shareable_proc が導入されました(同じく shareable_lambda は lambda を作る)。このメソッドは、与えられたブロックを shareable Proc として返します。
pr = Ractor.shareable_proc{ } p Ractor.shareable?(pr) #=> true
この時、無指定だと、self は nil になります。変更するには、self: キーワードを使います。
pr = Ractor.shareable_proc(self: Ractor){ self } p pr.call #=> Ractor
多くの場合 self: を利用しないようなケースで利用されると思うので、self: を指定しないといけないケースは少ないのではないでしょうか。
Ractor 間で共有する Proc は、たとえばタスクを渡すなどという用途で利用したいことが多そうです。
worker = Ractor.new do while task = Ractor.receive task.call end end def heavy_task = sleep(3) # 3 つの heavy_task を Ractor に任せる worker << Ractor.shareable_proc{ heavy_task } worker << Ractor.shareable_proc{ heavy_task } worker << Ractor.shareable_proc{ heavy_task }
そのため、shareable な Proc が作りづらいなぁ、と思っていたのですが、この API によって、この懸念が解決してよかったです。
補足:Proc が shareable になることができる条件:
- (dynamic)
selfが shareable であること - (static) ブロックの外側のローカル変数に書き込みを行わないこと
- (dynamic) ブロックの外側のローカル変数から読み込みを行う場合、そのローカル変数には shareable object が格納されていること。読み込みを行うローカル変数一覧は static に取得。
- (static) ブロックの外側のローカル変数から読み込みを行う場合、そのローカル変数には再代入されないこと。
ここで static, dynamic と言っているのは、static はコンパイル時に調査する内容、dynamic は、実行時(Proc 生成時)に調査する内容になります。なお、shareable Proc 内で eval する場合は、この条件が満たされることをチェックします。
class C a = 1 pr = proc{a = 2} Ractor.make_shareable(pr) #=> 'Ractor.make_shareable': can not make a Proc shareable because it accesses outer variables (a). (ArgumentError) # 外側のローカル変数 a に書き込みを行っているため、エラー end
4 が難しいのでもう少し補足します。
a = 1 pr = Ractor.shareable_proc(self: Ractor){ p a } #=> cannot make a shareable Proc because the outer variable 'a' may be reassigned. (Ractor::IsolationError) a = 2
この場合、Proc から参照される a は、ブロックの外側で再代入が行われています。このチェックが必要なのは、「ふつうの Proc」と意味をなるべく近づけるためです。というのも、shareable Proc は、そのタイミングでの変数の値、この場合 a に対する 1 を記録してしまいます。そのため、外側で a = 2 としても、常に shareable Proc の中では a は 1 が返ります。これは、ふつうの Proc の挙動とは異なるため、Ruby 4.0 からはこういうケースもエラーにしておこう、となりました。
(ko1)
Range#to_set が長さをチェックするようになった
Range#to_setnow performs size checks to prevent issues with endless ranges. [Bug #21654]
(0..).to_set としたとき、Ruby 3.4 までは無限サイズの Set を作ろうとして固まっていましたが、Ruby 4.0 では例外を投げるようになりました。
(0..).to_set #=> Ruby 3.4: メモリを食いつぶすまで終わらない #=> Ruby 4.0: cannot convert endless range to a set (RangeError)
(mame)
Range#overlap? の細かい挙動が変わった
Range#overlap?now correctly handles infinite (unbounded) ranges. [Bug #21185]
(..3).overlap?(nil..nil) の返り値が false から true に変わったそうです。
(mame)
Range#max の細かい挙動が変わった
Range#maxbehavior on beginless integer ranges has been fixed. [Bug #21174] [Bug #21175]
(..10).max(2) は [10, 9] を返すようになりました。(1..10).max(2) は以前から [10, 9] を返すので、それに合わせるとのことです。
とはいえ、 (1.0 .. 10).max(2) は例外を投げるので、なんだかよくわからないな? というのが個人的な感想。
(mame)
::Ruby が定義された
- A new toplevel module
Rubyhas been defined, which contains Ruby-related constants. This module was reserved in Ruby 3.4 and is now officially defined. [Feature #20884]
::Ruby モジュールが定義されました(Ruby 3.4 では、トップレベル Ruby 定数を定義しようとすると警告が出るようになっていました)。今あるのは、RUBY_VERSION などの定数が、こっちにも定義された、という感じです。RUBY_VERSION などの定数は、そのまま残っています。
p Ruby::VERSION #=> "4.0.0" p RUBY_VERSION #=> "4.0.0" と同じ pp Ruby.constants #=> [:REVISION, :COPYRIGHT, :ENGINE, :ENGINE_VERSION, :VERSION, :RELEASE_DATE, :DESCRIPTION, :PLATFORM, :PATCHLEVEL]
これからどんどん Ruby::... が増えるのかなぁ。
(ko1)
Ruby::Box が導入された
- A new (experimental) feature to provide separation about definitions. For the detail of "Ruby Box", see doc/language/box.md. [Feature #21311] [Misc #21385]
Ruby 4.0.0 の目玉機能、Ruby::Box が試験的に導入されました。
これについては特出しで記事を書きましたので、そちらをご覧ください。
(mame)
Set が組み込みクラスになった
Setis now a core class, instead of an autoloaded stdlib class. [Feature #21216]
Set が組み込みクラスになりました。C で書き直されて一部のメソッドが速くなったり、他の組み込み機能との親和性がこっそり高まったりしています。以前から Set は autoload が設定されていましたし、ユーザから見える挙動の変化は多くないだろう、たぶん、きっと、と思ってます。
Set#inspectnow returns a string suitable foreval, using theSet[]syntax (e.g.,Set[1, 2, 3]instead of#<Set: {1, 2, 3}>). This makes it consistent with other core collection classes like Array and Hash. [Feature #21389]
わかりやすく変わったのは、 Set#inspect の結果です。
p Set[1, 2, 3] #=> Ruby 3.4: #<Set: {1, 2, 3}> #=> Ruby 4.0: Set[1, 2, 3]
Array や Hash はそれらを作るリテラルで表示するので、 Set も(リテラルではないですが) Set を作る普通の記法で表示するようになりました。
- Passing arguments to
Set#to_setandEnumerable#to_setis now deprecated. [Feature #21390]
さりげない非互換としては、to_set の引数が廃止されました。これは何かというと、 Set の代わりに Set のサブクラスを引数で渡すことで、生成されるオブジェクトのクラスを指定できる機能でした。
class MySet < Set; end p (1..3).to_set #=> #<Set: {1, 2, 3}> p (1..3).to_set(MySet) #=> #<MySet: {1, 2, 3}>
個人的には、組み込みクラスは継承しないほうがいいと思います。
(mame)
Socket.tcp と TCPSocket.new で接続のタイムアウトが指定できるようになった
Socket.tcp&TCPSocket.newaccepts anopen_timeoutkeyword argument to specify the timeout for the initial connection. [Feature #21347]
Socket.tcp や TCPServer.new は、何らかの理由で接続できない場合、OS が規定するタイムアウトに達するまでブロックしていました。
このタイムアウトは、Linuxのデフォルトだと約127秒です。
# 127 秒ほどブロックしたあと例外になる(203.0.113.1 はテスト用のアドレス) p Socket.tcp("203.0.113.1", 80) #=> Connection timed out - connect(2) for 203.0.113.1:80 (Errno::ETIMEDOUT)
これはとても遅いので、まともなアプリケーションでは TCP 接続を Timeout.timeout でくくるのが事実上必須でした。
Ruby 4.0 では、 Socket.tcp や TCPServer.new に open_timeout キーワード引数を渡すことで、タイムアウト時間を指定できるようになりました。
# 1 秒ブロックしたあと例外になる p Socket.tcp("203.0.113.1", 80, open_timeout: 1) #=> Connection timed out - user specified timeout for 203.0.113.1:80 (Errno::ETIMEDOUT)
以前から、名前解決と接続に対してそれぞれ個別にタイムアウト設定する resolv_timeout キーワード引数と connect_timeout キーワード引数はありました。それとの違いは、open_timeout は名前解決から接続まで全体のタイムアウトを指定するところです。
Ruby 3.4 で導入された Happy Eyeballs v2 で、名前解決と接続の処理が一部並行処理されるようになり、個別のタイムアウトのモデルが理解しづらいものになったので、より便利な全体のタイムアウトである open_timeout が導入されました。
net-http はさっそく Timeout.timeout の代わりに open_timeout 引数を(利用可能なときは)使うように変わったようです。
完全に余談ですが、Linux のデフォルトが 127 秒なのは、1 秒から始めて exponential backoff の再送を 6 回行うためとのこと(1 + 2 + 4 + 8 + 16 + 32 + 64 = 127)。 net.ipv4.tcp_syn_retries = 6 を変えることで再送回数を変更できるようです。
(mame)
Socket.tcp や TCPSocket.new が投げる例外が変わった
- When a user-specified timeout occurred in
TCPSocket.new, eitherErrno::ETIMEDOUTorIO::TimeoutErrorcould previously be raised depending on the situation. This behavior has been unified so thatIO::TimeoutErroris now consistently raised. (Please note that, inSocket.tcp, there are still cases whereErrno::ETIMEDOUTmay be raised in similar situations, and that in both casesErrno::ETIMEDOUTmay be raised when the timeout occurs at the OS level.)
先に結論を書くと、これらのメソッドはタイムアウト時に Errno::ETIMEDOUT を投げたり IO::TimeoutError を投げたりするので、rescue する人は両方救うようにしてください。
begin Socket.tcp(...) rescue Errno::ETIMEDOUT, IO::TimeoutError end
Errno::E* はシステムコールの失敗に対応する例外という原則があります。この原則に従い、connect システムコールがタイムアウトで失敗したときは Errno::ETIMEDOUT が投げられます。一方で、ユーザが open_timeout で指定したタイムアウトはシステムコールの失敗ではないので、 IO::TimeoutError が投げられるようになりました。
以前はユーザが指定したタイムアウトの時も Errno::ETIMEDOUT を投げることがあった(が、 IO::TimeoutError のときもあったらしい)のですが、一旦原則どおりに合わせたようです。
両方 rescue しないといけないのは不便なのでなんとかしたいところですが、この問題に気づいたのがわりとリリース直前だった(この NEWS 解説記事の執筆がきっかけで判明した)ので、未来に期待。
(mame)
Unicode 17.0.0 にアップデートした
- Update Unicode to Version 17.0.0 and Emoji Version 17.0. [Feature #19908][Feature #20724][Feature #21275] (also applies to Regexp)
RubyのUnicodeのバージョンを15.0.0から17.0.0にアップデートしました。大きな変更はありません。新しい文字への対応、新しいUnicodeプロパティの追加、インド系文字の書記素クラスタの数え方のルール変更への対応が含まれています。
ちなみにRubyの正規表現ではUnicode プロパティでマッチできます。
'a'.match?(/\p{Hiragana}/) # => false 'あ'.match?(/\p{Hiragana}/) # => true
Hiraganaというプロパティがどんな正規表現にマッチするかはこのサイトで調べることができます。 🈀 という文字にもマッチするようですね。このようにプロパティはイメージと異なるマッチをすることがあるので、厳密にマッチしたい文字列が決まっている場合は使わないほうがよいです。
https://util.unicode.org/UnicodeJsps/regex.jsp?a=\p{Hiragana}&b=あa
Unicodeのバージョンアップ時にはこのようなプロパティの追加にも対応しています。自動化されているのでスクリプト流すだけで追加されて簡単です。
Rubyで使用できるUnicodeプロパティの一覧はこちら。
Unicodeのリリースノートはこちら。
(ima1zumi)
String#strip で削除する文字を指定できるようになった
String#strip,strip!,lstrip,lstrip!,rstrip, andrstrip!are extended to acceptselectorsarguments. [Feature #21552]
String#strip は、文字列両端の空白文字を削除するものですが、その削除する文字のセットをオプショナル引数として渡すことができるようになりました。複数の文字を渡すことで、それを削除する文字のセットとして扱います(String#tr と同じ)。
p ' str_!'.strip('!_') #=> " str" # 先頭スペースは削られず、末尾 _ と !が削られる
また、’x-y’ のような範囲指定もできます(これも String#tr と同じ)。
p 'str345'.strip('0-9') #=> "str" # 末尾 0~9 の文字を削除
削除したい文字列ではなく文字のセットを文字列として渡すというのは、String#delete_suffix の挙動と勘違いしちゃうんじゃないですか、と聞いてみたんですが(例えば 'bar'.strip('rab') で全部消える)、「試せばわかる」と一蹴されました。
(ko1)
Thread#raise/Fiber#raise が cause: キーワードを受け取れるようになった
- Thread
- Introduce support for
Thread#raise(cause:)argument similar toKernel#raise. [Feature #21360]
- Introduce support for
- Fiber
- Introduce support for
Fiber#raise(cause:)argument similar toKernel#raise. [Feature #21360]
- Introduce support for
Kernel#raise(cause: exc) のように、cause: 引数で、その例外を引き起こす原因となった、別の例外(デフォルトでは $! )が指定できるのですが、Thread#raise でも、cause: を指定できるようになりました。うかつに使うと、よくわからないことになりそうなので(別のスレッドに、今のスレッドで生じた例外を渡すなど)、ご利用は慎重に。
Fiber#raise(cause:) も、Thread#raise(cause:) と同じような話です。
(ko1)
Stdlib updates
We only list stdlib changes that are notable feature changes.
Other changes are listed in the following sections. We also listed release history from the previous bundled version that is Ruby 3.4.0 if it has GitHub releases.
- ostruct 0.6.3
- pstore 0.2.0
- 0.1.4 to v0.2.0
- benchmark 0.5.0
- logger 1.7.0
- rdoc 7.0.3
- win32ole 1.9.2
- 1.9.1 to v1.9.2
- irb 1.16.0
- reline 0.6.3
- readline 0.0.4
- fiddle 1.1.8
上記のライブラリはデフォルト gem ではなくなりました。つまり Gemfile に書いてなくても require できるライブライではなくなりましたので、利用される場合は気を付けてください。
The following default gem is added.
- win32-registry 0.1.2
The following default gems are updated.
- RubyGems 4.0.3
- bundler 4.0.3
- date 3.5.1
- delegate 0.6.1
- digest 3.2.1
- 3.2.0 to v3.2.1
- english 0.8.1
- 0.8.0 to v0.8.1
- erb 6.0.1
- error_highlight 0.7.1
- etc 1.4.6
- fcntl 1.3.0
- 1.2.0 to v1.3.0
- fileutils 1.8.0
- 1.7.3 to v1.8.0
- forwardable 1.4.0
- 1.3.3 to v1.4.0
- io-console 0.8.2
- 0.8.1 to v0.8.2
- io-nonblock 0.3.2
- io-wait 0.4.0
- 0.3.2 to v0.3.3, v0.3.5.test1, v0.3.5, v0.3.6, v0.4.0
- ipaddr 1.2.8
- json 2.18.0
- net-http 0.9.1
- openssl 4.0.0
- optparse 0.8.1
- pp 0.6.3
- 0.6.2 to v0.6.3
- prism 1.7.0
- psych 5.3.1
- resolv 0.7.0
- stringio 3.2.0
- strscan 3.1.6
- time 0.4.2
- 0.4.1 to v0.4.2
- timeout 0.6.0
- uri 1.1.1
- weakref 0.1.4
- 0.1.3 to v0.1.4
- zlib 3.2.2
- 3.2.1 to v3.2.2
The following bundled gems are updated.
- minitest 6.0.0
- power_assert 3.0.1
- rake 13.3.1
- test-unit 3.7.5
- rexml 3.4.4
- rss 0.3.2
- 0.3.1 to 0.3.2
- net-ftp 0.3.9
- 0.3.8 to v0.3.9
- net-imap 0.6.2
- net-smtp 0.5.1
- 0.5.0 to v0.5.1
- matrix 0.4.3
- 0.4.2 to v0.4.3
- prime 0.1.4
- 0.1.3 to v0.1.4
- rbs 3.10.0
- 3.8.0 to v3.8.1, v3.9.0.dev.1, v3.9.0.pre.1, v3.9.0.pre.2, v3.9.0, v3.9.1, v3.9.2, v3.9.3, v3.9.4, v3.9.5, v3.10.0.pre.1, v3.10.0.pre.2, v3.10.0
- typeprof 0.31.1
- debug 1.11.1
- 1.11.0 to v1.11.1
- base64 0.3.0
- 0.2.0 to v0.3.0
- bigdecimal 4.0.1
- drb 2.2.3
- 2.2.1 to v2.2.3
- syslog 0.3.0
- 0.2.0 to v0.3.0
- csv 3.3.5
- repl_type_completor 0.1.12
いっぱいアップデートされています。 読み切れないのでスキップ。
IRB: copy コマンドの追加
copy コマンドが追加されました。出力結果をシステムのクリップボードにコピーできます。
例えばこのようにRails consoleでモデルの出力結果をコピーしたい場合、出力後に copy を実行すると User:0x00.. から updated_at までクリップボードにコピーされます。
app(dev)> User.last => #<User:0x0000000351352c40 id: 1, email: "test@example.com", name: "Ruby", created_at: "2025-12-09 17:48:39.000000000 +0900", updated_at: "2025-12-09 17:48:39.000000000 +0900"> app(dev)> copy Copied to system clipboard
(ima1zumi)
(ko1追記: システムがもつ、クリップボードにコピーするコマンド(pbcopy とか)を呼び出すので、例えば ssh 先の端末でやっても動きません)
RubyGems and Bundler
Ruby 4.0 bundled RubyGems and Bundler version 4. see the following links for details.
- Upgrading to RubyGems/Bundler 4 - RubyGems Blog
- 4.0.0 Released - RubyGems Blog
- 4.0.1 Released - RubyGems Blog
- 4.0.2 Released - RubyGems Blog
- 4.0.3 Released - RubyGems Blog
こちらでアップデートが紹介されています。
Supported platforms
- Windows
- Dropped support for MSVC versions older than 14.0 (_MSC_VER 1900). This means Visual Studio 2015 or later is now required.
Windows ネイティブで、Visual Studio 2015 以上でないとビルドできなくなったそうです。14.0 、1900 、2015 と、VC 関係のバージョン番号は種類がたくさんあってむずかしいですね。
(mame)
Compatibility issues
- The following methods were removed from Ractor due to the addition of
Ractor::Port:Ractor.yieldRactor#takeRactor#close_incomingRactor#close_outgoging[Feature #21262]
これも記事を読んでね。
ObjectSpace._id2refis deprecated. [Feature #15408]
Object の ID (#object_id でとれるやつ)からそのオブジェクトを引くためのメソッドObjectSpace._id2ref が deprecated になりました。随分前から「deprecate しよう」という決定がされていたのですが、なぜか忘れられていたので、今回やっと非推奨となりました。
id = self.object_id p ObjectSpace._id2ref(id) #=> main # Ruby 4.0 では警告が出る -> warning: ObjectSpace._id2ref is deprecated
Process::Status#&andProcess::Status#>>have been removed. They were deprecated in Ruby 3.3. [Bug #19868]
Process::Status#& と Process::Status#>> が削除されました。もともと Ruby 3.3 から deprecated だったそうです。ステータスコードをちょいちょい見るために使ってたみたいですが、今では専用メソッドがあるためもう使ってないようです。
rb_path_checkhas been removed. This function was used for$SAFEpath checking which was removed in Ruby 2.7, and was already deprecated,. [Feature #20971]
rb_path_check という関数が削除されました。もともと $SAFE という古に削除された機能に依存した機能だったみたいなんですが、互換性のためなのか残されていたので、今回削除されたようです。
(ko1)
バックトレースの表記が変わった
- A backtrace for
ArgumentErrorof "wrong number of arguments" now include the receiver's class or module name (e.g., inFoo#barinstead of inbar). Bug #21698
wrong number of argument の例外のバックトレースで、クラス名が出るようになりました。
# Ruby 3.4 test.rb:1:in 'add': wrong number of arguments (given 1, expected 2) (ArgumentError) # Ruby 4.0 test.rb:1:in 'Object#add': wrong number of arguments (given 1, expected 2) (ArgumentError)
メソッド名の表記が、add から Object#add に変わっています。
- Backtraces no longer display
internalframes. These methods now appear as if it is in the Ruby source file, consistent with other C-implemented methods. Bug #20968
最近は組み込みメソッドが C ではなく Ruby で書かれるようになってきています。その影響で、バックトレースで <internal:???> という謎のファイルがしばしば見えることがありました。
# Ruby 3.4
$ ruby -e '[1].fetch_values(42)'
<internal:array>:211:in 'Array#fetch': index 42 outside of array bounds: -1...1 (IndexError)
from <internal:array>:211:in 'block in Array#fetch_values'
from <internal:array>:211:in 'Array#map!'
from <internal:array>:211:in 'Array#fetch_values'
from -e:1:in '<main>'
# Ruby 4.0
$ ruby -e '[1].fetch_values(42)'
-e:1:in 'Array#fetch_values': index 42 outside of array bounds: -1...1 (IndexError)
from -e:1:in '<main>'
これは、組み込みメソッドを定義しているソースを表す仮想的なファイル名でした。しかし、このファイル名を出されても普通のユーザにできることはなく、スタックトレースを読み飛ばす手間が発生するだけでした。ほとんどの場合、ユーザが見たいのはその組み込みメソッドの呼び出し元です。
ということで、<internal:*> の表示をばっさり省いて、呼び出し元でエラーが発生したかのように表示をするように変更しました。
(mame)
全部出てたほうがいいと思うんだけどな。
(ko1)
標準ライブラリの互換性について
CGI ライブラリがバンドルから外れた
- CGI library is removed from the default gems. Now we only provide
cgi/escapefor the following methods:CGI.escapeandCGI.unescapeCGI.escapeHTMLandCGI.unescapeHTMLCGI.escapeURIComponentandCGI.unescapeURIComponentCGI.escapeElementandCGI.unescapeElement
[Feature #21258]
cgi gem のほとんどがバンドルから外されました。現代ではメンテナンスコストに見合う価値がないと判断されたためです。
CGI.escape などの上記 8 つのメソッドだけは非常によく使われているので、 cgi/escape という形でバンドルが残っています。もし cgi gem 全体は必要なく、これらのメソッドだけが必要であれば、 require "cgi/escape" すると良いでしょう。
cgi gem 全体が必要であれば、 gem install cgi したり Gemfile に gem "cgi" を書き足したりするなどしてください。
(mame)
SortedSet がバンドルから外れた
- With the move of
Setfrom stdlib to core class,set/sorted_set.rbhas been removed, andSortedSetis no longer an autoloaded constant. Please install thesorted_setgem andrequire 'sorted_set'to useSortedSet. [Feature #21287]
SortedSet がバンドルから外されました。必要であれば、sorted_set gem をインストールしてください。
(mame)
C API updates
- IO
rb_thread_fd_closeis deprecated and now a no-op. If you need to expose file descriptors from C extensions to Ruby code, create anIOinstance usingRUBY_IO_MODE_EXTERNALand userb_io_close(io)to close it (this also interrupts and waits for all pending operations on theIOinstance). Directly closing file descriptors does not interrupt pending operations, and may lead to undefined behaviour. In other words, if twoIOobjects share the same file descriptor, closing one does not affect the other. [Feature #18455]
rb_thread_fd_close() が deprecated となり、呼び出しても何も行わないことになりました。この API は fd を直接受けるものだったのですが、Ruby レベルでは IO オブジェクト経由で close してほしい、ということで、こうなりました。もし、fd を直接いじりたい場合は、IO objectを作る際に RUBY_IO_MODE_EXTERNAL を使い、また rb_io_close(io) で close してほしいということです。
- GVL
rb_thread_call_with_gvlnow works with or without the GVL. This allows gems to avoid checkingruby_thread_has_gvl_p. Please still be diligent about the GVL. [Feature #20750]
GVL(Global/Giant VM Lock、ではもうないんですよね。Ractor ごとに持つので。Great Valuable Lock を最近推しています)を開放して何かしているとき、やっぱり GVL を再獲得して何かする(Ruby に関することは GVL がないと色々動きません)、というときのために rb_thread_call_with_gvl() という API があります。関数 f を渡して、f を GVL 獲得後に呼び出す、というものです。この API は、これまでは GVL を開放しているとき以外で動かすとエラーになっていたんですが、これをエラーなく呼べるようになりました。解放していても解放していなくても、どちらでも呼びたい、というケースがあるので、こっちのほうが便利だろう、ということです。
個人的には、GVL の獲得状況には sensitive であるべきというか、そもそもそんな共用するようなコード各シチュエーションを極力排除したほうがいいと思っているので反対だったんだけど、まぁいいんじゃない、ということで入りました。まぁ、そもそも GVL を外して実行するような処理は、極力シンプルに保つといいと思います。
- Set
- A C API for
Sethas been added. The following methods are supported: [Feature #21459]rb_set_foreachrb_set_newrb_set_new_caparb_set_lookuprb_set_addrb_set_clearrb_set_deleterb_set_size
- A C API for
Set が組み込みになった影響で、これらの C API が導入されました。ちなみに、rb_set_ prefix な C-API がそこそこあって(例えば rb_set_errinfo() )、ちょっと混乱するね、という話がありました。
(ko1)
Implementation improvements
Class#new(ex.Object.new) is faster in all cases, but especially when passing keyword arguments. This has also been integrated into YJIT and ZJIT. [Feature #21254]
Foo.new のような呼び出しが速くなりました。専用命令を入れた感じです。ちなみに、命令のラフな設計は私。
- GC heaps of different size pools now grow independently, reducing memory usage when only some pools contain long-lived objects
GC の改善。VWA の調整だと思います。
- GC sweeping is faster on pages of large objects
これ、何したのかわからないな。
- "Generic ivar" objects (String, Array,
TypedData, etc.) now use a new internal "fields" object for faster instance variable access
ユーザー定義オブジェクト以外でインスタンス変数を持つために、あたらしいデータ構造が導入されました。これは何のためだったんだろう。Ractor?
- The GC avoids maintaining an internal
id2reftable until it is first used, makingobject_idallocation and GC sweeping faster
id2ref を管理するためのデータを遅延生成にしたので、id2ref を利用しない限り、#object_id の呼び出しと sweep が速くなったそうです。これ、どうやったんだろうな?
object_idandhashare faster on Class and Module objects
クラスとモジュールで object_id と hash が速くなったそうです。これも何やったんだろ。
- Larger bignum Integers can remain embedded using variable width allocation
Bignum (大きな整数)もVWA 管理することで速くなったそうです。
Random,Enumerator::Product,Enumerator::Chain,Addrinfo,StringScanner, and some internal objects are now write-barrier protected, which reduces GC overhead.
いくつかのオブジェクトに write-barrier をちゃんと入れて、unprotected な状態ではなくしたそうです。GC が速くなる。
(あんまりちゃんと追えてなくてごめんなさい)
(ko1)
Ractor
A lot of work has gone into making Ractors more stable, performant, and usable. These improvements bring Ractor implementation closer to leaving experimental status.
- Performance improvements
- Frozen strings and the symbol table internally use a lock-free hash set Feature #21268
- Method cache lookups avoid locking in most cases
- Class (and generic ivar) instance variable access is faster and avoids locking
- CPU cache contention is avoided in object allocation by using a per-ractor counter
- CPU cache contention is avoided in xmalloc/xfree by using a thread-local counter
object_idavoids locking in most cases
- Bug fixes and stability
- Fixed possible deadlocks when combining Ractors and Threads
- Fixed issues with require and autoload in a Ractor
- Fixed encoding/transcoding issues across Ractors
- Fixed race conditions in GC operations and method invalidation
- Fixed issues with processes forking after starting a Ractor
- GC allocation counts are now accurate under Ractors
- Fixed TracePoints not working after GC Bug #19112
Ractor について、性能向上とバグフィックスがたくさん行われました。主に私以外の人が頑張ってくれた。
書いてないけど、上述の Ractor::Port の導入で、すごいばっさりと機能が整理できたので、その辺も性能向上になってるんじゃないかと思います。
JIT
- ZJIT
- Introduce an experimental method-based JIT compiler.
Where available, ZJIT can be enabled at runtime with the
--zjitoption or by callingRubyVM::ZJIT.enable. When building Ruby, Rust 1.85.0 or later is required to include ZJIT support. - As of Ruby 4.0.0, ZJIT is faster than the interpreter, but not yet as fast as YJIT. We encourage experimentation with ZJIT, but advise against deploying it in production for now.
- Our goal is to make ZJIT faster than YJIT and production-ready in Ruby 4.1.
- Introduce an experimental method-based JIT compiler.
Where available, ZJIT can be enabled at runtime with the
YJIT の後継である ZJIT が導入され、ビルド時に Rust 環境(rustc 1.85.0 以降)があれば、一緒にビルドされることになりました。--zjit コマンドライン引数で利用可能です。YJIT までは速くないので、production で利用するのはまだ待ってほしい、とのこと。Ruby 4.1 で YJIT を超えることを目標としているとのことです。
rubykaigi.org railsatscale.com
- YJIT
RubyVM::YJIT.runtime_statsratio_in_yjitno longer works in the default build. Use--enable-yjit=statsonconfigureto enable it on--yjit-stats.- Add
invalidate_everythingto default stats, which is incremented when every code is invalidated by TracePoint.
- Add
mem_size:andcall_threshold:options toRubyVM::YJIT.enable.
YJIT まわりも改善されたようですが、詳しくないのでスキップ。
- RJIT
--rjitis removed. We will move the implementation of the third-party JIT API to the ruby/rjit repository.
--rjit 周りが削除されたそうです。
(ko1)
おわりに
Ruby 4.0 の新機能や改善を紹介してきました。ここで紹介した以外でも、バグの修正や細かな改善が行われています。お手元の Ruby アプリケーションでご確認いただければと思います。
Ruby::Box や zjit や、Ractor の作り直しなど、いろいろ試しがいのあるリリースになりました。ぜひ、お手元にセットアップして新しい Ruby を楽しんでください。
Enjoy Ruby programming!
(ko1/mame, guest: ima1zumi)