2022-12-05 (Mon) [長年日記]
_ ...
を*, **, &
の構文糖にした話
Ruby Advent Calendar 2022の5日目の記事です。
Feature #19134で以下の文法エラーのケースを修正し、**
でキーワード引数だけ他のメソッドに渡すことができるように修正しました。
def foo(...)
bar(*) # OK
baz(&) # OK
quux(**) # 文法エラー
end
つまり、def foo(...)
はdef foo(*, **, &)
の構文糖になりました。
また、この修正により、以下のケースでbarにキーワード引数が渡っていなかったバグも修正されました。
def foo(*, **, &)
bar(...)
end
つまり、実引数の...
も*, **, &
の構文糖になりました。
普通は冗長な表現に対して後から構文糖を用意することが多い(例えば{x: x, y: y}
の構文糖として{x:, y:}
を追加する)ので、こういうケースは珍しいのではないかと思います。
きっかけ
Bug #19132の修正中に以下のコードが目にとまりました。
#ifdef RUBY3_KEYWORDS
#define idFWD_KWREST idPow /* Use simple "**", as tDSTAR is "**arg" */
#else
#define idFWD_KWREST 0
#endif
仮引数で...
を使用した場合は以下のように内部的な変数を用意していますが、Ruby 3ではキーワード引数も転送する必要があるので当然RUBY3_KEYWORDSというマクロはどこかで定義されているのだろうと思っていました。
static void
add_forwarding_args(struct parser_params *p)
{
arg_var(p, idFWD_REST);
#if idFWD_KWREST
arg_var(p, idFWD_KWREST);
#endif
arg_var(p, idFWD_BLOCK);
}
ところが念のため以下のようにidFWD_KWREST
が真の時にエラーにするようにしてコンパイルしてもエラーが発生しませんでした。
static void
add_forwarding_args(struct parser_params *p)
{
arg_var(p, idFWD_REST);
#if idFWD_KWREST
#error nokwrest
arg_var(p, idFWD_KWREST);
#endif
arg_var(p, idFWD_BLOCK);
}
ということは、
def foo(...)
bar(**)
end
はエラーになるのではないか、と思って試してみたところ、たしかにエラーになったので冒頭の問題に気付きました。
なぜ...
でキーワード引数が転送できていたのか
仮引数に...
が使用されていた場合、以下のようにruby2_keywordsというフラグが立つようになっていました。
static NODE*
new_args(struct parser_params *p, NODE *pre_args, NODE *opt_args, ID rest_arg,
NODE *post_args, NODE *tail, const YYLTYPE *loc)
{
...
args->ruby2_keywords = args->forwarding;
...
}
このフラグが立っていると、そのメソッドはModule#ruby2_keywordsが使用された場合と同様に、キーワード引数が*
で受け取られるようになります。
他のメソッドに*
を渡すとキーワード引数を含めて他のメソッドに転送できるので、**
が定義されていなくてもキーワード引数を転送できていたわけです。
つまり、修正前の
def foo(...)
bar(...)
end
は
ruby2_keywords def foo(*, &)
bar(*, &)
end
の構文糖になっていたのでした(*
はRubyレベルでは3.2で導入されましたが、それ以前のバージョンでも内部的には*
という変数名が使われていました)。
まとめ
Ruby 3.2で*
と**
が導入されましたが、当初は...
と併用されることを想定しておらず、すこしいびつな仕様になっていました。
...
は*, **, &
の構文糖であるというシンプルな仕様に修正しました。
ただ、この修正で...
が遅くなっているようなのでひょっとしたらrevertされるかもしれません。