class Hash class << self def ruby2_keywords_hash?(hash) !new(*[hash]).default.equal?(hash) end def ruby2_keywords_hash(hash) _ruby2_keywords_hash(**hash) end private def _ruby2_keywords_hash(*args) args.last end ruby2_keywords(:_ruby2_keywords_hash) if respond_to?(:ruby2_keywords, true) end end RUBY_VERSION # => "2.7.0" def passed_kw?(*args) Hash.ruby2_keywords_hash?(args.last) end ruby2_keywords(:passed_kw?) if respond_to?(:ruby2_keywords, true) passed_kw?({ a: 1 }) # => false passed_kw?(a: 1) # => true hash = { a: 1 } passed_kw?(hash) # => false passed_kw?(**hash) # => true kw = Hash.ruby2_keywords_hash(hash) passed_kw?(kw) # => true passed_kw?(**kw) # => true
説明書こうとしたけどだいぶめんどくさかったのでコードで感じ取ってほしいんですが、Ruby 2.7.0以降Ruby 3に向けてキーワード引数渡しの非互換をハンドリングするためにruby2_keywords
を使ってキーワード引数渡しされたhashオブジェクトにフラグを付けることができる、というかRuby 2.6以前とRuby 2.7以降の両方サポートしたいライブラリメンテナはこのフラグを付けて回らなければRuby 3ではArgumentErrorでお前はもう死んでいる状態になっています。
フラグが付けれるのはいいとしてRuby 2.7.0ではこのフラグが付いてるhashオブジェクトなのかそうじゃないhashオブジェクトなのか確かめる方法が公式には提供されてなくて、どこで呼び出されたときにフラグを付ける必要があって、ちゃんとフラグが付いたままお届け先のメソッドまでたどり着いてるのかのデバッグが死ぬほどめんどくさかった。
これはmameさんのハックを見て知った方法で、ようはフラグが付いてるhashオブジェクトかそうじゃないオブジェクトかで違う振る舞いをする処理を通らせてその結果を観測することでどっちだったかを確かめるという方法です。
このフラグが付いてるhashオブジェクトかそうじゃないオブジェクトかで違う振る舞いをする処理がRubyの世界からはほとんど存在しないので、普段Rubyでコードを書いてる常人が自力では気づけんやろって方法で、一部のメソッド(initialize
とかmethod_missing
とか)をRubyのコードから直接呼び出すんじゃなくてCのコードから間接的に呼び出されるときにフラグが付いてるhashオブジェクトをdup
するので、dup
されずにそのままのオブジェクト(object_id
)だったらフラグが付いてなかったってことでdup
されて別のオブジェクトが観測されたらフラグが付いてたオブジェクトだったという技を使っています。
いろいろ試行錯誤してみてRuby 2.7.0の時点ではnewを通さないとargs lastにkwargs flagが付いてるかどうかRubyの世界からは確かめようがないということが分かったけどこれはマジで答えを先に知ってないと気づきようがない無理ゲーすぎた
— Ryuta Kamizono (@kamipo) January 19, 2020
原理が分かったので既存のメソッドでこの用途に丁度いいメソッドを探した結果、Hash.new
にhashのデフォルト値としてフラグ付きかもしれないhashオブジェクトをsplat渡ししてHash#default
で取り出して観測するという方法が一番シンプルであろうというところに至り、この技を使ってRailsでもキーワード引数完全分離への対応を進めています。
このフラグ付きかどうか確かめる方法があるのかないのか常人では思い至らん問題にライブラリメンテナは直面すると思いますが、Ruby 2.7.1にはフラグ付きかどうか確かめる方法が公式にバックポートされる予定なので、これで警告が多い日も安心ですね。