12

I ran into this weird side effect that caused a bug or confusion. So imagine that this isn't a trivial example but an example of a gotcha perhaps.

name = "Zorg"

def say_hello(name)
  greeting = "Hi there, " << name << "?"
  puts greeting
end

say_hello(name)
puts name

# Hi there, Zorg?
# Zorg

This doesn't mutate name. Name is still Zorg.

But now look at a very subtle difference. in this next example:

name = "Zorg"

def say_hello(name)
  greeting = name << "?"
  puts "Hi there, #{greeting}"
end

say_hello(name)
puts name

# Hi there, Zorg?
# Zorg?  <-- name has mutated

Now name is Zorg?. Crazy. Very subtle difference in the greeting = assignment. Ruby is doing something internally with the parsing (?) or message passing chaining? I thought this would just chain the shovels like name.<<("?") along but I guess this isn't happening.

This is why I avoid the shovel operator when trying to do concatenation. I generally try to avoid mutating state when I can but Ruby (currently) isn't optimized for this (yet). Maybe Ruby 3 will change things. Sorry for scope-creep / side discussion about the future of Ruby.

I think this is particularly weird since the example with less side effects (first one) has two shovel operators where the example with more side effects has fewer shovel operators.

Update You are correct DigitalRoss, I'm making it too complicated. Reduced example:

one = "1"
two = "2"
three = "3"
message = one << two << three

Now what do you think everything is set to? (no peeking!) If I had to guess I'd say:

one is 123
two is 23
three is 3
message is 123

But I'd be wrong about two. Two is 2.

squarism
  • 3,242
  • 4
  • 26
  • 35
  • 1
    Just a note, the canonical way to join strings in Ruby is interpolation, rather than concatenation with plus or double shovel. That avoids the mutation question altogether. I am guessing that Matz chose to make the double shovel associate the way it does to support the use case of "a" << b << "c". Otherwise people coming from languages like C++ would have been quite surprised to see the side effects. Re avoiding mutating state, Ruby supports functional style as well as many other styles; Ruby isn't a pure functional language and I think that's intentional. – Brenton Fletcher Nov 30 '16 at 03:50
  • Totally. I usually do "#{one} #{two} #{three}". – squarism Nov 30 '16 at 18:13

3 Answers3

9

If we convert your a << b << c construct to a more method-ish form and throw in a bunch of implicit parentheses the behavior should be clearer. Rewriting:

greeting = "Hi there, " << name << "?"

yields:

greeting = ("Hi there, ".<<(name)).<<("?")

String#<< is modifying things but name never appears as the target/LHS of <<, the "Hi there ," << name string does but name doesn't. If you replace the first string literal with a variable:

hi_there = 'Hi there, '
greeting = hi_there << name << '?'
puts hi_there

you'll see that << changed hi_there; in your "Hi there, " case, this change was hidden because you were modifying something (a string literal) that you couldn't look at afterwards.

mu is too short
  • 426,620
  • 70
  • 833
  • 800
3

You are making it too complicated.

The operator returns the left-hand side and so in the first case it's just reading name (because "Hi there, " << name is evaluated first) but in the second example it is writing it.

Now, many Ruby operators are right-associative, but << is not one of them. See: https://stackoverflow.com/a/21060235/140740

Community
  • 1
  • 1
DigitalRoss
  • 143,651
  • 25
  • 248
  • 329
1

The right-hand side of your = is evaluated left to right.

When you are doing

"Hello" << name << "?"

The operation starts with "Hello", adds name to it, then adds "?" to the mutated "Hello".

When you do

name << "?"

The operation starts with name, and adds "?" to it, mutating name (which exists outside the internal scope of the method.

So in your example of one << two << three, you are mutating only one.

Mike Manfrin
  • 2,722
  • 2
  • 26
  • 41