Project

General

Profile

Actions

Feature #22108

open

Computed hash keys with (expr): syntax

Feature #22108: Computed hash keys with (expr): syntax

Added by yertto (_ yertto) 3 days ago. Updated about 12 hours ago.

Status:
Open
Assignee:
-
Target version:
-
[ruby-core:125722]

Description

Warning

This has potentially been obsoleted by Feature #22111

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:

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:

## Before -- mixed styles
n = 42; { key1: "symbol", "key-#{2}": "quoted symbol", RUBY_VERSION:, n => "bar" }.keys
# => [:key1, :"key-2", :RUBY_VERSION, 42]

## After -- uniform colon syntax
n = 42; { key1: "symbol", "key-#{2}": "quoted symbol", RUBY_VERSION:, n : "bar" }.keys
# => [:key1, :"key-2", :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 Ruby Feature Key type
{ name: value } v1.9 Symbol
{ "quoted label": value } v2.2 Feature #4935 Symbol (quoted label)
{ value_omission: } v3.1 Feature #14579 Value omission
{ (expr): value } ??? Feature #22108 Computed

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, 2018)
  • Parentheses signals a grouping or "evaluate this", whereas square brackets signals an Array/container

Improving on JavaScript

JavaScript's "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 PR on GitHub.

Historical context

A version of this was discussed on ruby-core in October 2007 (as part of "General hash keys for colon notation", murphy).
Unfortunately it was brought up during the v1.9 feature freeze, but it looks like Matz's invitation to discuss for v2.0 didn't end up going anywhere.
Since then, "quoted label": (Ruby 2.2, Feature #4935) and value omission (Ruby 3.1, Feature #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 (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.

Actions

Also available in: PDF Atom