追記
Ruby 2.6 からはキーワード引数にシンボル以外のキーを渡すことができなくなるようです, 自然な流れな気がしています.
mrubyのキーワード引数を実装してたらRubyのキーワード引数に謎の挙動を「発見」した。
— Yukihiro Matsumoto (@yukihiro_matz) 2018年7月26日
Non-Symbol key in keyword arguments hash causes an exception.
https://github.com/ruby/ruby/blob/1692ccaf9f49c001b18de8b7296ede3f68190ab8/NEWS より
HashWithIndifferentAccess
Rails (activesupport) で提供されている Hash
のサブクラス
特徴として、 Symbol
と String
を区別せず同一のキーとして取り扱う。
h1 = Hash.new
h1[:foo] = 1
h1[:bar] = 2
h1['bar'] = 3
puts h1[:foo] # => 1
puts h1['foo'] # => nil
puts h1[:bar] # => 2
puts h1['bar'] # => 3
h2 = HashWithIndifferentAccess.new
h2[:foo] = 1
h2[:bar] = 2
h2['bar'] = 3
puts h2[:foo] # => 1
puts h2['foo'] # => 1
puts h2[:bar] # => 3
puts h2['bar'] # => 3
HashWithIndifferentAccess
の内部的な話として、キーに対して Symbol
でアクセスが来た際は String
に丸めている。
h3 = HashWithIndifferentAccess.new
h3[:foo] = 1
h3['bar'] = 2
h3[:baz] = 3
# Symbol は String に丸められている
h3.keys # => ["foo", "bar", "baz"]
キーワード引数
Ruby 2.0.0 からの機能。メソッドを定義する際、以下のように記述できる。
def test foo: 1, bar: 2, baz: 3
[foo, bar, baz].join(',')
end
呼び出す際は、以下のような感じ。
以下の、「ハッシュを直接渡すこともできる」の部分が 今回のキモ
# 省略した箇所は、定義中のデフォルトが与えられる
test # => '1,2,3'
test foo: 4 # => '4,2,3'
test foo: 4, bar: 5, baz: 6 # => '4,5,6'
# ハッシュを直接渡すこともできる
arg = {foo: 7, bar: 8, baz: 9}
test arg # => '7,8,9'
キーワード引数に HashWithIndifferentAccess
を与えてみる
arg = HashWithIndifferentAccess.new({foo: 4, bar: 5, baz: 6})
test arg # => ArgumentError: wrong number of arguments (1 for 0)
- キーワード引数が受け入れ可能なハッシュは、全てのキーが
Symbol
であることが前提
参考 ⇒class.c
の rb_extract_keywords (と separate_symbol あたり) - しかしながら、
HashWithIndifferentAccess
はキーをString
で保持しているのでキーワード引数の要件を満たさない
ちなみに確認
arg = {foo: 4, bar: 5, "baz" => 6}
test arg # => ArgumentError: wrong number of arguments (1 for 0)
Hash
であっても同様で、キーが全て Symbol
でなければキーワード引数として受け付けてくれない。
そもそもエラーメッセージの ArgumentError
って何言ってんのよ?
追記: 以下の挙動は Ruby 2.5 までのものです.
ArgumentError: wrong number of arguments (1 for 0)
rb_extract_keywordsの説明 より
original_hashで参照されるHashオブジェクトから,Symbolである
キーとその値を新しいHashに取り出します.original_hashの指す
先には,元のHashがSymbol以外のキーを含んでいた場合はそれらが
コピーされた別の新しいHash,そうでなければ0が保存されます.
つまり、キーが全て Symbol でないハッシュを引数に与えると...
arg = {foo: 4, bar: 5, "baz" => 6}
test arg
以下のような呼び出しと扱われる。
test({"baz" => 6}, foo: 4, bar: 5)
(キーワード引数以外の) 引数が 1つ与えられる形になるので
先のエラー (0個引数を与えるべき箇所に1つの引数が渡っている) になる。
# 第一引数を用意しておくと...
def test1 x, foo: 1, bar: 2, baz: 3
[foo, bar, baz, x].join(',')
end
# 第一引数に arg 全てがわたり、キーワード引数に {} が渡される。
arg = HashWithIndifferentAccess.new({foo: 4, bar: 5, baz: 6})
test1 arg # => "1,2,3,{\"foo\"=>4, \"bar\"=>5, \"baz\"=>6}"