3.2 rc1 以降の変更

Ruby 3.2 アドベントカレンダーの最終日記事です。

qiita.com


3.2 rc1 以降の変更

今年も 12/25 に無事 Ruby 3.2.0 がリリースされた 🎉

ということで、3.2 rc1 以降の変更を、3.2.0-rc1 の NEWS.md3.2.0 リリースの NEWS.md の差分からピックアップ。 3.2 rc1 でも動きが変わってたのに NEWS.md に記載されてなかったものも含む。

Data#with 追加

Feature #19000: Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object] - Ruby master - Ruby Issue Tracking System

3.2 で新しく導入された Data クラスに、リリース3日前に with メソッドが追加された。

Data オブジェクトは immutable で値の変更はできないんだけど、with で一部の値を変更した新しい Data オブジェクトを返すことができるようになった。

Hoge = Data.define(:x, :y)
hoge = Hoge.new(x: 123, y: 456)
fuga = hoge.with(y: 789)
hoge  #=> #<data Hoge x=123, y=456>
fuga  #=> #<data Hoge x=123, y=789>

便利。

IO.new (IO.for_fd) に path オプション追加 / IO#path 追加

Feature #19036: Provide a way to set path for File instances created with for_fd - Ruby master - Ruby Issue Tracking System

IO.for_fd でファイルディスクリプタから IO オブジェクトを作ることができるけど、そのときに path オプションでファイル名を指定することができるようになった。

今まではファイル名が不明なオブジェクトで path を呼ぶと例外が発生していた:

f = File.open("hoge.txt")
f2 = File.for_fd(f.fileno)
f2.path  #=> File is unnamed (TMPFILE?) (IOError)

Ruby 3.2 では例外が発生せずに nil が返る:

f = File.open("hoge.txt")
f2 = File.for_fd(f.fileno)
f2.path  #=> nil

for_fd に path を指定するとそれが返る:

f = File.open("hoge.txt")
f2 = File.for_fd(f.fileno, path: "fuga.data")
f2.path  #=> "fuga.data"

この変更は File じゃなくて IO に対して行われたので、File だけじゃなくて IO 系のオブジェクト全部で path が使えるようになってる。

$stdin.path   #=> "<STDIN>"
$stdout.path  #=> "<STDIN>"
$stderr.path  #=> "<STDERR>"
r, w = IO.pipe
r.path        #=> nil
w.path        #=> nil
TCPSocket.new("localhost", 25).path  #=> nil

Regexp.linear_time? 追加

Feature #19194: Add Regexp.linear_time? - Ruby master - Ruby Issue Tracking System

Regexp.linear_time? で正規表現のマッチング処理が線形かどうかを確認できるようになった。

Regexp.linear_time?(/a/)       #=> true
Regexp.linear_time?(/(a)*\1/)  #=> false

String#dedup 追加

文字列オブジェクトは単項演算子の - で immutable な文字列を作ることができる。

s = 'abc'
x = -s
s.frozen?  #=> false
x.frozen?  #=> true
x.upcase!  #=> can't modify frozen String: "abc" (FrozenError)

これと同じことをする dedup というメソッドが追加された。

s = 'abc'
x = s.dedup
s.frozen?  #=> false
x.frozen?  #=> true
x.upcase!  #=> can't modify frozen String: "abc" (FrozenError)

Thread::Queue#pop, Thread::SizedQueue#pop, #push に timeout オプション追加

Feature #18774: Add Queue#pop(timeout:) - Ruby master - Ruby Issue Tracking System

Feature #18944: Add SizedQueue#push(timeout:) - Ruby master - Ruby Issue Tracking System

Queue#pop に timeout オプションを追加するとタイムアウトして nil を返すようになった。

q = Queue.new
q.push 123
Thread.new{ sleep }
q.pop  #=> 123
q.pop(timeout: 1)  #=> nil

ちなみに上のコードを Ruby 3.1 で実行すると queue empty (ThreadError) という例外が発生する。 Queue#pop に真の引数を指定するとキューが空だと ThreadError になるんだけど、3.1 までの pop はキーワード引数を受け付けないので、Hash オブジェクトという引数として渡される。Hash オブジェクトは真偽値的には真なので、ThreadError になる。

SizedQueue#pop も同じ。SizedQueue#push もブロックされるので同様に timeout オプションが追加された。

Time.new の引数に文字列を指定可能

Feature #18033: Time.new to parse a string - Ruby master - Ruby Issue Tracking System

Time.new に日時風文字列引数を渡すとパースしてくれるようになった。

Time.new('2022-12-25 01:23:45')   #=> 2022-12-25 01:23:45 +0900
Time.new('2022-12-25T01:23:45Z')  #=> 2022-12-25 01:23:45 UTC

便利。年月日時分秒がちゃんと区切られてる文字列の場合は Time.parse 使わなくてもよくなった。

20221225012345 みたいな文字列はダメ。

Time.new('20221225012345')    #=> 20221225012345-01-01 00:00:00 +0900

require 'time'
Time.parse('20221225012345')  #=> 2022-12-25 01:23:45 +0900

String#to_c で _ の連続の扱いが変わった

Bug #19087: String#to_c supports multiple "_" - Ruby master - Ruby Issue Tracking System

文字列を数値に変換する処理は、数値文字列中の _ は無視するんだけど、_ の連続は不正文字としてそこで評価が終わるようになってる。けど、Ruby 3.1 では String#to_c だけは __ も無視するようになっていた。

'1__234'.to_i  #=> 1
'1__234'.to_f  #=> 1.0
'1__234'.to_r  #=> (1/1)
'1__234'.to_c  #=> (1234+0i)

Ruby 3.2 では、これを他に合わせて __ 以降は処理しないようになった。

'1__234'.to_i  #=> 1
'1__234'.to_f  #=> 1.0
'1__234'.to_r  #=> (1/1)
'1__234'.to_c  #=> (1+0i)

ENV.clone がエラーになるようになった

Bug #17767: Cloned ENV inconsistently returns ENV or self - Ruby master - Ruby Issue Tracking System

Ruby 3.1 では ENV.dup はエラーになるのに ENV.clone はエラーにならなかったのが、3.2 では ENV.dup と同様にエラーになるようになった。

ENV.dup    #=> Cannot dup ENV, use ENV.to_h to get a copy of ENV as a hash (TypeError)
ENV.clone  #=> Cannot clone ENV, use ENV.to_h to get a copy of ENV as a hash (TypeError)

エラー終了時に例外メッセージをエスケープしないようになった

Feature #18367: Stop the interpreter from escaping error messages - Ruby master - Ruby Issue Tracking System

Ruby 3.1:

% ruby -e 'raise "hoge\0\1\a\b\e\f\n\r\s\t\u0002\v\x03fuga"'
-e:1:in `<main>': hoge\0\x01\a\b\e\f (RuntimeError)
\r  \x02\v\x03fuga

Ruby 3.2:

% ruby -e 'raise "hoge\0\1\a\b\e\f\n\r\s\t\u0002\v\x03fuga"'
-e:1:in `<main>': hoge
                     RuntimeError)
    
        fuga

今までも \n(改行), \s(空白), \t(タブ) はエスケープされずにそのまま表示されていたぽい。

これにより、例外メッセージにエスケープコードを入れて、

% ruby -e 'raise "\e[31m赤いメッセージ\e[0m"'
-e:1:in `<main>': 赤いメッセージ (RuntimeError)   ←「赤いメッセージ」部分は端末上で赤く表示される

みたいなことができるようになった。

今まではエスケープされてたのでこんな感じになってた。

% ruby -e 'raise "\e[31m赤いメッセージ\e[0m"'
-e:1:in `<main>': \e[31m赤いメッセージ\e[0m (RuntimeError)

クラス/モジュール定義時の名前空間の変更

Feature #18832: Do not have class/module keywords consider ancestors of Object - Ruby master - Ruby Issue Tracking System

モジュールを include した状態で、そのモジュール配下に存在するクラスと同名のクラスを class で指定したときに、3.1 まではモジュール配下の既存クラスの変更になっていたが、3.2 では新たなクラスが作られるようになった。

Ruby 3.1:

module Hoge; class Fuga; end; end
include Hoge
p Fuga       #=> Hoge::Fuga
class Fuga
  p self     #=> Hoge::Fuga
end
p Fuga       #=> Hoge::Fuga

Ruby 3.2:

module Hoge; class Fuga; end; end
include Hoge
p Fuga       #=> Hoge::Fuga
class Fuga
  p self     #=> Fuga
end
p Fuga       #=> Fuga

ちなみにトップレベルでなければ 3.1 も 3.2 も動きは変わらない。というか 3.1 のトップレベルだけ動きがおかしかったのが直されたって感じぽい。

module Parent
  module Hoge; class Fuga; end; end
  include Hoge
  p Fuga      #=> Parent::Hoge::Fuga
  class Fuga
    p self    #=> Parent::Fuga
  end
  p Fuga      #=> Parent::Fuga
end

これで Ruby 3.2 アドベントカレンダーは終了。最終日に大盛りだった。

Ruby 3.2 の変更のすべてを調べたわけではないけど、まあだいたいはわかった気になれた。

個人的に面白いと思ったのは IO#timeout とか Regexp.new に文字列でオプション指定可能 とか Integer#ceildiv とか Timeout.timeout がスレッドを浪費しない とかかなー。

ではよいお年を。