Feature #8895

Destructuring Assignment for Hash

Added by Jack Chen almost 2 years ago. Updated 12 months ago.

[ruby-core:57131]
Status:Open
Priority:Normal
Assignee:-

Description

Given Ruby already supports destructuring assignment with Array (a, b = [1, 2]), I propose destructuring assignments for Hash.

Basic example

  params = {name: "John Smith", age: 42}
  {name: name, age: age} = params

  # name == "John Smith"
  # age == 42

This would replace a common pattern of assigning hash values to local variables to work with.

General syntax

  { <key-expr> => <variable_name>, … } = <object that responds to #[]>

  # Symbols
  { foo: bar } = { foo: "bar" }
  bar == "bar"

  # Potential shorthand
  { foo } = { foo: "bar" }
  foo == "bar"

Use cases

  # MatchData
  { username: username, age: age } = "user:jsmith age:42".match(/user:(?<username>\w+) age:(?<age>\d+)/)
  username == "jsmith"
  age == "42"

Edge cases

  # Variable being assigned to more than once should use the last one
  { foo: var, bar: var } = {foo: 1, bar: 2}
  var == 2

Thoughts?


Related issues

Related to Ruby trunk - Bug #10028: nested rest keyword argument Assigned 07/11/2014

History

#1 Updated by Peter Zotov almost 2 years ago

This is an awesome idea! However, the parser bit is really evil. I tried implementing it myself (quite a bit of time ago) and completely gave up. It's not above my comprehension, but the amount of work even for my Ruby parser port is huge and daunting. Doing it in C is a nightmare.

That being said, I'm willing to discuss and/or provide guidance to any interested parties.

#2 Updated by Marc-Andre Lafortune almost 2 years ago

I suggested something similar in .
Here is a summary from my similar suggestion made in :

{key: 'default', other_key:, **other_options} = {other_key: 42, foo: 'bar'}
key # => 'default'
other_key # => 42
other_options # => {foo: 'bar'}

You'll note that it doesn't give the possibility to map the key to a different variable. Indeed, I don't think that it would be useful and I would rather encourage rubyists to use meaningful option and variable names. It also makes very similar to the way we declare keyword arguments for methods, so no additional learning curve. Your proposal is quite different.

#3 Updated by Tsuyoshi Sawada almost 2 years ago

Given that destructive assignments with array prohibits the [ ] on the left side of the assignment, that is:

a, b = [1, 2]

instead of:

[a, b] = [1, 2]

it would be more consistent if your proposal were:

name: name, age: age = {name: "John Smith", age: 42}

rather than:

{name: name, age: age} = {name: "John Smith", age: 42}

#4 Updated by Jack Chen almost 2 years ago

marcandre (Marc-Andre Lafortune) wrote:

I suggested something similar in .
Here is a summary from my similar suggestion made in :

{key: 'default', other_key:, **other_options} = {other_key: 42, foo: 'bar'}
key # => 'default'
other_key # => 42
other_options # => {foo: 'bar'}

You'll note that it doesn't give the possibility to map the key to a different variable. Indeed, I don't think that it would be useful and I would rather encourage rubyists to use meaningful option and variable names. It also makes very similar to the way we declare keyword arguments for methods, so no additional learning curve. Your proposal is quite different.

I considered the case of default options, but I couldn't figure out a way to make it read well, and there are many cases where the keys in the hash are not symbols. No value variable after other_key: feels a bit off to me, too.

I'm all for a way to figure out how to get the use case of default options in somehow but I feel that needs more consideration where as this is useful by itself.

#5 Updated by Jack Chen almost 2 years ago

sawa (Tsuyoshi Sawada) wrote:

Given that destructive assignments with array prohibits the [ ] on the left side of the assignment, that is:

a, b = [1, 2]

instead of:

[a, b] = [1, 2]

it would be more consistent if your proposal were:

name: name, age: age = {name: "John Smith", age: 42}

rather than:

{name: name, age: age} = {name: "John Smith", age: 42}

I left the braces in because I felt it would be easier to parse, however if without braces is doable as well, that would work also. Will update the proposal.

#6 Updated by Marc-Andre Lafortune almost 2 years ago

chendo (Jack Chen) wrote:

No value variable after other_key: feels a bit off to me, too.

Not surprising it feels off today, but you better get used to it because it's coming in 2.1.0: https://bugs.ruby-lang.org/issues/7701

#7 Updated by Boris Stitnicky almost 2 years ago

@whitequark: Hi whitequark, you here? Let me raise my commendations to you for your parser gem!

As for the issue at hand, why not just say:

{ name: "JohnSmith", age: 42 }.!

and have the assignment done:

name = "JohnSmith"
age = 42

If you want the assignment done to different variables, why not take Rails's Hash#slice one step further:

{ n: "JohnSmith", a: 42, foo: "bar" }.slice( name: :n, age: :a ) # produces { name: "JohnSmith", age: 42 }

and then

{ n: "JohnSmith", a: 42, foo: "bar" }.slice( name: :n, age: :a ).! # foo: "bar" is ignored away by the #slice method

produces the desired assignment:
name = "JohnSmith"
age = 42

I hope that .! syntax proposal doesn't suck too hard! It might be a general way of making objects perform assignments to local variables. I'm concerned about feature creep, though.

#8 Updated by Charlie Somerville almost 2 years ago

boris_stitnicky: This sort of feature would be close to impossible to implement in CRuby. I can't speak for JRuby or Rubinius (although I would imagine they're in the same position here) but CRuby relies on being able to statically determine all local variables for a scope ahead of time.

#9 Updated by Boris Stitnicky almost 2 years ago

@charliesome: I thought myself chendo was stretching it, thanks for making me realize why I felt so. It's all about those famous

a = a #=> nil

cases :-) But... somehow... sorry for a quiche eater like me to say this... I thought that maybe
being able to statically determine local variables is itself a design smell that might need
to be removed from the language... Sorry again for raising issues.

#10 Updated by Alexey Muranov almost 2 years ago

How about this:

(x, y, *rest, :a => v1, :b => v2, **options) = 1, 2, 3, 4, :a => :foo, :b => :bar, :c => false, :d => true
x       # => 1
y       # => 2
rest    # => [3, 4]
v1      # => :foo
v2      # => :bar
options # => {:c=>false, :d=>true}

#11 Updated by Sean Linsley about 1 year ago

This is what I'm imagining:

a, *b, c:, d: 'd', **e = [1, {c: 2}]

a == 1
b == []
c == 2
d == 'd'
e == {} # holds any extras just like `b`

Where an error would be thrown if the hash didn't have the given key, and no default was provided.

#12 Updated by Koichi Sasada 12 months ago

  • Description updated (diff)

#13 Updated by Koichi Sasada 12 months ago

+1 for this proposal.

I feel it is fine for me:

  k1: 1, k2: 2 = h
  kr1:, kr2: = h

  #=> same as
  k1 = h.fetch(:k1, 1)
  k2 = h.fetch(:k2, 2)
  kr1 = h.fetch(:k1)
  kr2 = h.fetch(:k2)

  # mixed
  k1: 1, k2: 2, kr1:, kr2: = h

  # compile to
  k1 = h.fetch(:k1, 1)
  k2 = h.fetch(:k2, 2)
  kr1 = h.fetch(:k1)
  kr2 = h.fetch(:k2)

Problem is what happen when `h' is not a hash object (and doesn't have to_hash method).
Just ignore is one option (what ary assignment do, like "a, b = 1 #=> 1, nil").

a, *b, c:, d: 'd', **e = [1, {c: 2}]

It should be:

a, (c:, d: 'd', **e) = [1, {c:2}]

#14 Updated by Nobuyoshi Nakada 12 months ago

  • Related to Bug #10028: nested rest keyword argument added

#15 Updated by Sean Linsley 12 months ago

Koichi Sasada wrote:

Problem is what happen when `h' is not a hash object (and doesn't have to_hash method).
Just ignore is one option (what ary assignment do, like "a, b = 1 #=> 1, nil").

a, *b, c:, d: 'd', **e = [1, {c: 2}]

It should be:

a, (c:, d: 'd', **e) = [1, {c:2}]

I don't follow. Can't this assignment behave the same way that method argument destructuring does? This currently works:

def foo(a, *b, c:, d: 'd', **e)
  [a, b, c, d, e]
end

foo 1, c: 2
# => [1, [], 2, "d", {}]

#16 Updated by Marc-Andre Lafortune 12 months ago

Sean Linsley wrote:

I don't follow. Can't this assignment behave the same way that method argument destructuring does?

I agree. Destructuring should work as method & block passing works (apart from block passing)

Also available in: Atom PDF