Reputation:
I have the following code which is causing me problems around the line I've marked.
arr = 'I wish I may I wish I might'.split
dictionary = Hash.new
arr.each_with_index do |word, index|
break if arr[index + 2] == nil
key = word << " " << arr[index + 1] #This is the problem line
value = arr[index + 2]
dictionary.merge!( { key => value } ) { |key, v1, v2| [v1] << v2 }
end
puts dictionary
Running this code, I would expect the following output:
{"I wish"=>["I", "I"], "wish I"=>["may", "might"], "I may"=>"I", "may I"=>"wish"}
However, what I instead get is
{"I wish"=>["I may", "I"], "wish I"=>["may I", "might"], "I may"=>"I wish", "may I"=>"wish I"}
I've found that if I replace the problem line with
key = word + " " + arr[index + 1]
Everything works as expected. What is it about the first version of my line that was causing the unexpected behaviour?
Upvotes: 0
Views: 60
Reputation: 67850
key = word << " " << arr[index + 1]
The problem is that String#<< performs an in-place operation so the string is modified the next time it's used. On the other hand String#+ returns a new copy.
You have been bitten by an imperative side-effect (which is not unusual since side-effects are a huge source of bugs. Unless there are very compelling performance reasons, a functional approach yields better code). For example, that's how it could be written using each_cons
and map_by
from Facets:
words = 'I wish I may I wish I might'.split
dictionary = words.each_cons(3).map_by do |word1, word2, word3|
["#{word1} #{word2}", word3]
end
Upvotes: 1
Reputation: 19475
The String#<< method modifies the original object on which it is called.
Here that is the object referred to by your word
variable which is just
another reference to one of the Strings in the arr
Array. You can see this
effect with the code:
a = 'Hello'
b = a << ' ' << 'World'
puts a.__id__
puts b.__id__
So when you use that method in one pass through the iterator it affects the following passes as well.
On the other hand the String#+ method creates a new String object to hold the combined strings. With this method one pass through the iterator has no effect on other passes.
Upvotes: 1