Hashの値の省略記法

Posted by Shugo Maeda on December 20, 2021 · 2 mins read

NaClの前田です。
Ruby Advent Calendar 2021の20日目の記事です。昨日は@getty104さんでした。

今回はRuby 3.1にするっと入ってしまったHashの値の省略記法を紹介します。

導入の経緯

最初の提案のきっかけは、何かでES6のenhanced object literalsを知って、2015年にFeature #11105 ES6-like hash literalsというチケットを作成したことでした。

この提案は、

{x, y}

{x: x, y: y}

とみなされるというもので、ES6と同じ記法です。

ただ、HashというよりSetに見えるという理由で却下されました。私自身、すごくほしいというよりは「実装できたから提案してみるか」という感じだったので、あまりがんばって説得しなかった気がします。

次の提案は2018年のFeature #14579 Hash value omissionです。Hashのように見えないのがいやだと言われたので

{x:, y:}

という記法に変更しました。

これならちゃんとHashに見えるので意気揚々と提案したのですが、「前の文法よりいいけど、ES6の文法と違うRuby独自の文法の導入になっちゃうし、どっちも直感的じゃないから好きじゃない」という理由でこれも却下されました。
ES6と違う文法を導入するのはいやで、ES6の文法もいやということで、ああこれは詰みですねということでもう提案を諦めることにしました。

ところが、その後も同じような提案が何度もあったようで、RubyKaigi Takeout 2021のまつもとさんのキーノートで「もうRubyの文法はあまり変えない」みたいな話がされていた直後の公開開発者会議でなぜか承認されて、するっとRuby 3.1に入ってしまいました。

武者さんたちがまつもとさんを一所懸命説得してくれたようなのですが、どうせ入らないだろうと思って正直あまりちゃんと聞いてなかったので、何で入ったのか私もよくわかっていません。

仕様

前述のとおり、

{x:, y:}

のようにHashの値を省略すると、値がキーと同名の定数、ローカル変数、またはメソッド呼び出しの値となります。

つまり基本的には

{x: x, y: y}

の省略記法なのですが、厳密にはキー名が予約語と同じ場合だけ挙動が異なります。
例えば、 {self: self} の場合の値は疑似変数selfではなく、selfという名前のローカル変数またはメソッド呼び出しの値になります。

これを応用すると、

def do_something(start:, end:)
  x = {end:}[:end]
end

のようにbinding.local_variable_get(:end)をより短かく書くことができます。

なお、

{"x":,"y":,}

のようにクォートを使う記法や、

{:x =>, :y =>}

のように => を使う記法では値の省略はできません。
この制約により、インスタンス変数やクラス変数、グローバル変数などの参照もできません。

クォートを使う記法では {"#{x}":} のように変数が埋め込まれた場合にxの値が "exit" だったら死ぬなどの問題が予想されます({exit:}と自分で書いちゃった時に死ぬのはそういうものです)し、 {"@x":}とか書けてうれしいかというと別にうれしくないと思うので、これでよいと思っています。

実装

実装はたったこれだけです。

diff --git a/parse.y b/parse.y
index 89c65992c4..85f9da5e74 100644
--- a/parse.y
+++ b/parse.y
@@ -4252,6 +4252,15 @@ assoc		: arg_value tASSOC arg_value
 		    /*% %*/
 		    /*% ripper: assoc_new!($1, $2) %*/
 		    }
+		| tLABEL
+		    {
+		    /*%%%*/
+                        NODE *val = gettable(p, $1, &@$);
+			if (!val) val = NEW_BEGIN(0, &@$);
+			$$ = list_append(p, NEW_LIST(NEW_LIT(ID2SYM($1), &@1), &@$), val);
+		    /*% %*/
+		    /*% ripper: assoc_new!($1, id_is_var(p, get_id($1)) ? var_ref!($1) : vcall!($1)) %*/
+		    }
 		| tSTRING_BEG string_contents tLABEL_END arg_value
 		    {
 		    /*%%%*/

tLABELx: のようなHashのキーの部分で、値(arg_value)が省略された場合は gettable(p, $1, &@$) でキーと同名の定数、ローカル変数、またはメソッド呼び出しの値を使うようにしています。

上記だとRipperでも値の部分を変数参照やメソッド呼び出しにしていますが、Ripper的には省略された場合が区別できた方がよいだろうということで、その後 assoc_new!($1, Qnil) に修正しています。

互換性

Ruby 3.1で導入した記法はRuby 3.0以前では文法エラーになるので、互換性の問題はありません。

ただ、Ruby 3.2以降については若干雲行きが怪しくなってきました。

foo key:

のように書けないというバグ報告が@koicさんからあって、遠藤さんが"It is by design"と回答されていました。これを許すと

foo key:
  bar

みたいなコードが書けなくなってしまうからです。提案者として当時そこまで考えていたかは怪しいですが、素直に実装するとそうなっちゃうよな、と思っていました。

ただ今改めてチケットを見たら、まつもとさんがRuby 3.1のリリース以降で許す方向で検討したいと言ってreopenしていて、これが許されると上記のような書き方ができなくなってしまうので、困る人は早めに反対しておいた方がいいと思います。

まとめ

Ruby 3.1にするっと入ってしまったHashの値の省略記法を紹介しました。
来年はRuby 3.2にするっと入ってしまったProc#usingを紹介したいと思います。