Feature #8895
closedDestructuring Assignment for Hash
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?
Updated by whitequark (whitequark *) over 11 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 11 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 11 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 11 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 11 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 11 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 11 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 11 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 11 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 11 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) over 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 10 years ago
- Description updated (diff)
Updated by ko1 (Koichi Sasada) over 10 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 10 years ago
- Related to Bug #10028: nested rest keyword argument added
Updated by seanlinsley (Sean Linsley) over 10 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 10 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) over 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 8 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.
Updated by jeremyevans0 (Jeremy Evans) about 3 years ago
- Is duplicate of Feature #6414: Destructuring Assignment added
Updated by jeremyevans0 (Jeremy Evans) about 3 years ago
- Status changed from Open to Closed