技術部の笹田(ko1)と遠藤(mame)です。クックパッドで Ruby (MRI: Matz Ruby Implementation、いわゆる ruby コマンド) の開発をしています。お金をもらって Ruby を開発しているのでプロの Ruby コミッタです。
昨日 12/25 に、恒例のクリスマスリリースとして、Ruby 3.2.0 がリリースされました(Ruby 3.2.0 リリース)。今年も Ruby 3.2 の NEWS.md ファイルの解説をします。NEWS ファイルとは何か、は以前の記事を見てください。
- プロと読み解く Ruby 2.6 NEWS ファイル - クックパッド開発者ブログ
- プロと読み解くRuby 2.7 NEWS - クックパッド開発者ブログ
- プロと読み解くRuby 3.0 NEWS - クックパッド開発者ブログ
- プロと読み解く Ruby 3.1 NEWS - クックパッド開発者ブログ
本記事は新機能を解説することもさることながら、変更が入った背景や苦労などの裏話も記憶の範囲で書いているところが特徴です。
Ruby 3.2 は、新しい機能をたくさん盛り込み、また非推奨の機能を削除するなどアグレッシブなバージョンです(ただ、往年の Ruby をご存じの方にとっては変更が足りず物足りないかもしれません)。性能改善もたくさん行われています。ぜひ使ってみてください。
ちなみに、Ruby 2.7はあと4ヶ月くらいでEOL(サポート終了)で、なんと Ruby 2.x がもうすぐ終わってしまいます。移行の準備はいかがですか?
Ruby 3.2 の目玉として、次のようなものがあげられています(リリースノートから抜粋)。
- WASIベースのWebAssemblyサポート
- 実用段階になったYJIT
- ReDoSに対するRegexpの改善
- SyntaxSuggest の導入
本記事では、これらを含めて NEWS ファイルにあるものをだいたい紹介していきます。
■言語の変更
メソッド引数の委譲に *
、**
無名引数が使えるようになった
Anonymous rest and keyword rest arguments can now be passed as arguments, instead of just used in method parameters. [Feature #18351]
def foo(*) bar(*) end def baz(**) quux(**) end
Rubyでメソッドに受けた引数を別のメソッドに渡す方法として、無名の*
と**
が利用できるようになりました。
# ポジショナル引数だけ委譲したい def foo(*) bar(*) end # これと同じ def foo(*args) bar(*args) end # キーワード引数だけ委譲したい def baz(**) quux(**) end # これと同じ def baz(**kw) quux(**kw) end
似たような話として、次のような機能がありました。
- Ruby 2.7 で導入された
...
記法(def f(...); g(...); end
)- Ruby 3.0 で引数が加えられるように拡張された(
def f(...) = g(:first, ...); end
)
- Ruby 3.0 で引数が加えられるように拡張された(
- Ruby 3.1 で導入されたブロックを委譲するための記法
def foo(&) = bar(&)
今回の拡張は、これらの「無名引数」シリーズの延長かと思います。今回の拡張で委譲する引数にちょっと足して渡す、みたいなのがやりやすくなったんではないかと思います。
def f(*, **) g(:first, *, add_key: 1, **) end # これと同じ def f(*args, **kw) g(:first, *args, add_key: 1, **kw) end
とくに、キーワード引数ですかねぇ。余談ですが、上記例で渡された引数に add_key
キーワードが含まれていた場合、kw
のほうが優先されるためadd_key: 1
は g()
にわたりません。渡されたキーワード引数が優先される場合はこれで問題ないのですが、絶対に add_key: 1
を渡したい場合は**
よりも後ろに書く必要があります。
def f1(*, **) p(*, add_key: 1, **) end def f2(*, **) p(*, **, add_key: 1) end f1(add_key:10) #=> {:add_key=>10} f2(add_key:10) #=> {:add_key=>1}
用途に応じて気を付けてください。
引数に名前を付けておけば、これまでと全く変わりません。無名にすることで名前を考える苦労がちょっと減るのですが、乱用するとわかりづらいコードになるので、容量用法にお気を付けください。
(ko1)
proc
の引数のコーナーケース挙動が少し変更
- A proc that accepts a single positional argument and keywords will no longer autosplat. [Bug #18633]
引数を複数受け取るProcに、配列を1つ渡して呼び出すと、配列が勝手に分解されます。
proc {|a, b| a }.call([1, 2]) #=> 1 proc {|a, b| b }.call([1, 2]) #=> 2
しかし引数を1つだけ受け取るProcの場合は、分解されません。
proc {|a| a }.call([1, 2]) #=> [1, 2] proc {|a| a }.call([1] #=> [1]
ここで、引数を1つ受け取りつつ、キーワード引数も受け取るProcの挙動はどうあるべきでしょうか。
proc {|a, **k| a }.call([1, 2]) #=> ?
Ruby 3.1では分解される(つまり 1
が返る)という挙動でしたが、Ruby 3.2からは分解されない(つまり [1, 2]
が返る)ということになりました。
Procの引数はとてもむずかしいですね。
(mame)
定数の評価順序が「左から右」の原則を守るようになった
Constant assignment evaluation order for constants set on explicit objects has been made consistent with single attribute assignment evaluation order. With this code:
foo::BAR = baz
foo
is now called beforebaz
. Similarly, for multiple assignments to constants, left-to-right evaluation order is used. With this code:foo1::BAR1, foo2::BAR2 = baz1, baz2
The following evaluation order is now used:
foo1
foo2
baz1
baz2
定数は、expr::FOO
のように記述できるのはご存じでしょうか。expr
にはたいていの Ruby 式を記述することができます。
class C; class D; end; end d = (p C)::D #=> C (p C の出力) p d #=> C::D
さらに、この記法は多重代入の左辺に使うことができます。そうすると Ruby 3.1 で修正されたコード順の問題が出てきます(参考: 「多重代入の評価順序が変更された」プロと読み解く Ruby 3.1 NEWS - クックパッド開発者ブログ)。
NEWS にあるコード例を、実際に動くコードにしてみます。
class FOO; end def foo1 = (p(:foo1); FOO) def foo2 = (p(:foo2); FOO) def baz1 = p(:baz1) def baz2 = p(:baz2) foo1::BAR1, foo2::BAR2 = baz1, baz2
これを Ruby 3.1 で実行してみると、
:baz1 :baz2 :foo1 :foo2
となります。つまり、右辺の2式が実行されたあと、左辺の2式が実行されています。そこで、Ruby 3.1 で多重代入の評価順序を修正した Jeremy Evans によって、ただしく「左から右」と評価されるように修正されました。
:foo1 :foo2 :baz1 :baz2
いやぁ、良かったよかった。ただ、これバイトコードだいぶ冗長にして若干遅くなるんですよね...。できれば、皆様にはこういう難しいコード(多重代入)は書かないでいただけると助かります。
ちなみに、すごいどうでもいいんですが、この多重代入が書けるようになったのは Ruby 1.8 以降なんですね。
(ko1)
Findパターンが実用段階
- "Find pattern" is no longer experimental. [Feature #18585]
Findパターンがパターンマッチで利用可能になりました。正確に言うとRuby 3.1でも実験的機能として導入されていましたが、今回から正式な機能となります。違いとしては、使っても警告が出ません。
ary = [1, 2, 3, 4, 5] # Find パターン if ary in [*, 3, *] p "3を含んでいる" end if ary in [*, 3, x, *] p "3の後にある値は#{ x }" end
上の例のように、最初にマッチした値の次の値、などを取りたい時に便利かもしれません。
(mame)
ruby2_keywordsが厳格に要求される
- Methods taking a rest parameter (like
*args
) and wishing to delegate keyword arguments throughfoo(*args)
must now be marked withruby2_keywords
(if not already the case). In other words, all methods wishing to delegate keyword arguments through*args
must now be marked withruby2_keywords
, with no exception. This will make it easier to transition to other ways of delegation once a library can require Ruby 3+. Previously, theruby2_keywords
flag was kept if the receiving method took*args
, but this was a bug and an inconsistency. A good technique to find the potentially-missingruby2_keywords
is to run the test suite, for where it fails find the last method which must receive keyword arguments, useputs nil, caller, nil
there, and check each method/block on the call chain which must delegate keywords is correctly marked asruby2_keywords
. [Bug #18625] [Bug #16466]
Ruby 3.0で導入されたruby2_keywords
がより厳格に要求されるようになりました。
ruby2_keywords
は、メソッドがキーワード引数を可変長引数の一部として受け取るようにする注釈です。その引数を更に他のメソッドに渡すとき、キーワード引数だったものとして渡されます。Ruby 2のときの委譲を再現するために使われます。
# fooは普通の引数もキーワード引数もまとめて可変長引数として受け取る ruby2_keywords def foo(*args) # 受け取った可変長引数をtargetに委譲する target(*args) end def target(k:) k end foo(k: 1) #=> 1
今回の変更は、次のような多段の委譲(foo→bar→target)のときの話です。
ruby2_keywords def foo(*args) bar(*args) end # Ruby 3.2からはここにもruby2_keywordsを書く必要がある def bar(*args) target(*args) end def target(k:) k end foo(k: 1)
foo
はruby2_keywords
が指定されていますが、bar
には指定されていないことに注意してください。
原則で言えばbar
も委譲をしているのでruby2_keywords
を指定すべきですが、Ruby 3.1までは指定しなくても意図通りに委譲ができていました。
しかし、Ruby 3.2からはruby2_keywords
の注釈が厳格に求められるようになりました。
よって、ruby2_keywords
を書き足してもいいですが、もしRuby 2との互換性を気にしないのであれば、次のように委譲するのがおすすめです。Ruby 2.7は2023年3月末にEOLになる見込みです。あと3ヶ月。
def bar(*args, **opts) target(*args, **opts) end
(mame)
■組み込みクラスのアップデート
Fiber#storage
が導入された
Introduce Fiber. and Fiber.= for inheritable fiber storage. Introduce Fiber#storage and Fiber#storage= (experimental) for getting and resetting the current storage. Introduce
Fiber.new(storage:)
for setting the storage when creating a fiber. [Feature #19078]Existing Thread and Fiber local variables can be tricky to use. Thread local variables are shared between all fibers, making it hard to isolate, while Fiber local variables can be hard to share. It is often desirable to define unit of execution ("execution context") such that some state is shared between all fibers and threads created in that context. This is what Fiber storage provides.
def log(message)
puts "#{Fiber[:request_id]}: #{message}"
end
def handle_requests
while request = read_request
Fiber.schedule do
Fiber[:request_id] = SecureRandom.uuid
request.messages.each do |message|
Fiber.schedule do
log("Handling #{message}") # Log includes inherited request_id.
end
end
end
end
end
You should generally consider Fiber storage for any state which you want to be shared implicitly between all fibers and threads created in a given context, e.g. a connection pool, a request id, a logger level, environment variables, configuration, etc.
ちょっと背景が長い話になります。
グローバル変数を使いたいときってありますよね。複数メソッドをまたいで情報を共有したい、でも引数で引き回すのも嫌。グローバル変数は、そんな我がままなあなたのための機能です。
ただ、グローバル変数はプロセス内で共有されてしまうので、複数のスレッドで共有したくないときは嫌ですね。そこで、スレッドローカルストレージ(TLS)が導入されていました。Thread#[]
でアクセスできるやつです。Thread.current[:request_id] = 1
としてけば、あるスレッド内で Thread.current[:request_id]
と参照すると 1、それ以外のスレッドからは別の値(もしくは未設定の nil
)が返るというものです。
その後、Ruby 1.9で、Fiber が導入されました。実は、TLS は Fiber ローカルな挙動にするためによく利用されてきました。例えば、p
メソッドなどでの循環参照の検出などです。そこで、Thread#[]
はTLSではなく、ファイバーローカルな値を扱う機能(FLS)になりました。
その後、Ruby 2.0で、あるスレッドで作ったFiber間で共有してほしいけど、スレッド間では共有してほしくない、という要望が出てきました。Fiber は Enumerator などでいつの間にか利用される可能性があるので、それらを使っていても同じ変数セットを使い続けてほしい、というものです。そこで、Thread#thread_variable_set/get
が TLS として導入されました。
そろそろ混乱してきたのではないかと思うのでまとめておきます。
- Ruby 1.8まで:
Thread#[]
は TLS - Ruby 1.9から:
Thread#[]
は FLS - Ruby 2.0から:
Thread#[]
は FLS、Thread#thread_variable_set/get
は TLS
まぁ、正直 Thread#thread_variable_set/get
が必要になるケースは、コンポーネント間の暗黙的な依存を増やすので、引数やインスタンス変数などで正しくデータを伝搬するべきだと思いますが...。
で、さらに次の要望が出てきました。現状のよくわからん TLS、FLS を一新する提案です。
- スレッドの親子(親スレッドと子スレッド)で共有する変数セット
- あるスレッドで作ったファイバで共有する変数セット
- ファイバの親子(親ファイバと子ファイバ)で共有する変数セット
- それらの変数セットを柔軟にとっかえひっかえしたい
つまり、スレッド -> スレッド、スレッド -> ファイバ、ファイバ -> ファイバの親子関係(スレッドを作るとファイバが作成されるので、そういう意味ではファイバ -> ファイバの親子関係しかないのですが)で共有される変数セットを用意したい、それをイイ感じに管理したい、というものです。これを、Fiber storage という機能群でまとめられました。利用用途は、a connection pool, a request id, a logger level, environment variables, configuration だそうです。
Fiber storage という名前ですが、Fiber local storage ではなく、Fiber 間で変数セットを共有するための仕組みです。ただし、ファイバを作成した時点で変数セットはコピーされるので、独立しています。
アクセスは Fiber[:key]
で行います(つまり、別ファイバの storage にはアクセスできません)。
Fiber[:k1] = :k1 Fiber.new{ # デフォルトでは親の Fiber storage が引き継がれる p Fiber[:k1] #=> :k1 Fiber[:k1] = :updated_k1 }.resume p Fiber[:k1] #=> :k1 (:updated_k1 ではない)
ファイバ生成時、引き継がない、独自の変数セットを持つ、という選択をすることもできます。
Fiber[:k1] = :k1 Fiber.new(storage: nil){ p Fiber[:k1] #=> nil }.resume Fiber.new(storage: {k1: :my_k1}){ p Fiber[:k1] #=> :my_k1 }.resume
また、Fiber#storage(=)
で変数のセットをまとめて取り出したり設定したりできます(なお、Fiber#storage=
は experimental feature です)。
Fiber.current.storage = {k1: :new_k1, k2: :new_k2} p Fiber[:k1] #=> :new_k1 p Fiber[:k2] #=> :new_k2
このまとめての設定機能が何に使うのかよくわからなかったんですが、スレッドプールからスレッドを取り出して、そこに今から実行するために必要になるストレージをガツンと設定するために必要なんだそうです。なるほどなぁ。
細かい話はもう少しあるんですが、十分細かい話をした気がするので、この辺で。
マニア向け:この機能が議論されていたきは、「ある種のダイナミックスコープ」と説明されていました(気になる人はググってください)。なるほどなぁ、並行実行単位を超えて共有されるダイナミックスコープ(並行実行単の生成を関数呼び出しととらえれば等価です)。確かに、便利そうではあるんだけど。ActiveRecord みたいな DSL で使いたいよなぁ。でも、これはこれで複雑そうなんで、大丈夫かなぁ。ほかの言語で似たような機能の成功例(もしくは失敗例)があったら教えてください。
人類は、必要に駆られてさまざまなスコープを導入してきました。
- グローバルスコープ(グローバル変数、プロセスローカル変数、Rubyだと定数空間も)
- ローカルスコープ(ローカル変数、ブロックローカル変数)
- レキシカルスコープ(ローカル変数、親クロージャ(Rubyだとブロック)の変数)
- ダイナミックスコープ(動的変数、Rubyにはない、今回のは似ている)
- インスタンスローカルスコープ(インスタンス変数)
- クラスローカルスコープ(クラス変数)
- スレッドローカルスコープ(スレッドローカル変数)
- ファイバローカルスコープ(ファイバローカル変数)
- Ractor ローカルスコープ(Racotr ローカル変数)
- (広げれば、分散システムのディレクトリサービスとかも入りそう)
- (きっと探せばもっとありそう)
今回、これにファイバの親子関係によるスコープが導入されたわけですね。人類の業は深い。
(ko1)
Fiber::Scheduler#io_select
が導入された
- Introduce
Fiber::Scheduler#io_select
for non-blocking IO.select. [Feature #19060]
Fiber scheduler で IO.select
をフックするためのインターフェースが導入されました。
(ko1)
IO#timeout=sec
が導入された
Introduce IO#timeout= and IO#timeout which can cause IO::TimeoutError to be raised if a blocking operation exceeds the specified timeout. [Feature #18630]
STDIN.timeout = 1 STDIN.read # => Blocking operation timed out! (IO::TimeoutError)
1度の I/O 操作(例えば、read とか write とか)に、その I/O が対応していれば、設定した時間を超えたらタイムアウトの例外(IO::TimeoutError
)が発生するようになりました。
r, w = IO.pipe r.timeout = 3 # pipe の片方の w に何も書いていないので、r.read してもずっと sleep する r.read(1) #=> Blocking operation timed out! (IO::TimeoutError)
(timed out! ってメッセージは、なんか元気いいですね。いいことあったのかな)
この API は便利そうですが、いくつか注意が必要です。
- 1回の操作ごとにタイムアウトはリセットされるので「複数の I/O 処理全部を 3 秒のタイムアウトで」のようにするには、結構大変です。
- 大きなバッファを write する場合、write が複数回に別れることがあります。このとき、途中の write でタイムアウトが生じると、書き出している途中で例外が発生するため、どこまで書き出したかわかりません。(read はそういうのないんだっけ? 遠藤さんによるとあるらしいです)というわけで、タイムアウトを検出しても、リトライできるとは思わない方が安全です。たとえば、ウェブのリクエストの場合は素直にエラーを返しましょう。
- 対応する I/O、および操作にしか効きません。典型的にはブロックデバイス(いわゆるストレージ上のファイル)には効きませんし、close などにも効きません。ほとんどの場合、(人間の尺度では)一瞬で処理が終わるので問題になりませんが、ハードウェアが壊れていて時間がかかったり、NFS上で時間がかかったりしていても、これにタイムアウトは効きません。簡単には、ソケットにしか効かないって思っておくのが良いと思います。
というわけで、ちょっと使うのが難しそうな API なんですが、「どーしょーもない」ことを検出するための最後の手段としてご利用いただくのがいいでしょうか。私はイマイチ、この API の良い用法がわかっていません。
(ko1)
IO.newにpathキーワードが渡せるようになった
- Introduce
IO.new(..., path:)
and promoteFile#path
toIO#path
. [Feature #19036]
ファイルディスクリプタ番号からIOを作る機能があるのですが、それにファイルパスを指定できるようになりました。
io = IO.for_fd(0, path: "hoge") p io #=> #<IO:hoge> p io.path #=> "hoge"
inspect
の結果に影響を与える程度なので、まあ、あまり使うことはないと思います。
いちおう導入の背景を書くと、
- ptyという拡張ライブラリが無理やりIO#pathを指定していた(実装がC言語なので、強引に書き換えることができてた)
- JRubyがpure Ruby(+FFI)でptyを模倣するライブラリを作っていたとき、
IO#path
を指定する方法がないことに気づいた - 困るので導入された
という感じです。
(mame)
Class#attached_object
が導入された
Class#attached_object
, which returns the object for which the receiver is the singleton class. Raises TypeError if the receiver is not a singleton class. [Feature #12084]
特異クラスから、それのインスタンスを得るメソッドが導入されました。
obj = Object.new klass = obj.singleton_class p obj #=> #<Object:0x00007f7919a60150> p klass.attached_object #=> #<Object:0x00007f7919a60150>
Rubyの型チェッカであるSorbet、のための型定義のスタブ生成ツールであるtapioca、は、Rubyの黒魔術を活用しまくって型定義を動的に推定するのですが、その際にこれが欲しかったとのこと。
(mame)
Data
クラスが導入された
- New core class to represent simple immutable value object. The class is similar to Struct and partially shares an implementation, but has more lean and strict API. [Feature #16122]
Measure = Data.define(:amount, :unit)
distance = Measure.new(100, 'km') #=> #<data Measure amount=100, unit="km">
weight = Measure.new(amount: 50, unit: 'kg') #=> #<data Measure amount=50, unit="kg">
weight.with(amount: 40) #=> #<data Measure amount=40, unit="kg">
weight.amount #=> 50
weight.amount = 40 #=> NoMethodError: undefined method `amount='
Data
という新しいクラスが導入されました。一言で言えば、書き換え不可のStruct
です。
Point = Data.define(:x, :y) pt = Point.new(1, 2) p pt.x #=> 1 p pt.y #=> 2
Data.define
をStruct.new
と読み替えれば、だいたい同じです。
違いというと、
- 書き換えをするメソッドがないこと
- 配列やハッシュのように扱うメソッド(
[]
とかselect
とか)がないこと - 初期化時にフィールドの値を省略できないこと
くらいです。
# 書き換えはできない pt.x = 3 #=> undefined method `x=' for #<data Point x=1, y=2> (NoMethodError) # []でフィールドを読み出せない pt[:x] #=> undefined method `[]' for #<data Point x=1, y=2> (NoMethodError) # newで引数が足りないとエラー Point.new(1) #=> missing keyword: :y (ArgumentError)
一部のフィールドだけ置き換えた新しいインスタンスを作りたい場合は、Data#with
が使えます。
pt2 = pt.with(x: 3) p pt2 #=> #<data Point x=3, y=2>
以下余談。
Rubyに新しいクラスを導入する場合、問題になるのが名前です。下手に定数を増やしてしまうと、その名前を使っているgemがあったとき、非互換となってしまう可能性があります。にもかかわらずData
という思い切った名前になったのは、かつてRuby自身がData
というトップレベルの定数を定義していたからです。
これは拡張ライブラリ作者が内部実装に使うために用意されていたクラスだったのですが、なぜか誰も使いませんでした。そして長らく非推奨となっていて、Ruby 3.0くらいでついに削除されました。なので、2年ほど経ってはいますが、今ならまだ他のライブラリとの衝突の可能性が低いのでは?ということで、この名前になりました。
(mame)
Encoding#replicate
が非推奨に
Encoding#replicate
has been deprecated and will be removed in 3.3. [Feature #18949]
エンコーディングをコピーして新しい名前を付ける Encoding#replicate
が非推奨になりました。
もともとは、無限にエンコーディングを増やすことを許すと、性能的に問題になるケースがある、ということで無限に増やす可能性があるけど、結局そんなに使うことないじゃん、ってことで、非推奨になりました。
(ko1)
Encoding::UTF_16
と Encoding::UTF_32
の特別扱いをやめた
- The dummy
Encoding::UTF_16
andEncoding::UTF_32
encodings no longer try to dynamically guess the endian based on a byte order mark. UseEncoding::UTF_16BE
/UTF_16LE
andEncoding::UTF_32BE
/UTF_32LE
instead. This change speeds up getting the encoding of a String. [Feature #18949]
Encoding::UTF_16/32
は、バイトエンディアンが決まっていない「仮」のエンコーディングです。そのため、Ruby 3.1ではこれらのエンコーディングが設定された文字列については、なんらかの文字列処理を行うたびに(エンコーディングを取り出すたびに)文字列先頭の Byte order mark (BOM) をチェックして LE か BE に動的に可能なら変更するようにしていました。
で、この変更は、この特別扱いをやめて、Encoding::UTF_16
については何らかの文字列処理(のエンコーディングを取り出す処理)をしても動的に LE/BE にはしないようにしました。つまり、利用できる文字列処理が凄く少なくなりました。処理前に、先にアプリケーションでチェックして LE か BE か設定しておいてください。
この特別扱いは、実はふつーの(今は多くの文字列は UTF-8 でしょうか)文字列処理も分岐を一個増やすので遅くなっちゃう、という問題が指摘されたので、じゃあやめて速くしてやろ、ってことで、やめました。実際、どれくらい速くなったのかな。
(ko1)
Encoding テーブルが 256 個になった
- Limit maximum encoding set size by 256. If exceeding maximum size,
EncodingError
will be raised. [Feature #18949]
エンコーディングシリーズの最後です。Encoding#replicate
のところで述べた通り、無限にエンコーディングを増やせるようにすると、性能上問題がありました。具体的には、テーブルを拡張する必要があるので、Ractor並列実行でも問題ないように同期するようにしていました。これがあると、一般的な文字列操作のためにエンコーディング情報を取り出す操作が(ちょびっと)遅くなってしまう、という問題がありました。
そこで、テーブル長を256個にする、つまり Ruby インタプリタが扱うエンコーディングの数を 256個を上限とすることで、この同期を排除しました(エンコーディングのロード時には、まだ同期処理が入っています)。
256個に制限されたので、万が一「わしのエンコーディングリストは65,536個あるぞ」みたいな方がいらっしゃいましたらお早めにご連絡ください。例えば、起動時に上限を指定できるようにする、みたいな拡張は可能です。ちなみに、Internet Assigned Numbers Authority (IANA) の Character Sets によると、知られている文字エンコーディング(なのか?)の数は 258 だそうです。全部持ってこられるとちょっとあふれちゃうね。
というわけで、 [Feature #18949] で提案されたエンコーディングに起因する性能向上策は導入されたわけですが、さてどれくらい速くなったんだろうな。
(ko1)
Enumerator.product
が導入された
- Enumerator.product has been added. Enumerator::Product is the implementation. [Feature #18685]
組み合わせを簡単に列挙できるメソッドが導入されました。
Enumerator.product([1, 2], [3, 4]) do |x, y| p [x, y] #=> [1, 3] # [1, 4] # [2, 3] # [2, 4] end
探索アルゴリズムなんかで便利そうですね。
Array#product
というメソッドもあったのですが、こちらはなぜか配列を返してしまうので、探索に使うには不向きなのでした。
(mame)
Exception#detailed_message
が導入された
Exception#detailed_message
has been added. The default error printer calls this method on the Exception object instead of#message
. [Feature #18564]
エラーメッセージを拡張するためのAPIが導入されました。did_you_meanやerror_highlightやsyntax_suggestなど、エラーメッセージを拡張するgem向けのものなので、アプリ開発者が直接使うことはあまりないかもしれません。
いちおう簡単な利用例を載せておきます。
class MyError < Exception def detailed_message(highlight: true, **) super + "\n\n補足情報です" end end raise MyError #=> test.rb:7:in `<main>': MyError (MyError) # # 補足情報です
エラーを扱うフレームワークでは、#message
を呼び出していたところで#detailed_message
を呼び出すように変えたほうがよいかもしれません。
前述の通り、このメソッドはerror_highlightなどエラーメッセージを拡張するgemのために導入しました。というのも、エラーメッセージを下手に上書きすることは非互換になってしまい、いくつかのプロジェクトのテストを失敗させてしまうためです。このあたりの背景はRubyKaigiで話したので、よければそちらを見てください。
https://rubykaigi.org/2022/presentations/mametter.html#day3
(mame)
Hash#shift
が空ハッシュに対しては常に nil
を返すようになった
Hash#shift
now always returns nil if the hash is empty, instead of returning the default value or calling the default proc. [Bug #16908]
Hashの要素をArray#shift
のように一個ずつ取り出す Hash#shift
というメソッドがあります。
h = {k1: 1, k2: 2} p(h.shift) #=> [:k, 1] p h #=> {:k2=>2}
さて、ここで空 Hash に shift するとどうなるでしょうか。
p({}.shift) #=> nil
(おそらく)期待した通り、nil
が返ります。
ここまでは、とくに疑問もないと思うのですが、ではデフォルト値があったらどうしましょう。デフォルト値は、「無いときにはこれを返す」というのがルールでした。
p Hash.new{true}.shift
結論を書くと、こんな感じでした。
- Ruby ~1.6:
nil
- Ruby 1.8~:
true
(デフォルト値だけ。key はなし)
キーと値のペアが返るはずなのに、true
が返ってくるというのが変です。さらに、デフォルト値をかえすときに、ついでレシーバを設定するようなケースだとどうでしょうか。
h = Hash.new {|h, k| h[k] = true } 5.times{ p [h.shift, h] } # Ruby 3.1 での結果 #=> # [true, {nil=>true}] # [[nil, true], {}] # [true, {nil=>true}] # [[nil, true], {}] # [true, {nil=>true}]
なんかよくわからない振動をしています(じっと見ると気持ちがわかります)。
で、これなんか変じゃない? ってことで、「空のときは(デフォルト値は無視して)単にnil
返そう」ということになりました。
余談ですが、私は Hash#shift
というメソッドをこの議論で初めて知りました。
(ko1)
Integer#ceildiv
が導入された
Integer#ceildiv
has been added. [Feature #18809]
切り上げの整数除算をするメソッドが追加されました。
# 5/3 を切り上げる 5.ceildiv(3) #=> 2
もう (a + b - 1) / b
と書かなくてすみますね。
pagenationなんかで便利かもしれません。つまり、1ページにn個のアイテムを持たせる時、全部でm個のアイテムを書くには何ページ必要か、を計算するとか。
(mame)
binding
が取れないコンテキストでは binding
メソッドは nil
を返すようになった
- Kernel#binding raises RuntimeError if called from a non-Ruby frame (such as a method defined in C). [Bug #18487]
これは気にしなくていいです。気にしたい人はきっと悪い人。
悪い人向けの補足です。これまで、C メソッドなど、Ruby で記述されたコンテキストでない場所で Binding を取ろうと、rb_funcall(:binding)
みたいなことをやったら、その呼び出し側の Binding が取れていたんですが(たしか)、Ruby 3.2 から例外があがるようになりました。悪いことはするもんじゃないですね。
冗談はおいといて、まぁほとんど踏むことはないんじゃないかと思います。
(ko1)
String
をバイト列として扱うメソッドがいくつか増えた
MatchData#byteoffset
has been added. [Feature #13110]
正規表現にマッチした位置を、「何文字めか」ではなく「何バイトめか」で返すメソッドが増えました。
"あいうえお" =~ /い/ # 「い」は3バイトめから6バイトめにある $~.byteoffset(0) #=> [3, 6]
String#byteindex
and String#byterindex have been added. [Feature #13110]
こちらはString#index
やString#rindex
と似ていますが、やはり「何バイトめか」を返すメソッドです。
"あいうえお".byteindex("い") #=> 3
おそらくTextbringerというRuby製エディタで必要だった機能と思われます。
(mame)
String#bytesplice
has been added. [Feature #18598]
これも、文字列をバイト単位で文字列を編集するためのメソッドです。
s = 'hello' p s.bytesplice(2, 2, 'LL') # s の 2 文字目から 2 文字分を 'LL' に置き換え #=> "LL" p s #=> "heLLo"
これ、なんで self じゃなくて "LL"
返すんだろ。
(聞いてみたら、String#[]=
にあわせたんだそうです。でも便利なのかなぁ?)
指定した箇所がコードポイントの途中だったりするとエラーがでるそうです。
(ko1)
Module.used_refinements
が導入された
Module.used_refinements
has been added. [Feature #14332]
現在実行中の箇所で、どの refinements が using
されているか返す Module.used_refinements
が追加されました。
module R1 refine String do end refine Array do end end module R2 refine Array do end end module R3 refine Hash do end refine Regexp do end end using R1 using R2 p Module.used_refinements #=> [#<refinement:String@R1>, #<refinement:Array@R2>, #<refinement:Array@R1>] using R3 p Module.used_refinements #=> [#<refinement:String@R1>, #<refinement:Array@R2>, #<refinement:Array@R1>, #<refinement:Hash@R3>, #<refinement:Regexp@R3>]
返り値は、Ruby 3.1 で導入された Refinement クラスです。
(ko1)
Module#refinements
が導入された
Module#refinements
has been added. [Feature #12737]
そのモジュールが含む Refinement クラス一覧を取る Module#refinements
が導入されました。
module R1 refine String do end refine Array do end end p R1.refinements #=> [#<refinement:String@R1>, #<refinement:Array@R1>] p String.refinements #=> []
何に使うんでしょうね。
(ko1)
Refinement#refined_class
が導入された
Refinement#refined_class
has been added. [Feature #12737]
ある Refinement がどのクラスを refine しているか返します。上で紹介した Module#refinements
を使ってサンプルコードを書いてみます。
module R1 refine String do end refine Array do end end p R1.refinements.map{|r| r.refined_class} #=> [String, Array]
(ko1)
Module#const_added
が追加された
Module#const_added
has been added. [Feature #17881]
定数が定義されたときに呼ばれる Module#const_added
が導入されました。
class C def self.const_added name p name super end end class C C_CONST = :C #=> :C_CONST class D #=> :D_CONST D_CONST = :D #=> ここでは呼ばれない end end
この手のフックは別の人も使っているかもしれない(利用している複数のモジュールをprepend
しているかもしれない)ので、super
を呼んでおくのが礼儀です。
この機能、何に使うといいですかねえ。あんまり「これやるから入れてほしい」って提案じゃなかった気がするんですよね。
(ko1)
undef されたメソッド一覧を返す Module#undefined_instance_methods
が導入された
Module#undefined_instance_methods
has been added. [Feature #12655]
Module#undef_method
(もしくは undef
式)と Module#remove_method
の違いをご存じでしょうか。どちらも、メソッドを使えなくしそうなんですが、Module#remove_method
は単純で、そのモジュール(クラス)に定義されているあるメソッドを削除します。そのメソッドを呼び出そうとすると、継承関係の親クラスにあるメソッドが呼ばれます。
class C def f = :C end class D < C def f = :D end D.remove_method(:f) p D.new.f #=> :C (C#f が呼ばれている)
Module#undef_method
(もしくは undef
式)は、「継承関係があろうと、そこでメソッド探索は失敗」というメソッドエントリを作成します。
class C def f = :C end class D < C def f = :D end D.undef_method :f p D.new.f #=> undefined method `f' for #<D:0x0000027b746213c0> (NoMethodError) # (C#f は呼ばれない)
というわけで、そんな undef
されたメソッド一覧を返すのが Module#undefined_instance_methods
です。
class C def f = :C end class D < C def f = :D end D.undef_method :f p D.undefined_instance_methods #=> [:f]
これも、なんで欲しかったんだろう。
(ko1)
Proc のこまごました話
Proc#dup
returns an instance of subclass. [Bug #17545]
Proc#dup
が、もし Proc の サブクラスだった場合、そのサブクラスのインスタンスを返すようになりました。しかし、Proc
のサブクラスなんて作ることあるんかな...。
Proc#parameters
now accepts lambda keyword. [Feature #15357]
Proc は、生成方法が lambda(->
)と proc(Proc.new
)で、ちょっと引数の扱いが変わります。具体的には、lambda だとメソッドのように厳密に(引数の数が違うと例外)、proc だとブロックのようにあいまいに(引数の数が違ってもなんとなく動かす)なるのですが、Proc#parameter
は、lambdaかprocかで結果が変わっていました。ただ、あるケースで proc で作った Proc であっても lambda のような引数が欲しい、ということがあったそうで、この挙動を指定する lambda
キーワードが導入されました。
p proc{|a|}.parameters #=> [[:opt, :a]] p proc{|a|}.parameters(lambda: true) #=> [[:req, :a]] p lambda{|a|}.parameters #=> [[:req, :a]] p lambda{|a|}.parameters(lambda: false) #=> [[:opt, :a]]
多分、逆(lambda だけど proc 形式で欲しい)はおまけですかね?
(ko1)
FreeBSD で Proc::RLIMIT_NPTS
が導入された
- Process
- Added
RLIMIT_NPTS
constant to FreeBSD platform
- Added
man によると「このユーザ id が作成することを許可する疑似端末の最大の数」とのことです。
(ko1)
正規表現エンジンにメモ化の最適化が導入された
- The cache-based optimization is introduced. Many (but not all) Regexp matching is now in linear time, which will prevent regular expression denial of service (ReDoS) vulnerability. [Feature #19104]
- Regexp.linear_time? is introduced. Feature #19194
正規表現エンジンが大幅に改良されました。最悪計算量が(多くの場合で)入力文字列長に対して線形になります。これにより、いわゆるReDoSと呼ばれる脆弱性の可能性を大きく減らすことができます。
人工的な例ですが、次の正規表現マッチングはRuby 3.1では10秒くらいかかります。これがRuby 3.2では一瞬で返ります。
/^(a|a)+$/ =~ "a" * 28 + "b"
時間がかかっていた理由をざっくりいうと、正規表現エンジンが同じマッチング失敗を何度も繰り返すせいでした。今回の最適化は、一度失敗したマッチングを記憶するようにして、同じ失敗を二度と繰り返さないようにしたという感じです。
これは @makenowjust さんがクックパッドにインターンに来て実装してくれた大成果になります。詳しくは作者本人が記事を書いてくれましたので、ご参照ください、
いくつかのコードを用いた実験によると、おおよそ9割の正規表現が最適化できる(つまり線形時間に抑えられる)ことがわかりました。逆に言うと、1割程度の正規表現は最適化できないので、「RubyではもうReDoSの心配は一切しなくてよい」というわけではないことにご注意ください。念のため。
Regexp.linear_time?
というメソッドで、正規表現が最適化対象かどうかを判定できます。
# 最適化できる p Regexp.linear_time?(/^(a|a)+$/) #=> true # 後方参照の \1 があるので最適化できない p Regexp.linear_time?(/^(a|a)+\1$/) #=> false
(mame)
正規表現マッチングにタイムアウトが指定できるようになった
Regexp.timeout=
has been added. Also, Regexp.new new supports timeout keyword. See [Feature #17837]
正規表現マッチングの時間上限を設定できるAPIが導入されました。もうひとつのReDoS緩和策です。
# 正規表現マッチングを最長1秒で打ち切る Regexp.timeout = 1 # 前述の最適化ができない正規表現で時間がかかると例外になる /^(a|a)+\1$/ =~ "a" * 28 + "b" #=> regexp match timeout (Regexp::TimeoutError)
最適化できない1割の可能性が心配なら、これも併用すると良いかもしれません。
(mame)
正規表現のフラグを文字列で指定できるようになった
Regexp.new
now supports passing the regexp flags not only as an Integer, but also as a String. Unknown flags raise ArgumentError. Otherwise, anything other thantrue
,false
,nil
or Integer will be warned. [Feature #18788]
Regexp.new
において、正規表現のフラグを文字列で指定できるようになりました。
Regexp.new("foo", "i") #=> /foo/i Regexp.new("foo", "m") #=> /foo/m Regexp.new("foo", "im") #=> /foo/im
以前は Regexp.new("foo", Regexp::IGNORECASE | Regexp::MULTILINE)
などと書く必要がありました。
なお、シンボルで :im
などとは書けません。
(mame)
RubyVM::AbstractSyntaxTree.parseにモードが増えた
- RubyVM::AbstractSyntaxTree
- Add
error_tolerant
option forparse
,parse_file
andof
. [Feature #19013] - Add
keep_tokens
option forparse
,parse_file
andof
. Add#tokens
and#all_tokens
for RubyVM::AbstractSyntaxTree::Node [Feature #19070]
- Add
Rubyのコードをパースするためのメソッドに新たなモードが増えました。
- syntax errorのあるコードでもなんとなくパースするモード(
error_tolerant: true
で有効になる) - 各ノードに対応するトークン列を保持するモード(
keep_tokens: true
で有効になる)
どちらもRubyのIDEサポートなどを実装することを念頭においた拡張になっています。詳しくは作者のyui-knkさんの記事をご参照ください。
(mame)
Setが組み込み(?)になった
Set
is now available as a built-in class without the need forrequire "set"
. [Feature #16989] It is currently autoloaded via the Set constant or a call to Enumerable#to_set.
set
ライブラリが組み込みになりました。
が、現状では、インタプリタがデフォルトでrequire "set"
をするだけという感じです*1。といっても、Railsがrequire "set"
をするらしいので、今更これの恩恵を受ける人はほとんどいないかもしれません。
残念ながらいまのところ、「Cで書き直されて高速」みたいな話はありません。{ 1, 2, 3 }
みたいな専用の記法が導入されたわけでもありません。今後に期待ですね。
(mame)
Unicode 15.0.0 になった
- Update Unicode to Version 15.0.0 and Emoji Version 15.0. [Feature #18639] (also applies to Regexp)
だそうです。
(ko1)
String#-@
にString#dedup
という別名がついた
String#dedup
has been added as an alias toString#-@
. [Feature #18595]
文字列の重複を排除(deduplicate)するString#-@
があったんですが、この別名で String#dedup
が入りました。
s1 = -'foo' s2 = 'foo'.dedup p s1.object_id == s2.object_id #=> true
(ko1)
Structの初期化にキーワードが使えるようになった
- A
Struct
class can also be initialized with keyword arguments withoutkeyword_init: true
on Struct.new [Feature #16806]
Structの初期化でキーワードを使えるようになりました。
Point = Struct.new(:x, :y) Point.new(y: 2, x: 1) #=> #<struct Point x=1, y=2>
Struct.new(:x, :y, keyword_init: true)
などとすれば同じような挙動は実現できていたのですが、このオプションが不要になります。
少し細かいことを言うと、keyword_init
はtrue/false/nilでそれぞれ意味が微妙に違います。
keyword_init: true
→Point.new(x: 1, y: 2)
で初期化できる、Point.new(1, 2)
はエラーkeyword_init: nil
→Point.new(x: 1, y: 2)
でもPoint.new(1, 2)
でも初期化できるkeyword_init: false
→Point.new(1, 2)
で初期化できる、Point.new(x: 1, y: 2)
は要注意の挙動(次の動作例を参照)
# Ruby 3.1 の挙動:Point.new({x: 1, y: 2}) と同じ扱い Point.new(x: 1, y: 2) #=> #<struct A x={:y=>2, :x=>1}, y=nil>
今回の変更は、keyword_init
のデフォルトがfalseからnilに変わったということになります。
(mame)
Thread.each_caller_location
が導入された
Thread.each_caller_location
is added. [Feature #16663]
自スレッドのバックトレース情報を取る Kernel#caller_locations
というメソッドがあります。
def f = g def g = pp(caller_locations) f #=> ["t.rb:1:in `f'", "t.rb:3:in `<main>'"]
で、これらのメソッドで全部取るんじゃなくて、必要な場所だけフィルタしたい、最初だけ欲しい、というときに、毎回ブロックに渡すインターフェースがあると便利そうです。で、最終的に着地点として Thread.each_caller_location
というメソッドが導入されました。
def f = g def g = Thread.each_caller_location{|loc| p loc} f #=> "t.rb:1:in `f'" # "t.rb:3:in `<main>'"
この例では、全部 p
で出力していますが、フィルタしたり、途中で break したりしてもいいわけです。
(ko1)
Queue#pop(timeout: sec)
とかが導入された
- Thread::Queue
Thread::Queue#pop(timeout: sec)
is added. [Feature #18774]
- Thread::SizedQueue
Thread::SizedQueue#pop(timeout: sec)
is added. [Feature #18774]Thread::SizedQueue#push(timeout: sec)
is added. [Feature #18944]
Queue#pop
とか SizedQueue#push/pop
とかは、待ちが生じる操作ですが、そこに timeout: sec
キーワード引数でタイムアウトが設定できるようになりました。
p Queue.new.pop(timeout:1) #=> nil
タイムアウトしたら、例外ではなく、nil
が返ります。
(ko1)
Timeがパターンマッチ可能になった
Time#deconstruct_keys
is added, allowing to use Time instances in pattern-matching expressions [Feature #19071]
要するに、こんなのが書けるようになりました。
# 1時2分3秒に実行した場合 Time.now => { hour:, min:, sec: } p hour #=> 1 p min #=> 2 p sec #=> 3
指定できるキーは :year、:month、:day、:yday、:wday、:hour、:min、:sec、:subsec、:dst、:zone だそうです。
- Added
Date#deconstruct_keys
andDateTime#deconstruct_keys
same as [Feature #19071]
DateやDateTimeも同様らしいです。
(mame)
Time.new
が Time#inspect
の結果の文字列フォーマットで時間を指定できるようになった
- Time.new now can parse a string like generated by Time#inspect and return a Time instance based on the given argument. [Feature #18033]
Time.new
でTime.parse
みたいなことができるようになりました。
Time.new("2022-12-25 15:00:00") #=> 2022-12-25 15:00:00 +0900 Time.new("2022-12-25 15:00:00 +0000") #=> 2022-12-25 15:00:00 +0000 Time.new("2022-12-25 15:00:00Z") #=> 2022-12-25 15:00:00 UTC
なんでもかんでも時刻として無理やり解釈しようとするTime.parse
と違って、Time.new
はフォーマットに厳格です。たとえば桁数は省略できません。
Time.new("2022-12-25 15:00:0") #=> two digits sec is expected after `:': :0 (ArgumentError)
ただし、ISO 8601の時刻表記のような "T" は許容されます。
# iso8601のような "T" は許容される Time.new("2022-12-25T15:00:00Z") #=> 2022-12-25 15:00:00 UTC
"T" のかわりに空白を許容する Time.iso8601
に近いかも。強いて特典をあげるなら、Time.new
は C 言語で書かれているので速いです。
(mame)
SyntaxError#path
が追加された
SyntaxError#path
has been added. [Feature #19138]
SyntaxErrorを引き起こした原因のファイル名を得られるようになりました。
begin load "broken.rb" rescue SyntaxError p $!.path #=> "broken.rb" end
後述するsyntax_suggestのために導入されました。一般的な用途は、あんまりないかも。
(mame)
CメソッドをフックしたときのTracePoint#binding
がnil
を返すようになった
TracePoint#binding
now returnsnil
forc_call
/c_return
TracePoints. [Bug #18487]
Kernel#binding
の話と似ています。これまで、c_call
/creturn
フックでは、呼び出し元の Ruby コードの Binding が返ってきましたが、これを nil
にしています。
TracePoint.new(:c_call){|tp| p [tp, tp.binding] }.enable{ p 1 } #=> # [#<TracePoint:c_call `p' t.rb:4>, nil] # [#<TracePoint:c_call `to_s' t.rb:4>, nil] # 1
多分、問題ないと思うんですが、もしはまったらゴメンなさい。
(ko1)
TracePoint#enable(target_thread: Thread.current)
がデフォルトになった
TracePoint#enable
target_thread
keyword argument now defaults to the current thread if a block is given andtarget
andtarget_line
keyword arguments are not passed. [Bug #16889]
TracePoint#enable
メソッドを用いると、その TracePont が有効になる対象をいろいろ決められるのですが、TracePoint#enable(target_thread:)
で有効にするスレッドを指定できます。
今までは、これがないと全スレッドに有効になっていたんですが、enable
にブロックがわたされているときは、まぁたいていはそのブロックを実行している自スレッドだけに効かせたいでしょう、ということで、target_thread: Thread.current
をデフォルトにするようにしました。nil
にすると全スレッドに効きます。また、target
、target_line
キーワードなどがついていても、全スレッドに有効になります。
Thread.new{loop{1.inspect}} TracePoint.new(:c_call){|tp| p tp # 別スレッドには効かない }.enable{ sleep 0.5 } p :term
(ko1)
UnboundMethod
がレシーバの情報を持たなくなった
UnboundMethod#==
returnstrue
if the actual method is same. For example,String.instance_method(:object_id) == Array.instance_method(:object_id)
returnstrue
. [Feature #18798]UnboundMethod#inspect
does not show the receiver ofinstance_method
. For exampleString.instance_method(:object_id).inspect
returns"#<UnboundMethod: Kernel#object_id()>"
(was"#<UnboundMethod: String(Kernel)#object_id()>"
).
これは多分興味ある人ほとんどいないと思うんだけど...。
これまで、Module#instance_method(method_name)
で取り出した UnboundMethod
インスタンスは、レシーバの情報をもっていました。
# Ruby 3.1 p m1 = String.instance_method(:object_id) #=> #<UnboundMethod: String(Kernel)#object_id()> p m2 = Array.instance_method(:object_id) #=> => #<UnboundMethod: Array(Kernel)#object_id()> p m1 == m2 #=> false
つまり、String や Array から取り出したメソッドだということがわかるようにしていたのです。 また、それらは別物とされていました。
ただ、そもそもそんな区別いる? という議論がされ、要らんのでは、ということになりました。
# Ruby 3.2 p m1 = String.instance_method(:object_id) #=> #<UnboundMethod: Kernel#object_id()> p m2 = Array.instance_method(:object_id) #=> #<UnboundMethod: Kernel#object_id()> p m1 == m2 #=> true
(ko1)
GC.latest_gc_info
が need_major_by
を返すようになった
- Expose
need_major_gc
viaGC.latest_gc_info
. GH-6791
GC.latest_gc_info
は直前の GC の実行状況を Hash で返すメソッドですが、その状況の中に、:need_major_by
が追加されました。これは、「次の GC が major GC と決まっていたら、その決定理由を返す」という情報です。
pp GC.latest_gc_info #=> # {:major_by=>:oldgen, :need_major_by=>nil, # nil なので(現時点では)次の GC は major GC を予定していない。 :gc_by=>:newobj, :have_finalizer=>false, :immediate_sweep=>false, :state=>:none}
次の GC が major GC かどうかは、直前の GC の実行状況によって「逼迫してそうだから次は major GC にしよ」とどこかのタイミングで決まるので、その情報を返すためのものです。
ただ、need_major_by
は「次のGC」の話でgc_by
は「前回のGC」の話で、区別しづらい良くない名前になってしまっています。気づかないうちに入ってしまっていました。不覚。
(ko1)
ObjectSpace.dump_all
が shape を返すようになった
ObjectSpace.dump_all
dump shapes as well. [GH-6868]
主にヒープの状況の詳細な分析に利用するための ObjectSpace.dump_all
の出力が、後述する object shape の情報を含むようになりました。
(ko1)
■stdlibのアップデート
BundlerがPubGrubバージョン解決アルゴリズムを利用するようになった
- Bundler
- Bundler now uses [PubGrub] resolver instead of [Molinillo] for performance improvement.
- Add support for
bundle gem --ext=rust
command. [GH-rubygems-6149]
BundlerはGemfileやgemspecに書かれた依存性を満たすバージョンを決定する必要があるのですが、これを行うアルゴリズムを早くて賢いものに変えたそうです。
このアルゴリズムはPubGrubと呼ばれていて、解説記事を眺めたところ、“unit propagation”、“logical resolution”、“conflict-driven clause learning”など、SATソルバの実装で聞くテクニックを使っているみたいです。
(mame)
CGI.escapeURIComponentが導入された
CGI.escapeURIComponent
andCGI.unescapeURIComponent
are added. [Feature #18822]
CGI.escape
とCGI.unescape
の亜種が導入されました。
CGI.escapeURIComponent("foo/bar baz") #=> "foo%2Fbar%20baz" CGI.escape("foo/bar baz") #=> "foo%2Fbar+baz"
違いは空白の扱いだけです。CGI.escape
は空白を+
に置換えますが、CGI.escapeURIComponent
は%20
にします。
Rubyらしからぬメソッド名は、JavaScriptのescapeURIComponent関数と同じであることを明示するために選ばれました。
(mame)
eval
されたコードのカバレッジを測定できるようになった
Coverage.setup
now acceptseval: true
. By this,eval
and related methods are able to generate code coverage. [Feature #19008]Coverage.supported?(mode)
enables detection of what coverage modes are supported. [Feature #19026]
コードカバレッジを測定するCoverage
ライブラリが、require
やload
したファイルだけでなく、eval
されたコードのカバレッジも測定できるようになりました。Coverage.start(lines: true, eval: true)
などと初期化する必要があります。
どう便利かというと、要するに、ERBのカバレッジがなんとなく取れるようになります。次のPRを見てください。
https://github.com/simplecov-ruby/simplecov/pull/1037
使いかたとしては、simplecov 0.22.0以降で次のように設定することで有効にできます。
SimpleCov.start do enable_coverage_for_eval end
実際にカバレッジ測定しているのはERBが生成したRubyコードで、その行番号をむりやりERBのカバレッジとして表示しているので、やや不自然に見えることもあるかもしれません。本格的にやるには、ERBにsourcemap的なものを吐かせる必要があるのだろうなあ。
(mame)
ERB::Util.html_escape
が速くなった
ERB::Util.html_escape
is made faster thanCGI.escapeHTML
.- It no longer allocates a String object when no character needs to be escaped.
- It skips calling
#to_s
method when an argument is already a String. ERB::Escape.html_escape
is added as an alias toERB::Util.html_escape
, which has not been monkey-patched by Rails.
ERB::Util.url_encode
is made faster usingCGI.escapeURIComponent
.
ERB::Util.html_escape
が速くなりました。次のコードでRuby 3.1.3では1.3秒、Ruby 3.2.0では0.67秒くらいでした。
s = "foo" 10000000.times { ERB::Util.html_escape(s) }
ただし、このために少しだけ非互換があります。
- エスケープ対象の文字が1文字もなかったら、引数の文字列をdupせずにそのまま返す(返り値を破壊的に使っている人は注意)
- 引数が文字列だったときに
#to_s
を呼ぶのをやめた
あとついでにERB::Util.url_encode
も速くなったそうです。前述のCGI.escapeURIComponent
を使ったとのこと。
(mame)
erbコマンドの-Sオプションが消された
-S
option is removed fromerb
command.
erbコマンドからセーフレベルを指定するオプションが消えました。セーフレベル自体はRuby 3.0で消されています。コマンドライン引数はdeprecatedとして残されていましたが、今回ついに消えたようです。
(mame)
FileUtils.ln_sr
が導入された
- Add
FileUtils.ln_sr
method andrelative:
option to FileUtils.ln_s. [Feature #18925]
ターゲットディレクトリからの相対パスでシンボリックリンクを生成するメソッドが増えました。
FileUtils.ln_s("src/file", "dest/symlink")
とすると、dest/symlink
はdest/src/file
へのシンボリックリンクになりますが、これは意図しない結果かもしれません。dest
と同じディレクトリにあるsrc
の中のfile
へシンボリックリンクをはりたかったら、FileUtils.ln_s("../src/file", "dest/symlink")
とする必要がありました。つまり、第一引数には第二引数からの相対パスを示す必要がありました。これからはFileUtils.ln_sr("src/file", "dest/symlink")
と書くだけですみます。
(mame)
IRBに多数のコマンドが導入された
- Added many of new commands and improvements. see [ruby-3-2-irb]
たくさんの新しいコマンドが導入されています。詳細は下記の記事をご参照ください。
余談:irb は、これまでは Ruby の式しか実行できませんでした。コマンドに見えるものも、すべて Ruby のメソッドとして実装されてきていました。今回のリリースでは、この制限をとっぱらって、(pryなどのように)独自の構文を導入しました。これによって、pry の$
といった機能が使えるようになっています。
個人的には、Ruby の式に限定していた理由をよく知らないのですが(単純性のため?)、この限定を解除したデメリットがないか、ちょっと興味があります。
(ko1)
Net::Protocolのバッファ実装が改善された
- Net::Protocol
- Improve
Net::BufferedIO
performance. [GH-net-protocol-14]
- Improve
net-httpなどで使われているバッファの実装が効率的になったようです。以前はString#slice!
を使って文字列を切り出していたため、長いバッファに対して先頭から一行ずつ読むような場合に遅くなっていたとのこと。改善後は切り出さずに先頭からのオフセットを管理するようにしたらしい。
(mame)
Pathname#lutimeが導入された
- Added
Pathname#lutime
. [GH-pathname-20]
Pathnameに最終アクセス時刻と更新時刻を変更するメソッドが追加されました。utime
との違いは、シンボリックリンクに対してlutime
を行ったとき、参照先のファイルではなくシンボリックリンク自身の時刻を変更するところです。
File.lutime
は以前からあったので、それのPathname版です。
(mame)
Socket
に定数が追加された
- Socket
- Added the following constants for supported platforms.
SO_INCOMING_CPU
SO_INCOMING_NAPI_ID
SO_RTABLE
SO_SETFIB
SO_USER_COOKIE
TCP_KEEPALIVE
TCP_CONNECTION_INFO
- Added the following constants for supported platforms.
Socket にいろんな定数が追加されました。詳しくないから詳細わかりません、ゴメン。
(ko1)
syntax_suggest gemが導入された
- The feature of
syntax_suggest
formerlydead_end
is integrated in Ruby. [Feature #18159]
end
が足りなかったり多すぎたりした場合に、親切なエラーメッセージを出すようにするsyntax_suggest gem(旧名 dead_end gem)が導入されました。
class C def foo end def bar # endが欠けている if cond? end def baz end end
$ ruby test.rb test.rb: --> /tmp/wrong_syntax.rb Unmatched keyword, missing `end' ? 1 class C 5 def bar > 7 if cond? 8 end 12 end /tmp/wrong_syntax.rb:12: syntax error, unexpected end-of-input (SyntaxError)
if
に対するend
が足りていないことを、行単位の要約によって示してくれています。
dead_end gemはRubyKaigi Takeout 2021で作者のRichard Schneemanが発表していたものです。無事入ってよかったですね。
(mame)
WindowsでUNIXSocketをサポートした(かも)
- Add support for UNIXSocket on Windows. Emulate anonymous sockets. Add support for File.socket? and File::Stat#socket? where possible. [Feature #19135]
最近の Windows では UNIX Domain Socket (ぽいもの)をサポートしているそうです。へー、知らなかった。 というわけで、対応していれば対応する、というパッチが入ったそうです。すごい。
(ko1)
ライブラリのアップデート
The following default gems are updated.
- RubyGems 3.4.1
- abbrev 0.1.1
- benchmark 0.2.1
- bigdecimal 3.1.3
- bundler 2.4.1
- cgi 0.3.6
- csv 3.2.6
- date 3.3.3
- delegate 0.3.0
- did_you_mean 1.6.3
- digest 3.1.1
- drb 2.1.1
- english 0.7.2
- erb 4.0.2
- error_highlight 0.5.1
- etc 1.4.2
- fcntl 1.0.2
- fiddle 1.1.1
- fileutils 1.7.0
- forwardable 1.3.3
- getoptlong 0.2.0
- io-console 0.6.0
- io-nonblock 0.2.0
- io-wait 0.3.0
- ipaddr 1.2.5
- irb 1.6.2
- json 2.6.3
- logger 1.5.3
- mutex_m 0.1.2
- net-http 0.3.2
- net-protocol 0.2.1
- nkf 0.1.2
- open-uri 0.3.0
- open3 0.1.2
- openssl 3.1.0
- optparse 0.3.1
- ostruct 0.5.5
- pathname 0.2.1
- pp 0.4.0
- pstore 0.1.2
- psych 5.0.1
- racc 1.6.2
- rdoc 6.5.0
- readline-ext 0.1.5
- reline 0.3.2
- resolv 0.2.2
- resolv-replace 0.1.1
- securerandom 0.2.2
- set 1.0.3
- stringio 3.0.4
- strscan 3.0.5
- syntax_suggest 1.0.2
- syslog 0.1.1
- tempfile 0.1.3
- time 0.2.1
- timeout 0.3.1
- tmpdir 0.1.3
- tsort 0.1.1
- un 0.2.1
- uri 0.12.0
- weakref 0.1.2
- win32ole 1.8.9
- yaml 0.2.1
- zlib 3.0.0
The following bundled gems are updated.
- minitest 5.16.3
- power_assert 2.0.3
- test-unit 3.5.7
- net-ftp 0.2.0
- net-imap 0.3.4
- net-pop 0.1.2
- net-smtp 0.3.3
- rbs 2.8.2
- typeprof 0.21.3
- debug 1.7.1
いろいろアップデートされました。詳細は各 gem のリリースノートを見てください。
自分が見ているので debug.gem のアップデートだけ紹介すると、新しいデバッグコマンド(until
など)が足されたり、VSCode/Chrome連携がよくなったり、たくさんのバグフィックスが入っていたりします。
(ko1)
■サポートプラットフォーム
- WebAssembly/WASI is added. [Feature #18462]
RubyがWebAssembly/WASIに対応しました。かんたんに言えば、Rubyがブラウザ上で動いて楽しいです。
しかしWebAssembly/WASIはブラウザにとどまらず、いろいろな活用が期待されているプラットフォームで、いわゆるエッジコンピューティングやプラグイン機構などでこれから使われていく、かもしれません。今回のRubyの対応は、そういう将来に備えた布石です。
RubyKaigi 2022の初日のキーノートで作者の @kateinoigakukun が発表していたので、記憶に残っている人も多いのではないかと思います。実装など詳しくはそちらの発表資料を御覧ください。
クックパッドはRubyKaigiのブース企画として、このWebAssembly/WASI対応を使ったコードパズルを作りました。興味あれば、次の2つの記事をご覧ください。
(mame)
■非互換
String#to_c
のわずかな変更
String#to_c
currently treat a sequence of underscores as an end of Complex string. [Bug #19087]
"1__0".to_c
と書いたとき、Ruby 3.1では(10+0i)
が帰っていましたが、Ruby 3.2からは(1+0i)
が返ります。
(mame)
ENV.clone
が例外を投げる
- Now
ENV.clone
raisesTypeError
as well asENV.dup
[Bug #17767]
ENV.clone
が例外を投げるようになりました。
(mame)
削除された定数
The following deprecated constants are removed.
Fixnum
andBignum
[Feature #12005]Random::DEFAULT
[Feature #17351]Struct::Group
Struct::Passwd
消すぞ消すぞ、と言われていた古い定数が削除されました。
(ko1)
削除されたメソッド
The following deprecated methods are removed.
Dir.exists?
[Feature #17391]File.exists?
[Feature #17391]Kernel#=~
[Feature #15231]Kernel#taint
,Kernel#untaint
,Kernel#tainted?
[Feature #16131]Kernel#trust
,Kernel#untrust
,Kernel#untrusted?
[Feature #16131]Method#public?
,Method#private?
,Method#protected?
,UnboundMethod#public?
,UnboundMethod#private?
,UnboundMethod#protected?
[Bug #18729] [Bug #18751] [Bug #18435]
上記のメソッドが削除されました。ほとんどのメソッドは呼ぶと警告が出ていたものになります。
Dir.exists?
は代わりにDir.exist?
を使うようにしてください。Rubyのメソッド名は原則として原型を使うので、三単現のsがあるものは削除となりました。
Method#public?
などはRuby 3.1で導入されたメソッドだったのですが、キャンセルして削除されました。これには次のような事情があります。
Ruby 3.2の仕様の議論ではメソッドオブジェクトのコーナーケースの整理が一大トピックとなっていて、非常に長時間をかけて細かい議論が行われていました。その中で、「メソッドの可視性というのは、メソッド自体が持つものではなく、それが所属するクラスが持つものである」という哲学的な整理にいたり、「Methodクラスがpublicかどうかを返すのはおかしい」ということで、Ruby 3.1で導入されたばかりだったということもあり、あっさり削除されることなりました。なお、この一大トピックの全体の結論としては、「Ruby 3.0のときの挙動がほぼ正しかった」となったので、内容は割愛します。
(mame)
拡張ライブラリのソースコード非互換性
- Extension libraries provide PRNG, subclasses of Random, need updates. See [PRNG update] below for more information. [Bug #19100]
疑似乱数生成器を自分で実装するためのインターフェースに修正が入りました。興味ある人は資料を見てください。
(ko1)
エラーメッセージのエスケープをやめた
- Ruby no longer escapes control characters and backslashes in an error message. [Feature #18367]
Rubyはエラーメッセージを表示する時に制御文字やバックスラッシュをエスケープしていたのですが、これをやめました。error_highlightでバックスラッシュのある行を表示するとき、下線位置がずれることがあったのですが、この変更でずれなくなります。
(mame)
include
が絡んだクラス・モジュールを定義するときの定数解決
- When defining a class/module directly under the Object class by class/module statement, if there is already a class/module defined by
Module#include
with the same name, the statement was handled as "open class" in Ruby 3.1 or before. Since Ruby 3.2, a new class is defined instead. [Feature #18832]
module X module Y end end include X p Y #=> X::Y class Y end #=> # Ruby 3.1: class X::Y とオープンクラスをやろうとして、モジュールなのでエラー # Ruby 3.2: class Y を実行し、新しく ::Y を定義
このプログラムでは、トップレベルで X
を include しているので、トップレベルで Y
が参照できます(X::Y
)。
このとき、class/module
文でクラスやモジュールを定義しようとすると、X::Y
に対してオープンしようとしていましたが、Ruby 3.2 からはまったく新しい Y
が定義されるようになりました。
(ko1)
■stdlibの非互換
libyamlやlibffiのソースコードのバンドルをやめた
- Psych no longer bundles libyaml sources. And also Fiddle no longer bundles libffi sources. Users need to install the libyaml/libffi library themselves via the package manager like apt, yum, brew, etc. [Feature #18571]
Rubyのパッケージはlibyamlやlibffiのソースコードをバンドルしていて、これらがインストールされていない環境ではバンドル版をビルドして利用していたのですが、このバンドルをやめました。今後は各自でシステムにインストールしてください。
たしかRuby 2.0でPsychが導入されるとき、libyamlが簡単にインストールできない環境のためにバンドルを始めたのですが、libyamlに脆弱性が発見されたときにRubyもリリースを検討しないといけないなど、意外とコストが高かったので、バンドルをやめることは長らく懸案となっていました。
Windowsでlibffiを入れるためにパッチが必要と思われていたのがバンドル中止に踏み切れなかった理由だったのですが、libffi本家側の改善でそのパッチが不要になっており、vcpkgなどでも簡単にインストールできることがわかったので、今回思い切ってバンドル中止となりました。
(mame)
CGI::Cookieがname/path/domainの文字列を検査するようになった
- Check cookie name/path/domain characters in
CGI::Cookie
. [CVE-2021-33621]
CGI::Cookieを生成する際、属性値の中身をRFC 6265にもとづいて検査するようになりました。これは脆弱性の可能性を防ぐためです。
バグ発見者の徳丸浩さんが記事を書いてくれましたので、あわせてご参照ください。
(mame)
URI.parseが少しだけ変更
URI.parse
return empty string in host instead of nil. [sec-156615]
次のような文字列をURLとしてパースした時に、ホスト名がnilだったところ、空文字列が返るようになりました。
URI("http:////example.com/").host #=> ""
(mame)
■C APIの更新
更新されたC API
The following APIs are updated.
- PRNG update
rb_random_interface_t
in ruby/random.h updated and versioned. Extension libraries which use this interface and built for older versions need to rebuild with addinginit_int32
function.
疑似乱数生成器を提供するためのAPIが変わったそうです。
(ko1)
追加されたC API
VALUE rb_hash_new_capa(long capa)
was added to created hashes with the desired capacity.
事前に指定された分のメモリを確保しておく rb_hash_new_capa()
が導入されました。
rb_internal_thread_add_event_hook
andrb_internal_thread_add_event_hook
were added to instrument threads scheduling. The following events are available:RUBY_INTERNAL_THREAD_EVENT_STARTED
RUBY_INTERNAL_THREAD_EVENT_READY
RUBY_INTERNAL_THREAD_EVENT_RESUMED
RUBY_INTERNAL_THREAD_EVENT_SUSPENDED
RUBY_INTERNAL_THREAD_EVENT_EXITED
スレッドが停止したり実行可能になったり実際に実行再開したりするときに内部的なイベントを発行して、それをトラップすることでスレッドの挙動を計測することができるようになりました。
実際にこれを使ったツールを作っていただいています(ivoanjo/gvl-tracing: Get a timeline view of Global VM Lock usage in your Ruby app )。
rb_debug_inspector_current_depth
andrb_debug_inspector_frame_depth
are added for debuggers.
デバッガのために、スタックフレームの深さに関する情報を返す API が追加されました。
(ko1)
削除されたC API
The following deprecated APIs are removed.
rb_cData
variable.- "taintedness" and "trustedness" functions. [Feature #16131]
rb_cData
というグローバル変数が削除されました。これはData
クラスの項で少し触れた、過去のData定数に入っていたクラスの残骸です。
また、Ruby 3.0で廃止されたセーフレベルに関するC API群も削除されました。
(mame)
■実装の改善
- Fixed several race conditions in Kernel#autoload. [Bug #18782]
autoload の thread-safety の状況が改善されました。
(ko1)
- Cache invalidation for expressions referencing constants is now more fine-grained.
RubyVM.stat(:global_constant_state)
was removed because it was closely tied to the previous caching scheme where setting any constant invalidates all caches in the system. New keys,:constant_cache_invalidations
and:constant_cache_misses
, were introduced to help with use cases for:global_constant_state
. [Feature #18589]
定数のキャッシュが、いままで大雑把に管理していた(たとえば、ある定数を定義したら、すべての定数キャッシュを全部クリア)のを、より細かく制御することになりました。
それに関連して、RubyVM.stat
の返す値が変わりました。
(ko1)
- Variable Width Allocation is now enabled by default. [Feature #18239].
Ruby 3.1 では off にしてリリースされていた Variable Width Allocation (VWA) がオンになりました。これは、オブジェクトのサイズを可変長にすることで、追加のメモリアロケーション(つまり malloc
)を減らして性能向上を目指すものです。
実際どれくらいの高速化になっているのかは知らないんだよなぁ。
(ko1)
- Added a new instance variable caching mechanism, called object shapes, which improves inline cache hits for most objects and allows us to generate very efficient JIT code. Objects whose instance variables are defined in a consistent order will see the most performance benefits. [Feature #18776]
オブジェクトシェイプという仕組みを使ってインスタンス変数アクセスの高速化が図られました。 とくに、JITにおいて、効くんじゃないかと思われるテクニックです。
詳細は、RubyKaigi 2022 の Jemma Issroff さんによる Implementing Object Shapes in CRuby - RubyKaigi 2022、それから RubyKaigi 2021 の Chris Seaton さん(先日早逝の報を聞いて悲しい)によるキーノートThe Future Shape of Ruby Objects by Chris Seaton - RubyKaigi Takeout 2021 をごらんください。
(ko1)
- Speed up marking instruction sequences by using a bitmap to find "markable" objects. This change results in faster major collections. [Feature #18875]
命令列(バイトコード)からマークするべきオブジェクト一覧を取り出すのに、ちょっと面倒なことをしていたのを、ビットマップを用いることで、ピタッとわかるようにしたという話です。このパッチにより、Major GCの性能が向上したそうです。
性能向上については、Shopify からの貢献が大きいです。リーダーの Ufuk の Twitter のスレッドが参考になりそうなので、(DeepLが)日本語に訳しておきました。
Ufuk's explanation of Ruby 3.2 achievements by Shopify
(ko1)
■JIT
YJIT
- YJIT is no longer experimental
- Has been tested on production workloads for over a year and proven to be quite stable.
- YJIT now supports both x86-64 and arm64/aarch64 CPUs on Linux, MacOS, BSD and other UNIX platforms.
- This release brings support for Mac M1/M2, AWS Graviton and Raspberry Pi 4.
- Building YJIT now requires Rust 1.58.0+. [Feature #18481]
- In order to ensure that CRuby is built with YJIT, please install
rustc
>= 1.58.0 before running./configure
- Please reach out to the YJIT team should you run into any issues.
- In order to ensure that CRuby is built with YJIT, please install
- Physical memory for JIT code is lazily allocated. Unlike Ruby 3.1,
the RSS of a Ruby process is minimized because virtual memory pages
allocated by
--yjit-exec-mem-size
will not be mapped to physical memory pages until actually utilized by JIT code. - Introduce Code GC that frees all code pages when the memory consumption
by JIT code reaches
--yjit-exec-mem-size
.RubyVM::YJIT.runtime_stats
returns Code GC metrics in addition to existinginline_code_size
andoutlined_code_size
keys:code_gc_count
,live_page_count
,freed_page_count
, andfreed_code_size
.
- Most of the statistics produced by
RubyVM::YJIT.runtime_stats
are now available in release builds.- Simply run ruby with
--yjit-stats
to compute and dump stats (incurs some run-time overhead).
- Simply run ruby with
- YJIT is now optimized to take advantage of object shapes. [Feature #18776]
- Take advantage of finer-grained constant invalidation to invalidate less code when defining new constants. [Feature #18589]
- The default
--yjit-exec-mem-size
is changed to 64 (MiB). - The default
--yjit-call-threshold
is changed to 30.
YJIT にも色々変更入ってますね。
- Rust で書き直し
- x86-64 に加え、ARM にも対応した
- コードGC に対応した
が大きいトピックでしょうか。細かいことは追えていないので説明できません。ゴメン。
(ko1)
MJIT
- The MJIT compiler is re-implemented in Ruby as
ruby_vm/mjit/compiler
. - MJIT compiler is executed under a forked Ruby process instead of
doing it in a native thread called MJIT worker. [Feature #18968]
- As a result, Microsoft Visual Studio (MSWIN) is no longer supported.
- MinGW is no longer supported. [Feature #18824]
- Rename
--mjit-min-calls
to--mjit-call-threshold
. - Change default
--mjit-max-cache
back from 10000 to 100.
MJITも書き変わっていて、いろいろRubyで書くという野心的な話をしています。 詳しくは国分さんの RubyKaigi 2022 での発表 Towards Ruby 4 JIT - RubyKaigi 2022 をご覧になると良いかと思います。
(ko1)
おわりに
Ruby 3.2の新機能や改善を紹介してきました。ここで紹介した以外でも、バグの修正や細かな改善が行われています。お手元の Ruby アプリケーションでご確認いただければと思います。
Ruby 3.2では、いろいろアグレッシブに改善が行われています。Shopify ではリリース前から大規模に利用して、10%の性能改善を得ているそうです。それだけ安定しているってことですね。ぜひ、お手元にセットアップして新しい Ruby を楽しんでください。
では、よいお年をお迎えください。
(ko1/mame)
*1:正確にはlazyにロードするように少しだけ工夫されています。