ruby のキーワード引数について

##はじめに
Rubyのキーワード引数は、ハッシュだと思って使っていましたが、どうやらそうじゃなさそうだったので、改めて調べてみました。
また、調べてみたところ、バージョンによって挙動が違いましたので、そのことについても記載します。
###検証バージョン
3.0.1


##キーワード引数とは
>キーワード引数とは、引数にキーワードを付けて、順序に関係なく対応する引数を決定する記述方法である。
キーワード引数を使うことで、何を意味するものか分かりやすく、引数に指定する順序を考慮する必要がなくなるメリットがある。

引用[Rubyのキーワード引数](https://tokitsubaki.com/ruby-keyword-arguments/531/)

###通常の引数を使用
次のプログラムでは、3つの仮引数を受け取るメソッドを定義して、呼出し時に3つの実引数を指定しています。

~~~ruby
def greet(first_name, last_name, age)
    puts "#{first_name} #{last_name}、#{age}歳です。" # -> 山田 太郎、20歳です。
end
greet("山田", "太郎", 20)
~~~
###キーワード引数を使ったプログラム

通常の方法と結果に違いはなく、3つの仮引数と実引数の受け渡しをしていますが、キーワード引数の場合は、「キーワード: 値」の形式で引数を指定します。また、仮引数と実引数の値がキーワードによって紐付けられています。

~~~ruby
def greet(first_name:, last_name:, age:)
    puts "#{first_name} #{last_name}、#{age}歳です。" # -> 山田 太郎、20歳です。
end
greet(first_name: "山田", last_name: "太郎", age: "20")
~~~

仮引数と実引数の値がキーワードによって紐付けられているため、定義時と呼び出し時で、引数の順番が異なっていても大丈夫です。

~~~ruby
def greet(first_name:, last_name:, age:)
    puts "#{first_name} #{last_name}、#{age}歳です。" # -> 山田 太郎、20歳です。
end
greet(last_name: "太郎", age: "20", first_name: "山田")
~~~

###ダブルsplatパラメータ
パラメータ名の前に ** を付けるとダブルsplatパラメータになります。
これで、可変長のキーワード引数リストを受け取る前提となります。引数としてキーワード引数以外を取らなくなり、不要なバグを防ぐことができます。

~~~ruby
def greet(**params)
    first_name=params[:first_name]
    last_name=params[:last_name]
    age=params[:age]
    puts "#{first_name} #{last_name}、#{age}歳です。" # -> 山田 太郎、20歳です。
end
greet(first_name: "山田", last_name: "太郎", age: "20")
~~~


##キーワード引数は、ハッシュではない。
ここからが本題です。上記のように、キーワードオブジェクトは、ハッシュのような見た目で、変数の取り出し方の部分もハッシュのそれと同じように見えます。実際呼び出し時の実引数を次のように書くこともできます。

~~~ruby
def greet(**params)
    first_name=params[:first_name]
    last_name=params[:last_name]
    age=params[:age]
    puts "#{first_name} #{last_name}、#{age}歳です。" # -> 山田 太郎、20歳です。
end
greet(:first_name=> "山田", :last_name=> "太郎", :age=> "20")
~~~

以上のことから、私はてっきりキーワード引数はハッシュオブジェクトだと思っていました。そして、次のようにコードを記述してしまいます。（実行環境は、Ruby バージョン3.0.1です。）

~~~ruby
def greet(**params)
    first_name=params[:first_name]
    last_name=params[:last_name]
    age=params[:age]
    puts "#{first_name} #{last_name}、#{age}歳です。"  # ->  wrong number of arguments (given 1, expected 0) (ArgumentError)
end

#ハッシュオブジェクトを返すメソッドを定義
def information
 {first_name: "山田", last_name: "太郎", age: "20"}
end
#greetメソッドの引数にハッシュオブジェクトを渡す。
greet(information)
~~~
このコードを実行すると、wrong number of arguments (given 1, expected 0) (ArgumentError)が発生します。この時、キーワード引数がハッシュオブジェクトでないことを悟りました。

[Rubyのキーワード引数はシンボルっぽく定義するけど、シンボルそのものではない、という話]
(https://qiita.com/jnchito/items/74e0930c54df90f9704c)でも紹介されていますが、キーワード引数のキーは、歴史的経緯（キーワード引数が導入される前は、ハッシュで表現していた。）からシンボルっぽく表現されたもので、シンボルではないみたいです。

###ruby3.0以前は、キーワード引数とハッシュに互換性があった
ただ、ruby2.0以降、ruby3.0より前のバージョンでは、ハッシュからキーワード引数への自動変換が行われていました。そのため、バージョン３.０１では、先ほどエラーが出た記述でも問題なく動いていたようです。しかしこのキーワード引数とオプション引数のどちらを渡してもよいという互換性は、実に多くのバグやエッジケースの温床となっていたようです。そこでバージョン3.0からは、自動変換が行われず、エラーとなる仕様に変更になったようです。
参考:[Ruby 2.7: ハッシュからキーワード引数への自動変換が非推奨に（翻訳）](https://techracho.bpsinc.jp/hachi8833/2019_10_31/82588)

##キーワード引数にハッシュを渡したいとき
>メソッド呼び出しにおいて最後の引数としてハッシュオブジェクトを渡し、 他にキーワード引数を渡さず、かつ、呼ばれたメソッドがキーワード引数を 受け取るとき、警告が表示されます。キーワード引数として扱いたい場合は、 明示的にdouble splat演算子（**）を足すことで警告を回避できます。 このように書けばRuby 3でも同じ意味で動きます。

引用:[NEWS for Ruby 2.7.0](https://docs.ruby-lang.org/ja/latest/doc/news=2f2_7_0.html)
次のように呼び出し時にdouble splat演算子（**）を引数に明示的に付け足せば、バージョン3.0以降でもエラーが発生しません。

~~~ruby
def greet(**params)
    first_name=params[:first_name]
    last_name=params[:last_name]
    age=params[:age]
    puts "#{first_name} #{last_name}、#{age}歳です。"  # -> 山田 太郎、20歳です。
end

#ハッシュオブジェクトを返すメソッドを定義
def information
 {first_name: "山田", last_name: "太郎", age: "20"}
end
#greetメソッドの引数にハッシュオブジェクトを渡す。実引数の先頭にダブルsplat修飾子を付け足す
greet(**information)
~~~

##メソッドの引数にハッシュとして渡す。
キーワード引数がハッシュでないとわかったので、おとなしく、仮引数はハッシュのみ受け取れるようにします。

~~~ruby
#ハッシュのみを引数にとるように設定
def greet(params={})
    first_name=params[:first_name]
    last_name=params[:last_name]
    age=params[:age]
    puts "#{first_name} #{last_name}、#{age}歳です。"  # -> 山田 太郎、20歳です。
end

#ハッシュオブジェクトを返すメソッドを定義
def information
 {first_name: "山田", last_name: "太郎", age: "20"}
end
#greetメソッドの引数にハッシュオブジェクトを渡す。
greet(information)
~~~

仮引数の部分は、次のように、受け取り形式の制約をなくすように記述しても大丈夫です。

~~~ruby
#引数の受け取り形式の制約をなくす
def greet(params)
    first_name=params[:first_name]
    last_name=params[:last_name]
    age=params[:age]
    puts "#{first_name} #{last_name}、#{age}歳です。"  # -> 山田 太郎、20歳です。
end

#ハッシュオブジェクトを返すメソッドを定義
def information
 {first_name: "山田", last_name: "太郎", age: "20"}
end
#greetメソッドの引数にハッシュオブジェクトを渡す。
greet(information)
~~~

##まとめ

- キーワード引数のキーは、歴史的経緯からシンボルのように見えるだけでシンボルではなく、キーワード引数自体もハッシュオブジェクトではない。
- ruby3.0より前のバージョンでは、キーワード引数とハッシュの自動補完が行われていた（２.７からは非推奨）
- バージョン3.0以降は、キーワード引数にハッシュを渡すとエラーが発生する。（呼び出し時の実引数にダブルsplat付け足せばエラーは発生しない。）



##参考
[Rubyのキーワード引数](https://tokitsubaki.com/ruby-keyword-arguments/531/)
[Rubyのキーワード引数はシンボルっぽく定義するけど、シンボルそのものではない、という話]
(https://qiita.com/jnchito/items/74e0930c54df90f9704c)
[NEWS for Ruby 2.7.0](https://docs.ruby-lang.org/ja/latest/doc/news=2f2_7_0.html)
[Ruby 2.7: ハッシュからキーワード引数への自動変換が非推奨に（翻訳）](https://techracho.bpsinc.jp/hachi8833/2019_10_31/82588)