Ruby 2.7 の変更点 - キーワード引数

Ruby 2.7 アドベントカレンダーの3日目の記事です。

qiita.com

キーワード引数

Ruby は 2.0 から次のような感じでキーワード引数を使用することができるようになりました。

def hoge(k: 123)
  p k
end

hoge()          #=> 123
hoge(k: 456)    #=> 456
hoge({k: 789})  #=> 789

2.7 では最後の形式で warning が出るようになりました。

a.rb:7: warning: The last argument is used as the keyword parameter
a.rb:1: warning: for `hoge' defined here

キーワード引数は、最後の引数が Hash の場合に特別な扱いをするという感じだったのですが、それだと省略可能な位置引数(デフォルト値を持つ引数)がある場合に、予期しない挙動になるからということみたいです。

def hoge(h={}, k: 123)
  p [h, k]
end

hoge()          #=> [{}, 123]
hoge(k: 456)    #=> [{}, 456]
hoge({k: 789})  #=> [{}, 789]
hoge({a: 789})  #=> unknown keyword: a (ArgumentError)

呼び出してる側は Hash オブジェクトを第一引数で渡してるだけなのに、キーワード引数として扱われてしまってるという…。

なお、シンボルじゃないキーが含まれてるとそれはキーワード引数としては扱われないんですが、

h = {"a"=>123, k: 789}
hoge(h)  #=> [{"a"=>123}, 789]

ひとつの Hash オブジェクトを渡したのに、勝手に位置引数とキーワード引数に分割されちゃうという罠。

ということで来たるべき 3.0 ではこの挙動を変更して、Hash をキーワード引数としては扱わないようにするらしく、その前に既存コードの修正を促すために warning を出すということのようです。 3.0 では、キーワード引数を持つメソッドを呼び出す際に、{ } を省略すると Hash ではなくキーワード引数として扱われるようになるみたいです。

これに伴って、「復活したものや入りそうで入らなかったもの」で書いたように、2.6 までと若干の挙動の違いもあります。

なお、Hash をキーワード引数ではなく位置引数に渡したい場合は、2.7 では次のようにすれば良さそうです。

hoge({k: 789}, **{})  #=> [{:k=>789}, 123]

2.6 ではダメでした。

hoge({k: 789}, **{})  #=> [{}, 789]

キーワード引数を持たないメソッドでは、従来どおり { } を省略してキーワード引数の形式で呼び出しても Hash として渡されます。

def hoge(h)
  p h
end

hoge({k: 123})  #=> {:k=>123}
hoge(k: 123)    #=> {:k=>123}

2.7 ではメソッドがキーワード引数を取らないことを明示することもできるようになりました。 この場合はキーワード引数の形式で呼び出すとエラーになります。

def hoge(h, **nil)
  p h
end

hoge({k: 123})  #=> {:k=>123}
hoge(k: 123)    #=> no keywords accepted (ArgumentError)

いろいろややこしいですが、2.7 は 3.0 前の最後のバージョン(たぶん)なので、移行期間ということでこのようになってるんでしょう。 互換性を重視する Ruby らしいですね。