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

今週はブロックの引数に関するバグチケなどチケットがありました。

[Bug #18635] Enumerable#inject without block/symbol will return values or raise LocalJumpError

  • ブロック引数のない Enumerable#inject を呼び出すと『値が返ってくるか例外が発生するか』で一貫性がないというバグ報告
pp [].inject       # => nil
pp [1].inject      # => 1
pp [1, 2].inject   # error: `each': no block given (LocalJumpError)
  • 前者2つはイテレーションされないので『要素の最初の値』をそのまま返している感じですかね?
  • チケットを立てた人は『常に LocalJumpError (もしくは ArgumentError )を発生させたほうがいい』とコメントしている

[Bug #18632] Struct.new wrongly treats a positional Hash as keyword arguments

  • 以下のように Hash を位置引数として渡しているがキーワード引数としてエラーになっているというバグ報告
# これはキーワード引数なので意図するエラーになる
# error: `new': unknown keyword: :name (ArgumentError)
Struct.new(:a, name: "b")
# これは Hash を位置引数で渡している
# しかし、キーワード引数を渡した時と同じエラーになる
# error: `new': unknown keyword: :name (ArgumentError)
Struct.new(:a, { name: "b" })
  • また位置引数に Hash を渡すとエラーになるが、以下のように空の hash を渡すとエラーにならない
# OK
p Struct.new(:a, {}).members
# => [:a]
  • ややこしい…

[Bug #18633] proc { |a, **kw| a } autosplats and treats empty kwargs specially

  • ブロックの引数は引数が2つ以上の場合は位置引数を分割引数として値を受け取る
# これは a = [1, 2] で受け取る
p proc { |a| [a] }.call [1, 2]
# => [[1, 2]]

# これは a = 1, b = 2 と配列を分割して受け取る
p proc { |a, b| [a, b] }.call [1, 2]
# => [1, 2]
  • これが『位置引数が1つでキーワード引数がある場合も同じ挙動になっている』というバグ報告
# キーワード引数がある場合も a = 1 として配列を分割して受け取る
p proc { |a, **kwd| [a, kwd] }.call [1, 2]
# => [1, {}]
  • ちなみに実引数にキーワード引数がある場合は『分割引数として受け取らない』
p proc { |a, **kwd| [a, kwd] }.call [1, 2], c: 3
# => [[1, 2], {:c=>3}]

# 空のキーワード引数でも同様
p proc { |a, **kwd| [a, kwd] }.call [1, 2], **{}
# => [[1, 2], {}]

[Bug #18629] block args array splatting assigns to higher scope _ var

  • 以下のように仮引数が _ でない場合はスコープの外の変数は書き換わらないが _ の場合は外のスコープの変数が書き換わってしまうというバグ報告
# これは書き換わらない
v = 1; [[2]].each{ |(v)| }; p v   # => 1

# これは書き換わってしまう
_ = 1; [[2]].each{ |(_)| }; p _   # => 2
  • 『これは _ 付き変数( _ 自体も含む)が特殊な変数になっているから』とコメントされている
# 同じ名前の仮引数があるとエラーになる
def a(b, b) end   # SyntaxError

# 同じ名前でも _ が付いている場合はエラーにならない
def a(_b, _b) end # no error

[Bug #18624] const_source_location returns [false, 0] when autoload is defined for the constant

  • 以下のように autoload を設定している定数に対して const_source_location を呼び出した時に [false, 0] が返ってくるのは期待する挙動ではないというバグ報告
    • ['/path/to/test2.rb', 2] が返ってくるのが期待する挙動
# test.rb
path = File.join(__dir__, 'test2')
Object.autoload 'Test2', path
require path

p Object.const_source_location 'Test2'
# test2.rb
class Test2
end
$ ruby -v test.rb
ruby 3.2.0dev (2022-03-11T08:38:13Z master 2e4516be26) [x86_64-linux]
[false, 0]
  • これは Zeitwerk を使った場合に同様の問題があるらしい
# test.rb
require "zeitwerk"
loader = Zeitwerk::Loader.for_gem
loader.setup

require File.join(__dir__, 'test2')

p Zeitwerk::VERSION
p Object.const_source_location 'Test2'
# test2.rb
class Test2
end

[Bug #18620] Not possible to partially curry lambda or proc's call method

  • Method オブジェクトを #curry 化すると次のように #[] を別々に呼び出して評価できる
class Foo
  def foo(a, b)
    a + b
  end
end
Foo.new.method(:foo).curry[1][2] # => 3
  • これは proclambda でも同じ挙動になる
lambda { |a, b| a + b }.curry[1][2] # => 3
proc { |a, b| a + b }.curry[1][2] # => 3
  • しかし Proc#callMethod オブジェクト化した場合は期待する挙動にならないというバグ報告
# error: `block in <top (required)>': wrong number of arguments (given 1, expected 2) (ArgumentError)
lambda { |a, b| a + b }.method(:call).curry[1][2]
# error: `+': nil can't be coerced into Integer (TypeError)
proc { |a, b| a + b }.method(:call).curry[1][2]
  • これがバグかどうかは次の開発者会議で議論されるとの事
  • ちなみに #[] に複数の引数を渡すと正しく動作する
lambda { |a, b| a + b }.method(:call).curry[1, 2] # => 3
proc { |a, b| a + b }.method(:call).curry[1, 2] # => 3

[Bug #18561] Make singleton def operation and define_singleton_method explicitly use public visibility

  • 次のようにクラススコープで private を呼び出した後に『クラスメソッド』を定義しても private にはならない
class X
  private

  X.define_singleton_method(:hoge) do
    "hoge"
  end
end

# クラスメソッドは private ではないので呼び出せる
pp X.hoge
# => "hoge"
  • これは private になるのはあくまでも『クラスのインスタンスメソッド』になるから
  • このチケットでは『この挙動が仕様であることを明示的にドキュメントに書こう』という旨になる
  • ただし、以下のように『特定のケースでクラスメソッドが private になる』というバグ報告もされている
class X
  class << X
    private

    # 特異クラスのスコープ内で define_singleton_method を定義すると private になる
    X.define_singleton_method(:hoge) do
      "hoge"
    end

    # こっちは private にならない
    def X.foo
      "foo"
    end
  end
end

# OK
pp X.foo    # => "foo"

# NG
# error: `<main>': private method `hoge' called for X:Class (NoMethodError)
pp X.hoge

[Bug #18622] const_get still looks in Object, while lexical constant lookup no longer does

  • 次のように :: で定数参照した場合と const_get で定数参照した場合で差異があるというバグ報告
module ConstantSpecsTwo
  Foo = :cs_two_foo
end

module ConstantSpecs
end

# これは定数参照できる
p ConstantSpecs.const_get("ConstantSpecsTwo::Foo") # => :cs_two_foo

# これはエラーになる
# error: const_get.rb:9:in `<main>': uninitialized constant ConstantSpecs::ConstantSpecsTwo (NameError)
p ConstantSpecs::ConstantSpecsTwo::Foo
  • これは Ruby 2.5 で定数参照が変わった時に関連しているぽい
class C
end

# トップレベルで定義された定数は暗黙的に Object の配下で定義される
class C2
end

# なので Ruby 2.4 以前では C:: で参照する事ができていたが Ruby 2.5 からはできなくなった
p C::C2
# Ruby 2.4 => C2
# Ruby 2.5 => error: `<main>': uninitialized constant C::C2 (NameError)
  • 議論は盛り上がってるぽいけど全然追えてない…
  • 個人的にはトップレベル定数の扱いを変えたい…
    • トップレベルで定義された定数は暗黙的に private 定数になる、みたいなルールがすっきりすると思ってる

[Feature #18617] Allow multiples keys in Hash#[] acting like Hash#dig

  • Hash#dig のように Hash#[] に複数の引数を渡せるようにする提案
hash[:a][:b][:c][:d][:e][:f][:u]
hash[:a, :b, :c, :d, :e, :lov, :u]
  • と記述できるようにする提案
  • Hash#[] で対応するのであれば Array#[] とかでも対応する必要がありそう