user360907
user360907

Reputation:

Appending strings using << does not work as expected, but using + does

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

Answers (2)

tokland
tokland

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

qqx
qqx

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

Related Questions