From: davidm@... Date: 2015-01-08T04:42:05+00:00 Subject: [ruby-core:67423] [ruby-trunk - Bug #10713] Assigning default value for a Hash as an empty Array creating unpredictable results Issue #10713 has been updated by David MacMahon. Arvinder Singh wrote: > ~~~ > irb(main):001:0> letters = Hash.new([]) > => {} > irb(main):002:0> letters[:a] << 1 > => [1] > ~~~ The `letters[:a]` part of the second line returns the "default value" of the Hash because the `:a` key does not exist in the Hash. The `<< 1` part pushes a `1` onto the end of the default value, but it does not assign the default value (and certainly not a modified copy of the default value) to the `:a` key of the Hash. To get the behavior you expect/desire you can use `letters[:a] <<= 1`, which is equivalent to `letters[:a] = letters[:a] << 1`, but be careful when using Hashes with default objects: ~~~ irb(main):001:0> letters = Hash.new([]) # Default object == single instance => {} irb(main):002:0> letters[:a] <<= 1 => [1] irb(main):003:0> letters[:a] => [1] irb(main):004:0> letters => {:a=>[1]} irb(main):005:0> letters[:b] # Returns default object (same object as letters[:a])! => [1] ~~~ To get a different object for each fetch of a non-existent key, use a Hash with a default Proc that returns new object every time: ~~~ irb(main):001:0> letters = Hash.new {[]} # Default Proc returns new instance each call => {} irb(main):002:0> letters[:a] <<= 1 => [1] irb(main):003:0> letters[:a] => [1] irb(main):004:0> letters => {:a=>[1]} irb(main):005:0> letters[:b] # Returns different empty Array until letters[:b] is assigned => [] irb(main):006:0> letters[:b].object_id => 2152992900 irb(main):007:0> letters[:b].object_id => 2153118760 irb(main):008:0> letters[:b].object_id => 2153112180 irb(main):009:0> letters[:b] = letters[:b] => [] irb(main):010:0> letters[:b].object_id => 2156907140 irb(main):011:0> letters[:b].object_id => 2156907140 ~~~ Given all the trickiness and because sometimes you don't have control over the creation of the Hash, I generally end up doing this instead: ~~~ >> # h is any Hash, e.g. from method call >> h[:a] ||= [] => [] >> h[:a] << 1 => [1] # etc... ~~~ This uses the "||=" idiom, which can obliterate an existing `nil` or `false` value, so you still have to be a little careful (though it's very rarely an issue). ---------------------------------------- Bug #10713: Assigning default value for a Hash as an empty Array creating unpredictable results https://bugs.ruby-lang.org/issues/10713#change-50853 * Author: Arvinder Singh * Status: Rejected * Priority: Normal * Assignee: * Category: core * Target version: current: 2.2.0 * ruby -v: ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14] * Backport: 2.0.0: UNKNOWN, 2.1: UNKNOWN, 2.2: UNKNOWN ---------------------------------------- Creating a Hash with a default value works fine, unless the default value is an empty Array. E.g. the following returns an empty Hash... ~~~ irb(main):001:0> letters = Hash.new([]) => {} irb(main):002:0> letters[:a] << 1 => [1] irb(main):003:0> letters[:a] << 2 => [1, 2] irb(main):004:0> letters[:a] => [1, 2] irb(main):005:0> letters => {} ~~~ whereas the following code explicitly defining hash keys works. ~~~ irb(main):001:0> letters = {a: [], b: []} => {:a=>[], :b=>[]} irb(main):002:0> letters[:a] << 1 => [1] irb(main):003:0> letters[:a] << 2 => [1, 2] irb(main):004:0> letters[:a] => [1, 2] irb(main):005:0> letters => {:a=>[1, 2], :b=>[]} ~~~ Is this an unpredictable(bug) or an expected behavior(feature). I tend to lean towards the former, but I might be wrong. -- https://bugs.ruby-lang.org/