2022/07/22 今回の気になった bugs.ruby のチケット

今週は Time.new24時 を指定してタイムゾーンを渡した時に意図しない結果が返ってくるバグチケットがありました。

[Bug #18929] ruby master looks slower than 3.1 on a micro benchmark of short-lived objects

  • 次のように短命なオブジェクトを生成するときのベンチマークRuby 3.1 と比較して master が遅くなっているというバグ報告
$ time ruby -ve '10000000.times { Object.new }'
ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [x86_64-linux]

real    0m2.503s
user    0m2.484s
sys     0m0.016s

$ time ./local/bin/ruby -ve '10000000.times { Object.new }'
ruby 3.2.0dev (2022-07-20T00:40:59Z master e330dceb3f) [x86_64-linux]

real    0m3.074s
user    0m3.016s
sys     0m0.052s

[Bug #18927] Can't access class variable directly with class inheritance

  • 以下のように親クラスから子クラスのクラス変数にアクセスする事はできない
class Parent
  def self.class_var
    @@class_var
  end
end

class Child < Parent
  @@class_var = "class_var"
end

# error: uninitialized class variable @@class_var in Parent (NameError)
p Child.class_var
  • しかし、以下のように class_variable_get を使用すると子クラスのクラス変数を取得する事ができる
class Parent
  def self.class_var
    # class_variable_get だとアクセスする事ができる
    class_variable_get(:@@class_var)
  end
end

class Child < Parent
  @@class_var = "class_var"
end

p Child.class_var
# => "class_var"
  • このように @@class_varclass_variable_get(:@@class_var) で挙動が違うがこれは意図する挙動なのか?というチケットになる
  • クラス変数は自身とサブクラスでのみ参照できるのが期待する挙動になる
  • また以下のようにコメントされている
It's best to avoid using class variables completely in Ruby.
# 訳: Rubyでは、クラス変数を完全に使わない方がよいでしょう。

[Feature #18930] Officially deprecate class variables

  • 上の [Bug #18927] からの派生でクラス変数は紛らわしいので公式で非推奨にする提案
    • 具体的にはドキュメントに明記したりとか Warning[:deprecation] = true の場合にのみ警告を出したりとか
  • だいたいの場合はインスタンス変数で代替できるのでクラス変数がなくてもそんなに困らないとは思うんですが、既存のコードに対する影響はかなり大きそうですねえ

[Bug #18837] Not possible to evaluate expression with numbered parameters in it

  • 以下のように binding 経由で元のブロックの引数を参照する事ができる
def dumper(bnd)
  puts bnd.local_variable_get 'i'
  puts bnd.eval 'i * 10'
end

[1,2].each { |i| dumper(binding) }
  • しかし _1 の場合は Binding#eval で参照することができない
def dumper(bnd)
  puts bnd.local_variable_get('_1')
  puts bnd.eval '_1 * 10'
end

[1,2].each do
  # この行がないと local_variable_get でも _1 を参照する事ができない
  some = _1
  dumper(binding)
end

[Bug #18922] Time at 24:00:00 UTC is not normalized

  • Time.newタイムゾーンを指定して 24時 を指定した場合に意図しない挙動になっているバグ報告
# TimeZone を渡さない場合は問題ない
# 次の日の0時になっている
pp Time.new(2000, 1, 1, 24, 0, 0)
# => 2000-01-02 00:00:00 +0900

# TimeZone を渡した場合に意図しない時刻が返ってくる
pp Time.new(2000, 1, 1, 24, 0, 0, "UTC")
# => 2000-01-01 23:00:00 UTC

# 内部では 2000/01/01 24:00:00 というような情報を持っている
pp Time.new(2000, 1, 1, 24, 0, 0, "UTC").to_a
# => [0, 0, 24, 1, 1, 2000, 7, 0, true, "UTC"]
  • 開発版ではこの問題は既に修正済み
pp RUBY_VERSION   # => "3.2.0"
pp Time.new(2000, 1, 1, 24, 0, 0, "UTC")
# => 2000-01-02 00:00:00 UTC

pp Time.new(2000, 1, 1, 24, 0, 0, "UTC").to_a
# => [0, 0, 0, 2, 1, 2000, 1, 1, true, "UTC"]

[Bug #18038] Invalid interpolation in heredocs

  • 以下のようにヒアドキュメントで式展開をした時に意図しない結果になっているというバグ報告
pp RUBY_VERSION
# => "3.0.4"

var = 1

# 式展開されない
v1 = <<~CMD
  something
  #{"/#{var}"}
CMD

# 式展開される
v2 = <<~CMD
  something
  #{other = "/#{var}"}
CMD

# 式展開されない
v3 = <<~CMD
  something
  #{("/#{var}")}
CMD

p v1   # => "something\n/\n"
p v2   # => "something\n/1\n"
p v3   # => "something\n/\n"

p v1 == v2   # => false
p v2 == v3   # => false
  • この問題は Ruby 2.7 から発生していて Ruby 3.1 で修正済み

2022/07/14 今回の気になった bugs.ruby のチケット

今週は :"@=".inspect の結果が Ruby で評価できない文字列を返すというバグ報告がありました。

[Bug #18905] :"@=".inspect is non-evaluatable

  • :"@=".inspect の結果が Ruby で評価できない文字列を返すというバグ報告
# :@= を返すが
p :"@=".inspect # => ":@="

# :@= という Symbol をリテラルで定義できない
# :@= のような Symbol を定義する場合は "" でくくる必要がある
# error: `@' without identifiers is not allowed as an instance variable name
:@=
  • これは以下の条件の時に再現する
    • @ @@ $ [ から始まり
    • prefix の後に識別子が続かず
    • = で終了する
# こういう Symbol は "" がつく
p :"42hoge"   # => :"42hoge"
p :"+aa"      # => :"+aa"
p :"@+"       # => :"@+"

# 以下は "" がつかない
p :"@="       # => :@=
p :"@hoge="   # => :@hoge=
p :"[][]="    # => :[][]=
p :"$$$$="    # => :$$$$=
  • 前提として #inspect は評価できる Ruby のコードを返すわけではない
# 必ずしもこういう式が成り立つわけではない
eval(obj.inspect)
  • これは ruby-2.2.0-preview2 のタイミングで挙動が変わったらしい
$ docker run --rm -e "ALL_RUBY_SINCE=ruby-2.0" rubylang/all-ruby ./all-ruby -e "puts '4_to_5='.to_sym.inspect"
ruby-2.0.0-p0       :"4_to_5="
...
ruby-2.2.0-preview1 :"4_to_5="
ruby-2.2.0-preview2 :4_to_5=
...
ruby-3.2.0-preview1 :4_to_5=

[Feature #18913] Add object name to the NoMethodError error message: undefined method _method_' forclass' in `object_name'

  • エラーメッセージにオブジェクト名を追加したいという提案
  • 例えば以下のようなエラーメッセージに対して
bar = nil
bar.i_wish_i_saw_the_name_bar
# (irb):00:in `<main>': undefined method `i_wish_i_saw_the_name_bar' for nil:NilClass (NoMethodError)
  • 以下のように bar というレシーバの名前を出したいという提案
bar = nil
bar.i_wish_i_saw_the_name_bar
#(irb):00:in `<main>': undefined method `i_wish_i_saw_the_name_bar' for nil:NilClass in `bar' (NoMethodError)
#                                                                                        ^^^ It should mention the object name
  • 例えば以下のようなケースで役に立つ
    • どの foo 呼び出しでエラーになっているのかがわからない
a = OpenStruct.new
b = nil
c = nil
who_failed = a.foo & b.foo & c.foo
# (irb):00:in `<main>': undefined method `foo' for nil:NilClass (NoMethodError)
$ ruby test.rb
test.rb:5:in `<main>': undefined method `foo' for nil:NilClass (NoMethodError)

who_failed = a.foo & b.foo & c.foo
                      ^^^^

$ ruby -v
ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [x86_64-linux]

2022/07/07 今回の気になった bugs.ruby のチケット

今週は Ractor.make_shareable(obj) 時に obj.make_shareable を呼び出せるようにする提案がありました。

[Bug #18896] Shellwords.escape(nil) returns "empty" string

require 'shellwords'

pattern = 'Jan 15'
puts "grep #{Shellwords.escape(pattern)} file"
# => grep Jan\ 15 file
  • このモジュールに対して Shellwords.escape(nil) を渡すと空の文字列が返ってくるがこれが意図していないのではないかというバグ報告
    • 例外を出すのが意図しているとのこと
require "shellwords"

puts Shellwords.escape(nil)
# => "''"
  • このライブラリしらなかった

[Feature #18894] Object#make_shareable

  • Marshal.dump(obj)obj.marshal_dump を呼び出すように Ractor.make_shareable(obj) を呼び出した時に obj.make_shareable が呼び出されるようにする提案
  • obj ごとに固有の make_shareable を実装する必要がある時は便利そう?
    • 例えば『 Resolv::Hosts の場合は遅延して初期化を行っているので Ractor では利用できない』とコメントされていますね
      • なので make_shareable ないで事前にロードする仕組みが必要
  • ちなみに Ractor.make_shareable は任意のオブジェクトを Ractor 間でやりとりできるようにするための仕組み
obj = [1, 2, "hoge"]

# 共有可能オブジェクトではない
p Ractor.shareable? obj   # => false

# Ractor.make_shareable(obj) を呼び出すと obj を Ractor で利用できるようにする
# これは結果的に全てのオブジェクトが freeze されることになる
Ractor.make_shareable(obj)
p Ractor.shareable? obj   # => true
p obj.frozen?             # => true
p obj[2].frozen?          # => true

2022/06/30 今回の気になった bugs.ruby のチケット

今週は Module#autoload で意図しない定数が定義された時に対応するチケットがありました。

[Feature #18815] instance_{eval,exec} vs Proc#>>

  • 次のように Proc#>> した結果は instance_eval / instance_exec に適さない
measure = proc { p "self=#{self}"; size }
multiply = proc { '*' * _1 }

# measure のブロック内のレシーバは 'test' になる
'test'.instance_eval(&measure)
# "self=test"
#  => 4

# しかし Proc#>> した場合はその限りではない
'test'.instance_eval(&measure >> multiply)
# "self=main"
# NameError (undefined local variable or method `size' for main:Object)
FLATTEN = -> { [*_1] }
IDENTITY = -> { _1 }
DATE = -> { Date.parse(_1) }

# パラメータのリストをどのように処理するのかの定義を持つ定数を定義したい
TRANSFORMATIONS = {
  param1: FLATTEN,
  param2: FLATTEN,
  param3: IDENTITY,
  # この時に他の処理を呼び出しつつ、コンテキストに依存するような処理を定義したい
  # param4: FLATTEN >> -> { allowed?(:something) ? _1 : DEFAULT }
  # 現状だと以下のように定義する必要がある
  param4: -> { allowed?(:something) ? FLATTEN.(_1) : DEFAULT }
}

[Bug #18813] Let Module#autoload be strict about the autoloaded constant

# /tmp/x.rb
module M
  class X
  end
end
# sample.rb
module M
  # M::X を参照した時に自動的に require '/tmp/x' される
  autoload :X, '/tmp/x'
end

# このタイミングで require '/tmp/x' される
M::X
  • また、まだ読み込まれていない場合に Module.constantsModule.const_defined? を使用すると次のような結果が返ってくる
module M
  autoload :X, '/tmp/x'
end

# まだ M::X は定義されていないが定義されているかのように振る舞う
p M.constants(false)          # => [:X]
p M.const_defined?(:X, false) # => true
  • この時に /tmp/xM::X が定義されていない場合に意図しない動作になる
# /tmp/x.rb

# M の配下ではなくてトップレベルに X が定義される
class X
end
# sample.rb
module M
  autoload :X, '/tmp/x'
end

p M.constants(false)          # => [:X]
p M.const_defined?(:X, false) # => true

module M
  # このタイミングで require '/tmp/x' される
  # しかし、実際には M::X は定義されない
  X
end

# それにより結果が変わる
p M.constants(false)          # => []
p M.const_defined?(:X, false) # => false
  • このように autoload するときの構造と実際に require された結果が違う場合はエラーにしたいというのがこのチケットの趣旨になる
  • 今回の対応では Ruby 3.2 では警告を出す対応になっている
# /tmp/x.rb

# M の配下ではなくてトップレベルに X が定義される
class X
end
# sample.rb
# -W を付けた時のみ警告が出る
pp $VERBOSE   # => true

module M
  autoload :X, '/tmp/x'
end

module M
  X
  # => /tmp/voLY8oW/52:10: warning: Expected /tmp/x to define M::X but it didn't
end

2022/06/23 今回の気になった bugs.ruby のチケット

今週はトップレベルで include したあとにクラス定義すると予期しないクラスを再オープンするというバグ報告がありました。

[Feature #18832] Do not have class/module keywords consider ancestors of Object

  • 次のように include M した後に C < String を定義すると意図しないエラーになるバグ報告
    • M::C とトップレベルの C は別のクラスとして扱われることを期待する
module M
  class C
  end
end

include M

# C は定義されていない
p Object.const_defined?(:C, false)
# => false

# M::C を再オープンしようとしていてエラーになっている
# error: superclass mismatch for class C (TypeError)
class C < String # (1)
end
  • これは include M すると C を参照する時に M::C を参照するようになっているからぽい
module M
  class C
  end
end

include M

# これは明示的に親スコープがない C を参照するので false
p Object.const_defined?(:C, false)

# これは C を探索しようとして M::C を見つけるのでそれを参照する
p C
# => M::C

# この C も M::C を参照する
class C < String # (1)
end
  • ちなみに以下のようにトップレベル以外だとエラーにはならない
module M
  class C
  end
end

module N
  include M

  # これは M::C を参照する
  pp C   # = > M::C

  # これは N::C を参照する
  class C < String
    pp self   # => N::C
  end

  # これは N::C を参照する
  pp C   # = > N::C
end
require "active_record"
require "rexml"

include REXML

class Comment < ActiveRecord::Base # superclass mismatch for class Comment (TypeError)
end

2022/06/17 今回の気になった bugs.ruby のチケット

[Bug #18826] Symbol#to_proc inconsistent, sometimes calls private methods

  • #tap& 渡しでメソッドを呼び出す場合に privateprotected メソッドを呼び出す事ができるバグ報告
class Test
  protected

  def referenced_columns
    puts "hello"
  end
end

# protected メソッドを呼び出す事ができる
Test.new.tap(&:referenced_columns)
# => hello

# Symbol#to_proc 経由でも呼び出せる
:referenced_columns.to_proc.call Test.new
# => hello

[Bug #18827] __ENCODING__ is not set to the source encoding when saving script lines

p __ENCODING__
# => #<Encoding:UTF-8>
# encoding: euc-jp
p __ENCODING__
# => #<Encoding:EUC-JP>
# -Ke を渡すと #<Encoding:EUC-JP> になる
$ ruby -Ke -e 'p __ENCODING__'
#<Encoding:EUC-JP>

$ cat script_lines.rb
SCRIPT_LINES__ = {}

# -Ke を渡すと #<Encoding:EUC-JP> になるがそうでない
$ ruby -r./script_lines.rb -Ke -e 'p __ENCODING__'
#<Encoding:UTF-8>

2022/06/11 今回の気になった bugs.ruby のチケット

今週は標準ライブラリをパターンマッチに対応させる提案がありました。

[Feature #18821] Expose Pattern Matching interfaces in core classes

  • Ruby のいくつかの標準ライブラリをパターンマッチに対応させるチケット
  • 提案されているのは以下の通り

Set

  • Array のようなパターンマッチを行う
# Hypothetical implementation
class Set
  alias_method :deconstruct, :to_a
end

Set[1, 2, 3] in [1, 2, *]
# => true

Matrix

  • Array のようなパターンマッチを行う
class Matrix
  alias_method :deconstruct, :to_a
end
# => :deconstruct

Matrix[[25, 93], [-1, 66]] in [[20..30, _], [..0, _]]
# => true

CSV

  • ヘッダーカラムをキーにして Hash のようなパターンマッチを行う
require "csv"
require "net/http"
require "json"

# Hypothetical implementation
class CSV::Row
  def deconstruct_keys(keys)
    # Symbol/String is contentious, yes, I will address in a moment
    self.to_h.transform_keys(&:to_sym)
  end
end

# Creating some sample data for example:
json_data = URI("https://jsonplaceholder.typicode.com/todos")
  .then { Net::HTTP.get(_1) }
  .then { JSON.parse(_1, symbolize_names: true) }

headers = json_data.first.keys
rows = json_data.map(&:values)

# Yes yes, hacky
csv_data = CSV.generate do |csv|
  csv << headers
  rows.each { csv << _1 }
end.then { CSV.parse(_1, headers: true) }

# But can provide very interesting results:
csv_data.select { _1 in userId: "1", completed: "true" }.size
# => 11

RegexpMatchData

  • 名前付きキャプチャに対して Hash のようなパターンマッチを行う
class MatchData
  alias_method :deconstruct, :to_a

  def deconstruct_keys(keys)
    named_captures.transform_keys(&:to_sym).slice(*keys)
  end
end

IP_REGEX = /
  (?<first_octet>\d{1,3})\.
  (?<second_octet>\d{1,3})\.
  (?<third_octet>\d{1,3})\.
  (?<fourth_octet>\d{1,3})
/x

'192.168.1.1'.match(IP_REGEX) in {
  first_octet: '198',
  fourth_octet: '1'
}
# => true

[Feature #18183] make SecureRandom.choose public

  • SecureRandom.choose という private メソッドを public メソッドにするチケット
require "securerandom"

# 第一引数の配列の中からランダムで10文字を返す
pp SecureRandom.send(:choose, ['a', 'b', 'c'], 10)
# => "cabbbaaaab"

pp SecureRandom.send(:choose, [*'A'..'Z', *'0'..'9'], 10)
# => "7JZMZUK4GD"
def alphanumeric(n=nil, alphabet: ALPHANUMERIC)
  n = 16 if n.nil?
  choose(alphabet, n)
end

[Feature #11689] Add methods allow us to get visibility from Method and UnboundMethod object.

  • Ruby 3.1 で {Method,UnboundMethod}#{public?,private?,protected?} が新しく追加された
class X
  private def private_method
  end

  public def public_method
  end
end

# 該当する可視性であれば true を返す
pp X.instance_method(:private_method).private?   # => true
pp X.instance_method(:private_method).public?    # => false
pp X.instance_method(:public_method).private?    # => false
pp X.instance_method(:public_method).public?     # => true
  • 最近この実装に対して matz がコメントしており Ruby 3.2 では Revert される流れになっている
    • https://bugs.ruby-lang.org/issues/11689#note-24
    • このチケットでは可視性はメソッドの属性という前提だったが、実際には各クラスで可視性ごとのメソッドのリストが必要とのこと
      • ##18435 を調べていて気づいたとのこと
      • そうしないと #18729#18751 のような問題が発生する
  • 参照しているメソッドは同じでもクラスによって可視性が変わる、ってことなんですかねー

[Bug #18790] cannot load such file -- digest (LoadError)

  • 特定の環境で CRuby をビルドすると require': cannot load such file -- digest (LoadError) になってビルドに失敗するので対処するチケット
  • 地味に困っているので何かしら本体で対応されるとうれしいなあ