Project

General

Profile

Feature #22108

Updated by yertto (_ yertto) 4 months ago

# Computed hash keys with `(expr):` syntax 

 Allow `{ (expr): value }` as a computed hash key. 
 Almost 20 years in the making, the missing puzzle piece for Hash's "new" colon notation: 

 ```ruby 
 h = { 
   name: "symbol shorthand",            # Ruby 1.9+ 
   "quoted label": "symbol label",      # Ruby 2.2+ (Feature #4935) 
   value_omission: ,                    # Ruby 3.1+ (Feature #14579) 
   (expr): "computed",                  # THIS PROPOSAL 
 } 
 ``` 

 ## Motivation 

 Ruby has several colon-based hash key syntaxes but the `=>` ("hash rocket") is still needed for non-symbolic keys. 
 Adding `(expr):` is a baby step toward allowing all-colon hashes: 

 ```ruby 
 ## Before -- mixed styles 
 n=42; { name: "Alice", role: "admin", "key-#{n}": "foo", RUBY_VERSION:, n => "bar" }.keys 
 # => [:name, :role, :"key-42", :RUBY_VERSION, 42] 

 ## After -- uniform colon syntax 
 n=42; { name: "Alice", role: "admin", "key-#{n}": "foo", RUBY_VERSION:, (n): "bar" }.keys 
 # => [:name, :role, :"key-42", :RUBY_VERSION, 42] 
 ``` 

 (And maybe a step closer to one day retiring our old friend "hash rocket" from Hashes entirely?) 


 ### Completing the colon family 

 | Example | Since Ruby | Key type | 
 |---------|-------|----------| 
 | `{ name: value }` | [v1.9](https://docs.ruby-lang.org/en/3.4/NEWS/NEWS-1_9_1.html) | Symbol | 
 | `{ "quoted label": value }` | [v2.2](https://docs.ruby-lang.org/en/3.4/NEWS/NEWS-2_2_0.html) | Symbol (quoted label) | 
 | `{ value_omission: }` | [v3.1](https://docs.ruby-lang.org/en/3.4/NEWS/NEWS-3_1_0_md.html) | Value ommission | 
 | `{ (expr): value }` | *???* | Runtime expression value | 

 ### Readability for computed-key-heavy code 

 Code that builds dynamic hashes currently forces a style break midway through a literal. 
 This is especially noticeable with interpolated keys, numeric keys, or variable-driven keys. 

 ### Reducing `=>` overloading 

 The `=>` token is now being used to serve more purposes than just the Hash literal (aka "Hash rocket") it was originally used for. 
 - rightward assignment 
   ``` 
   expr => var 
   ``` 
 - pattern capture 
   ``` 
   case {name: "Alice", role: "admin"} 
   in {name: String => name, role:}    # capture the name String value into `name` 
     p name    # => "Alice" 
   end 
   ``` 
 - rescue variables 
   ``` 
   rescue SomeExceptionClass => e 
   ``` 
   ``` 
   rescue => e 
   ``` 
 Reducing its use in Hashes simplifies the language, especially for newcomers. 

 ## Design 

 ### `(expr):` vs `[expr]:` 

 Parenthesized expressions were chosen over square bracket delimited: 

 - Idempotent wrapping: `((x))` = `(x)`, but `[[x]]` != `[x]` (nested array) 
 - Array keys: `{(["a"]): "b"}` reads naturally vs `{[["a"]]: "b"}` requiring extra wrapping 
 - `jq` precedent: the JSON query language uses identical `{("a"+"b"): 59}` syntax (since [jq 1.6](https://jqlang.org/manual/v1.6/#object-construction), 2018) 
 - Parentheses signals a grouping or "evaluate this", whereas square brackets signals an Array/container 

 ### Improving on JavaScript 

 JavaScript's "[computed property names](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Object_initializer#computed_property_names)" `{ [expr]: value }` were introduced in ECMAScript 2015 (ES6), 3 years before the jq version. 
 However in Ruby, `[]` already means Array literal and method call. 
 Rather than overloading `[]` further, `(expr):` (from jq) improves on the JavaScript design. 
 Parentheses naturally signal evaluation, `():` inside `{}` is unambiguous to the lexer, and it matches the well-established jq convention. 

 ### Edge cases handled 

 - `(expr) : value` (space before colon) -- *syntax error* (consistent with `name : value`) 
 - `{ (expr): }` (value omission) -- *syntax error* (runtime expression, no compile-time local to infer) 
 - Mixed styles: `{ (1): "one", two: "two", "three": "three" }` -- *valid* 
 - Nested parens: `{ ((1 + 2) * 3): "nested" }` -- *valid* 
 - Nested hashes: `{ ({(1): "one"}): "two" }` -- *valid* 

 ## Implementation 

 Three changes to `parse.y` (Lrama LALR(1) parser, zero new conflicts): 

 1. Lexer: emit `tLABEL_END` when `EXPR_ENDFN` state, no space before `:`, and inside `brace_nest > 0` 
 2. Grammar: new `assoc` alternative -- `tLPAREN compstmt ")" tLABEL_END arg_value` 
 3. Precedence: `%nonassoc ')'` to disambiguate 

 Reference implementation: [feature/computed-hash-keys](https://github.com/ruby/ruby/pull/17299) PR on GitHub. 

 ## Historical context 

 This was discussed on ruby-core in October 2007 (["General hash keys for colon notation"](https://public-inbox.org/ruby-core/E1ImQBE-00033s-IZ@x31/T/), murphy). 
 Withdrawn due to the 1.9 feature freeze. 
 Since then, `"quoted label":` (Ruby 2.2, [Feature #4935](https://bugs.ruby-lang.org/issues/4935)) and value omission (Ruby 3.1, [Feature #14579](https://bugs.ruby-lang.org/issues/14579)) have expanded the colon family, making the computed-key gap more conspicuous. 
 Meanwhile, jq introduced the identical `{("a"+"b"):59}` syntax in [jq 1.6](https://jqlang.org/manual/v1.6/#object-construction) jq 1.6 (2018), demonstrating real-world viability. 

 ## Open questions 

 - Is this syntax acceptable to the community? 
 - Is the jq precedent compelling enough to address the "no language does this" objection? 

 ## Future directions 

 All colon-based key syntaxes would now be available. 
 This opens up the possibility of eventually deprecating `=>` from Hash literals (while keeping it for rescue, pattern matching, and rightward assignment). 
 This proposal does not require that change - it is simply the enabling step, and any deprecation timeline could be a separate discussion.

Back