Feature #20738
openRemoving a specific entry from a hash literal
Added by ursm (Keita Urashima) about 2 months ago. Updated 27 days ago.
Description
Sometimes I want to decide whether or not to add a particular entry to a hash depending on a condition. If the entire hash does not use nil, I can use Hash#compact.
{
foo: 1,
bar: bar? ? 2 : nil
}.compact
But if I want to remove only a specific entry while leaving the other nil, it is somewhat cumbersome. I have to either assign the hash once and change it destructively, or use Hash#reject.
h = {
foo: 1,
baz: nil
}
h[:bar] = 2 if bar?
{
foo: 1,
bar: bar? ? 2 : :drop,
baz: nil
}.reject {|_, v| v == :drop }
As a suggestion, how about a special value that indicates an invalid key for the hash? With this, the above example could be written like this:
{
foo: 1,
bar: bar? ? 2 : Hash::DROP,
baz: nil
} #=> {foo: 1, baz: nil}
Updated by nobu (Nobuyoshi Nakada) about 2 months ago
"A special value" doesn't feel like a good idea to me.
Updated by osyo (manga osyo) about 2 months ago
How about using **
?
def bar? = false
{
foo: 1,
**(bar? ? { bar: 2 } : {})
}
# => {:foo=>1}
Also, you can use **nil
in ruby 3.4-dev.
def bar? = false
{
foo: 1,
**({ bar: 2 } if bar?)
}
# => {:foo=>1}
see:
Updated by ursm (Keita Urashima) about 2 months ago
Yes, I sometimes do that as well. However, I am not happy that the shape of the resulting hash is unclear.
Updated by ursm (Keita Urashima) about 2 months ago
nobu (Nobuyoshi Nakada) wrote in #note-1:
"A special value" doesn't feel like a good idea to me.
Hmmm, does that mean we should extend the syntax? For example, something like this?
{
foo: 1,
?bar: nil
} #=> {foo: 1}
Updated by ursm (Keita Urashima) about 2 months ago
With the previous idea, I can't have both removing entries and returning nil depending on the condition.
# If user.child? is true and user.parent is nil, I want parent_name: nil, but the entry is removed.
{
?parent_name: user.child? ? user.parent&.name : nil
}
Updated by mame (Yusuke Endoh) about 2 months ago
History. Long ago in Ruby, such a special value actually existed. It was nil
.
# ruby 1.4.6
h = { 1 => 2 }
p h #=> {1=>2}
# This removes the key 1
h[1] = nil
p h #=> {}
However, there were more and more cases where we want to treat nil
as an ordinary value. Finally, nil
has lost this speciality.
Considering this history, the special value is not a good idea.
Updated by ursm (Keita Urashima) about 2 months ago
I believe that the following two points will prevent the same problems as in the past:
- Use a value that is never used (e.g., Hash::DROP) instead of nil.
- Special treatment of “special value” only if the hash is constructed with literals.
{
foo: Hash::DROP
} #=> {}
h = {}
h[:foo] = Hash::DROP
h #=> {foo: Hash::DROP}
Note that I am not concerned with the “special value” approach. If there is a better way, please let me know.
Updated by Eregon (Benoit Daloze) about 2 months ago · Edited
I don't think it's OK to magically drop entries from a literal based on some value, it's way too surprising.
Notably this would make the capacity of the literal potentially wrong, etc.
And of course Hash::DROP
could leak into some variable and then unintentionally drop an entry, that'd be horrible to debug.
{
foo: 1,
**({ bar: 2 } if bar?)
}
# OR
h[:bar] = 2 if bar?
Sounds good enough to me.
I don't think it's very frequent to need this to warrant a syntax change either.
Updated by ursm (Keita Urashima) about 2 months ago
Eregon (Benoit Daloze) wrote in #note-8:
I don't think it's very frequent to need this to warrant a syntax change either.
There are several Rails codes that can be improved with this feature. In my opinion, it is especially useful in multiple situations, such as generating JSON responses and constructing HTTP request headers. It would be more useful if it could be applied to keyword arguments as well.
- https://github.com/rails/rails/blob/6d1252cf3e65a7720aad5511ff719b44e49fd2a3/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb#L1121
- https://github.com/rails/rails/blob/6d1252cf3e65a7720aad5511ff719b44e49fd2a3/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb#L201
- https://github.com/rails/rails/blob/6d1252cf3e65a7720aad5511ff719b44e49fd2a3/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb#L1141
- https://github.com/rails/rails/blob/6d1252cf3e65a7720aad5511ff719b44e49fd2a3/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb#L19
- https://github.com/rails/rails/blob/6d1252cf3e65a7720aad5511ff719b44e49fd2a3/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb#L31
- https://github.com/rails/rails/blob/6d1252cf3e65a7720aad5511ff719b44e49fd2a3/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb#L16
Updated by jeremyevans0 (Jeremy Evans) about 2 months ago
ursm (Keita Urashima) wrote in #note-9:
There are several Rails codes that can be improved with this feature. In my opinion, it is especially useful in multiple situations, such as generating JSON responses and constructing HTTP request headers. It would be more useful if it could be applied to keyword arguments as well.
- https://github.com/rails/rails/blob/6d1252cf3e65a7720aad5511ff719b44e49fd2a3/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb#L1121
- https://github.com/rails/rails/blob/6d1252cf3e65a7720aad5511ff719b44e49fd2a3/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb#L201
- https://github.com/rails/rails/blob/6d1252cf3e65a7720aad5511ff719b44e49fd2a3/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb#L1141
- https://github.com/rails/rails/blob/6d1252cf3e65a7720aad5511ff719b44e49fd2a3/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb#L19
- https://github.com/rails/rails/blob/6d1252cf3e65a7720aad5511ff719b44e49fd2a3/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb#L31
- https://github.com/rails/rails/blob/6d1252cf3e65a7720aad5511ff719b44e49fd2a3/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb#L16
I reviewed every example listed and in all cases I think it would be made harder to understand using the proposed feature. Each example listed is straightforward and easy to understand currently.
Updated by byroot (Jean Boussier) about 2 months ago
There are several Rails codes that can be improved with this feature.
As one of the maintainer of the code you linked to, I agree with @jeremyevans0 (Jeremy Evans) that it's currently fine as it is. I wouldn't use such feature if it was added.
Updated by ursm (Keita Urashima) about 2 months ago
I would like to offer that as I used a simple grep pattern, I could only find simple examples. I wanted to show that this is not as rare as it seems.
Updated by ursm (Keita Urashima) about 2 months ago
It would be better to explain the motive. This is an appropriate code.
{
foo: 1
bar: 2
}
This is not a mistake, but it's a circuitous code.
h = {}
h[:foo] = 1
h[:bar] = 2
h
The gist of this proposal is that if a condition is included, it should be written in the same form as the “appropriate code”.
h = {}
h[:foo] = 1
h[:bar] = 2 if bar?
h
Updated by matz (Yukihiro Matsumoto) about 1 month ago
I don't want to add a special value (Hash::DROP) nor special syntax (?key:) here. Use h = {foo: 1}; h[:bar] = 2 if bar?
.
Matz.
Updated by ursm (Keita Urashima) 27 days ago
OK, I'm sorry to hear that, but I'm glad to hear your opinions. Thanks.