Under certain conditions Hash#keys does not work correctly in Ruby before version 2.4
Demo code:
h = { a: 1, b: 2, c: 3 }
h.each do |k, v|
h.delete(:a)
p h
p h.keys
break
end
Ruby 2.3.8 output:
{:b=>2, :c=>3}
[:b]
Ruby 2.5.1 output:
{:b=>2, :c=>3}
[:b, :c]
I agree it is not good to modify hash when iterating. But I did not see the relation between the modification the hash and the work keys method.
Why is this happening?
Interesting question. This isn't an answer yet, but it's too long for a comment and it could help others answer the question.
Which Rubies are affected?
I created a GitHub repository with a very simple spec:
describe Hash do
it "should always know which keys are left" do
h = { a: 1, b: 2, c: 3 }
h.each do |k, v|
h.delete :a
expect(h.keys).to eq [:b, :c]
end
end
end
Thanks to Travis, it's easy to see which Ruby versions have this bug:
Ruby 2.1
Ruby 2.2
Ruby 2.3
When did the bug appear?
The bug wasn't in ruby-2.1.0-preview2
The bug was in ruby-2.1.0-rc1
When was the bug fixed?
https://github.com/ruby/ruby/tree/v2_4_0_preview2 was the last tag with this bug.
https://github.com/ruby/ruby/tree/v2_4_0_preview3 is the first tag without this bug.
I just spent an hour using git bisect and make install in order to find that the bug has been fixed in this commit (75775157).
Introduce table improvement by Vladimir Makarov
.
[Feature #12142] See header of st.c for improvment details.
You can see all of code history here:
https://github.com/vnmakarov/ruby/tree/hash_tables_with_open_addressing
This improvement is discussed at
https://bugs.ruby-lang.org/issues/12142 with many people,
especially with Yura Sokolov.
st.c: improve st_table.
include/ruby/st.h: ditto.
internal.h, numeric.c, hash.c (rb_dbl_long_hash): extract a
function.
ext/-test-/st/foreach/foreach.c: catch up this change.
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk#56650
b2dd03c8-39d4-4d8f-98ff-823fe69b080e
It has been confirmed by #Vovan, who found this commit 1 minute before I did.
Related
I recently created a pull request for a gem which is building on travis against quite old ruby versions for backward compatibility.
In my commit I wanted to introduce a whitelist on some method options passed as an hash parameter.
In Rails with a recent ruby version it would look like:
MY_WHITELIST = %i(a b c)
def my_method(options={})
#options = options.slice(*MY_WHITELIST)
end
In order to grant backward compatibility in a standalone gem, I provided a solution like:
MY_WHITELIST = [:a, :b, :c]
def my_method(options={})
#options = options.select { |k, _| MY_WHITELIST.include?(k) }
end
This pass for ruby 1.9.3 but raise the following exception for 1.8.7:
TypeError: Symbol as array index
According to the documentation, this way to initialise an array should be accepted.
Have you ever experienced with use? What would you suggest?
As suggested in a comment by #mr_sudaca, the solution was to make the selection on an array:
Hash[options.to_a.select { |k, _| MY_WHITELIST.include?(k) }]
A basic example of what is confusing me:
def [](row, col)
self[row][col]
end
x = [[1,3], [4,5]]
x[0][0] #normal
x[0, 0] #syntactic sugar?
I have been told that these are equivalent statements, but when I run the sugar one, I get a different answer. How am I supposed to write it in syntactic sugar?
You need to put your def [](row, col) method in a class that contains your data. So something like:
$ irb
2.3.0 :001 > class MyData
2.3.0 :002?> attr_accessor :my_array
2.3.0 :003?> def [](row, col)
2.3.0 :004?> my_array[row][col]
2.3.0 :005?> end
2.3.0 :006?> end
=> :[]
2.3.0 :007 > x = MyData.new
=> #<MyData:0x007f96dc8024b8>
2.3.0 :008 > x.my_array = [[1, 3], [4, 5]]
=> [[1, 3], [4, 5]]
2.3.0 :009 > x[1,1]
=> 5
2.3.0 :010 >
There are two problems with this:
You are adding your method to the Object class. But Array has its own [] which overrides the one in Object, so your method never gets called …
… which is a good thing, because it doesn't work anyway: all the method does is call itself, that would lead to a stackoverflow because of runaway recursion, but thankfully, the method calls itself with one argument, but is defined to take two parameters, and thus all you get is an ArgumentError.
Now, forgetting for a moment that monkey patching a core class is a terrible idea, you could to this:
module TwoDArrayExtension
def [](x, y)
super(x).method(__callee__).super_method.(y)
end
end
class Array
prepend TwoDArrayExtension
end
x = [[1, 3], [4, 5]]
x[0][0] #normal
# in `[]': wrong number of arguments (given 1, expected 2) (ArgumentError)
# Hey, we just re-defined [] to take two arguments, so obviously this cannot work!
x[0, 0] #syntactic sugar?
#=> 1
This "works" in the sense that it gets your syntactic sugar example to pass. But your "normal" example now breaks: you have re-defined how arrays work, so you can no longer use it like an array. Tinkering with core classes like this has serious ramifications. For example:
Irb is written in Ruby, and uses arrays internally, among other things. You cannot test your code in Irb, because it will just crash.
The same applies to Pry.
The same applies to any and all existing code that uses arrays … which is pretty much all Ruby code ever written. You cannot use any gems. You cannot use and standard libraries.
In fact, in an implementation like Rubinius, where pretty much everything, including large parts of the core libraries themselves, and even the compiler are written in Ruby and thus use arrays internally, you probably won't even be able to get your code to run at all.
In short: yes, you can do this, but you really, really, really, REALLY don't want to.
One of the examples in Peter Cooper's Beginning Ruby for polymorphism involves the to_s method. He gives this example:
puts 1000.to_s
puts [1, 2, 3].to_s
puts ({ :name => 'Fred', :age => 10 }).to_s
and shows this as the output:
1000
123
age10nameFred
but the output I get is:
1000
[1, 2, 3]
{:name=>"Fred", :age=>10}
Does anyone know why this would be the case? Was there a change in ruby, or is there something I'm doing wrong? Or not enough info to tell? How can I find it out?
The examples work using ruby 1.8.7, which is getting a bit dated. Ruby 1.9.3 (the current version) changed the to_s implementation for Arrays and Hashes.
EDIT: See Ruby 1.9 Array.to_s behaves differently?
We're doing a bit of work in Ruby 1.8.7 that requires traversing and partitioning an undirected graph, that has been failing weirdly in production. When I distil the failing code down to its barest components, I get this strangely failing test:
it 'should be able to clear a ruby set of arrays' do
a = ["2", "b", "d"]
b = ["1", "a", "c", "e", "f"]
set = Set.new([a, b])
a.concat(b)
p "before clear: #{set.inspect}"
set.clear
p "after clear: #{set.inspect}"
set.size.should == 0
end
The test fails with this output:
"before clear: #<Set: {[\"1\", \"a\", \"c\", \"e\", \"f\"], [\"2\", \"b\", \"d\", \"1\", \"a\", \"c\", \"e\", \"f\"]}>"
"after clear: #<Set: {[\"2\", \"b\", \"d\", \"1\", \"a\", \"c\", \"e\", \"f\"]}>"
expected: 0
got: 1 (using ==)
Attempts to delete from the set also behave in strange ways. I'm guessing that Ruby is getting hung up on the hash values of the keys in the array changing under concat(), but surely I should still be able to clear the Set. Right?
There is a workaround for this, if you duplicate the set after you modify the keys, the new set will have the updated keys and clear properly. So setting set = set.dup will fix that problem.
The .dup approach was indeed my first work-around, and did as advertised.
I ended up adding the following monkey-patch to Set:
class Set
def rehash
#hash.rehash
end
end
which allows me to rehash the set's keys after any operation that changes their hash values.
This appears to all be fixed in Ruby 1.9.
I'd like to have my array items scrambled.
Something like this:
[1,2,3,4].scramble => [2,1,3,4]
[1,2,3,4].scramble => [3,1,2,4]
[1,2,3,4].scramble => [4,2,3,1]
and so on, randomly
Built in now:
[1,2,3,4].shuffle => [2, 1, 3, 4]
[1,2,3,4].shuffle => [1, 3, 2, 4]
For ruby 1.8.6 (which does not have shuffle built in):
array.sort_by { rand }
For ruby 1.8.6 as sepp2k's example, but you still want use "shuffle" method.
class Array
def shuffle
sort_by { rand }
end
end
[1,2,3,4].shuffle #=> [2,4,3,1]
[1,2,3,4].shuffle #=> [4,2,1,3]
cheers
Code from the Backports Gem for just the Array for Ruby 1.8.6. Ruby 1.8.7 or higher is built in.
class Array
# Standard in Ruby 1.8.7+. See official documentation[http://ruby-doc.org/core-1.9/classes/Array.html]
def shuffle
dup.shuffle!
end unless method_defined? :shuffle
# Standard in Ruby 1.8.7+. See official documentation[http://ruby-doc.org/core-1.9/classes/Array.html]
def shuffle!
size.times do |i|
r = i + Kernel.rand(size - i)
self[i], self[r] = self[r], self[i]
end
self
end unless method_defined? :shuffle!
end
The Ruby Facets library of extensions has a Random module which provides useful methods including shuffle and shuffle! to a bunch of core classes including Array, Hash and String.
Just be careful if you're using Rails as I experienced some nasty clashes in the way its monkeypatching clashed with Rails'...