Project

General

Profile

Actions

Feature #8895

closed

Destructuring Assignment for Hash

Added by chendo (Jack Chen) over 10 years ago. Updated over 2 years ago.

Status:
Closed
Assignee:
-
Target version:
-
[ruby-core:57131]

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 2 (0 open2 closed)

Related to Ruby master - Bug #10028: nested rest keyword argumentRejectedmatz (Yukihiro Matsumoto)Actions
Is duplicate of Ruby master - Feature #6414: Destructuring Assignment ClosedActions

Updated by whitequark (whitequark *) over 10 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.

Updated by marcandre (Marc-Andre Lafortune) over 10 years ago

I suggested something similar in [ruby-core:41772].
Here is a summary from my similar suggestion made in [ruby-core:41772]:

{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.

Updated by sawa (Tsuyoshi Sawada) over 10 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}

Updated by chendo (Jack Chen) over 10 years ago

marcandre (Marc-Andre Lafortune) wrote:

I suggested something similar in [ruby-core:41772].
Here is a summary from my similar suggestion made in [ruby-core:41772]:

{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.

Updated by chendo (Jack Chen) over 10 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.

Updated by marcandre (Marc-Andre Lafortune) over 10 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

Updated by Anonymous over 10 years ago

@whitequark (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.

Updated by Anonymous over 10 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.

Updated by Anonymous over 10 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.

Updated by alexeymuranov (Alexey Muranov) over 10 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}

Updated by seanlinsley (Sean Linsley) almost 10 years 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.

Updated by ko1 (Koichi Sasada) over 9 years ago

  • Description updated (diff)

Updated by ko1 (Koichi Sasada) over 9 years 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}]

Updated by nobu (Nobuyoshi Nakada) over 9 years ago

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

Updated by seanlinsley (Sean Linsley) over 9 years 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", {}]

Updated by marcandre (Marc-Andre Lafortune) over 9 years 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)

Updated by matz (Yukihiro Matsumoto) almost 8 years ago

The proposed syntax is much harder to implement than it looks. It conflicts with Hash literals. As a result, humans can be confused as well.

Probably this kind of problems should be addressed by pattern matching, which we are considering to add to Ruby in the future.

Matz.

Updated by bughit (bug hit) over 7 years ago

the closest you can get to hash destructuring is via block params:

{a: 1, b: 2}.tap do |a:, b:|
end

but unfortunately this has its own issues (#11048), it's too strict about missing/extra keys, which doesn't make sense since blocks are intended to be looser with parameter binding.

Actions #19

Updated by jeremyevans0 (Jeremy Evans) over 2 years ago

Actions #20

Updated by jeremyevans0 (Jeremy Evans) over 2 years ago

  • Status changed from Open to Closed
Actions

Also available in: Atom PDF

Like0
Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0