プロと読み解く Ruby 3.2 NEWS

技術部の笹田(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 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.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: 1g() にわたりません。渡されたキーワード引数が優先される場合はこれで問題ないのですが、絶対に 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 before baz. 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:

    1. foo1
    2. foo2
    3. baz1
    4. baz2

    [Bug #15928]

定数は、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パターンがパターンマッチで利用可能になりました。正確に言うと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 through foo(*args) must now be marked with ruby2_keywords (if not already the case). In other words, all methods wishing to delegate keyword arguments through *args must now be marked with ruby2_keywords, with no exception. This will make it easier to transition to other ways of delegation once a library can require Ruby 3+. Previously, the ruby2_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-missing ruby2_keywords is to run the test suite, for where it fails find the last method which must receive keyword arguments, use puts nil, caller, nil there, and check each method/block on the call chain which must delegate keywords is correctly marked as ruby2_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)

fooruby2_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 promote File#path to IO#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.defineStruct.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_16Encoding::UTF_32 の特別扱いをやめた

  • The dummy Encoding::UTF_16 and Encoding::UTF_32 encodings no longer try to dynamically guess the endian based on a byte order mark. Use Encoding::UTF_16BE/UTF_16LE and Encoding::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が導入された

切り上げの整数除算をするメソッドが追加されました。

# 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をバイト列として扱うメソッドがいくつか増えた

正規表現にマッチした位置を、「何文字めか」ではなく「何バイトめか」で返すメソッドが増えました。

"あいうえお" =~ /い/

# 「い」は3バイトめから6バイトめにある
$~.byteoffset(0) #=> [3, 6]
  • String#byteindex and String#byterindex have been added. [Feature #13110]

こちらはString#indexString#rindexと似ていますが、やはり「何バイトめか」を返すメソッドです。

"あいうえお".byteindex("い") #=> 3

おそらくTextbringerというRuby製エディタで必要だった機能と思われます。

(mame)

これも、文字列をバイト単位で文字列を編集するためのメソッドです。

s = 'hello'
p s.bytesplice(2, 2, 'LL') # s の 2 文字目から 2 文字分を 'LL' に置き換え
#=> "LL"
p s
#=> "heLLo"

これ、なんで self じゃなくて "LL" 返すんだろ。 (聞いてみたら、String#[]= にあわせたんだそうです。でも便利なのかなぁ?)

指定した箇所がコードポイントの途中だったりするとエラーがでるそうです。

(ko1)

Module.used_refinements が導入された

現在実行中の箇所で、どの 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 が導入された

そのモジュールが含む 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 がどのクラスを 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 が導入されました。

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#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 は、生成方法が 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

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 さんがクックパッドにインターンに来て実装してくれた大成果になります。詳しくは作者本人が記事を書いてくれましたので、ご参照ください、

techlife.cookpad.com

いくつかのコードを用いた実験によると、おおよそ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 than true, 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 for parse, parse_file and of. [Feature #19013]
    • Add keep_tokens option for parse, parse_file and of. Add #tokens and #all_tokens for RubyVM::AbstractSyntaxTree::Node [Feature #19070]

Rubyのコードをパースするためのメソッドに新たなモードが増えました。

  • syntax errorのあるコードでもなんとなくパースするモード(error_tolerant: trueで有効になる)
  • 各ノードに対応するトークン列を保持するモード(keep_tokens: trueで有効になる)

どちらもRubyのIDEサポートなどを実装することを念頭においた拡張になっています。詳しくは作者のyui-knkさんの記事をご参照ください。

yui-knk.hatenablog.com

(mame)

Setが組み込み(?)になった

  • Set is now available as a built-in class without the need for require "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 to String#-@. [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 without keyword_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: truePoint.new(x: 1, y: 2)で初期化できる、Point.new(1, 2)はエラー
  • keyword_init: nilPoint.new(x: 1, y: 2)でもPoint.new(1, 2)でも初期化できる
  • keyword_init: falsePoint.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 が導入された

自スレッドのバックトレース情報を取る 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::SizedQueue

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 and DateTime#deconstruct_keys same as [Feature #19071]

DateやDateTimeも同様らしいです。

(mame)

Time.newTime#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.newTime.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を引き起こした原因のファイル名を得られるようになりました。

begin
  load "broken.rb"
rescue SyntaxError
  p $!.path #=> "broken.rb"
end

後述するsyntax_suggestのために導入されました。一般的な用途は、あんまりないかも。

(mame)

CメソッドをフックしたときのTracePoint#bindingnilを返すようになった

  • TracePoint#binding now returns nil for c_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 and target and target_line keyword arguments are not passed. [Bug #16889]

TracePoint#enable メソッドを用いると、その TracePont が有効になる対象をいろいろ決められるのですが、TracePoint#enable(target_thread:) で有効にするスレッドを指定できます。

今までは、これがないと全スレッドに有効になっていたんですが、enable にブロックがわたされているときは、まぁたいていはそのブロックを実行している自スレッドだけに効かせたいでしょう、ということで、target_thread: Thread.current をデフォルトにするようにしました。nil にすると全スレッドに効きます。また、targettarget_line キーワードなどがついていても、全スレッドに有効になります。

Thread.new{loop{1.inspect}}
TracePoint.new(:c_call){|tp| 
  p tp # 別スレッドには効かない
}.enable{
  sleep 0.5
}
p :term

(ko1)

UnboundMethod がレシーバの情報を持たなくなった

  • UnboundMethod#== returns true if the actual method is same. For example,String.instance_method(:object_id) == Array.instance_method(:object_id)returns true. [Feature #18798]
  • UnboundMethod#inspect does not show the receiver of instance_method. For example String.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_infoneed_major_by を返すようになった

  • Expose need_major_gc via GC.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はGemfileやgemspecに書かれた依存性を満たすバージョンを決定する必要があるのですが、これを行うアルゴリズムを早くて賢いものに変えたそうです。

このアルゴリズムはPubGrubと呼ばれていて、解説記事を眺めたところ、“unit propagation”、“logical resolution”、“conflict-driven clause learning”など、SATソルバの実装で聞くテクニックを使っているみたいです。

(mame)

CGI.escapeURIComponentが導入された

  • CGI.escapeURIComponent and CGI.unescapeURIComponent are added. [Feature #18822]

CGI.escapeCGI.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 accepts eval: 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ライブラリが、requireloadしたファイルだけでなく、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 than CGI.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 to ERB::Util.html_escape, which has not been monkey-patched by Rails.
  • ERB::Util.url_encode is made faster using CGI.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 from erb command.

erbコマンドからセーフレベルを指定するオプションが消えました。セーフレベル自体はRuby 3.0で消されています。コマンドライン引数はdeprecatedとして残されていましたが、今回ついに消えたようです。

(mame)

FileUtils.ln_srが導入された

  • Add FileUtils.ln_sr method and relative: option to FileUtils.ln_s. [Feature #18925]

ターゲットディレクトリからの相対パスでシンボリックリンクを生成するメソッドが増えました。

FileUtils.ln_s("src/file", "dest/symlink")とすると、dest/symlinkdest/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-httpなどで使われているバッファの実装が効率的になったようです。以前はString#slice!を使って文字列を切り出していたため、長いバッファに対して先頭から一行ずつ読むような場合に遅くなっていたとのこと。改善後は切り出さずに先頭からのオフセットを管理するようにしたらしい。

(mame)

Pathname#lutimeが導入された

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

Socket にいろんな定数が追加されました。詳しくないから詳細わかりません、ゴメン。

(ko1)

syntax_suggest gemが導入された

  • The feature of syntax_suggest formerly dead_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)

■サポートプラットフォーム

RubyがWebAssembly/WASIに対応しました。かんたんに言えば、Rubyがブラウザ上で動いて楽しいです。

しかしWebAssembly/WASIはブラウザにとどまらず、いろいろな活用が期待されているプラットフォームで、いわゆるエッジコンピューティングやプラグイン機構などでこれから使われていく、かもしれません。今回のRubyの対応は、そういう将来に備えた布石です。

RubyKaigi 2022の初日のキーノートで作者の @kateinoigakukun が発表していたので、記憶に残っている人も多いのではないかと思います。実装など詳しくはそちらの発表資料を御覧ください。

クックパッドはRubyKaigiのブース企画として、このWebAssembly/WASI対応を使ったコードパズルを作りました。興味あれば、次の2つの記事をご覧ください。

techlife.cookpad.com

techlife.cookpad.com

(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 raises TypeError as well as ENV.dup [Bug #17767]

ENV.cloneが例外を投げるようになりました。

(mame)

削除された定数

The following deprecated constants are removed.

消すぞ消すぞ、と言われていた古い定数が削除されました。

(ko1)

削除されたメソッド

The following deprecated methods are removed.

上記のメソッドが削除されました。ほとんどのメソッドは呼ぶと警告が出ていたものになります。

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#includewith 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にもとづいて検査するようになりました。これは脆弱性の可能性を防ぐためです。

バグ発見者の徳丸浩さんが記事を書いてくれましたので、あわせてご参照ください。

blog.tokumaru.org

(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 adding init_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 and rb_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 and rb_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)を減らして性能向上を目指すものです。 実際どれくらいの高速化になっているのかは知らないんだよなぁ。

shopify.engineering

(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.
  • 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 existing inline_code_size and outlined_code_size keys: code_gc_count, live_page_count, freed_page_count, and freed_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).
  • 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にロードするように少しだけ工夫されています。