0

I'm just trying to make sure I understand what's happening here. I get that += is reassignment so maybe that's why str isn't modified but why doesn't upcase! modify str here either?

def change_me(str)
  str += "?"
  str.upcase!
end

question = "whats your name"
change_me(question)

puts question

'whats your name'
=> nil
  • This is because Ruby uses `pass by value` while it is passing the arguments. – Rajagopalan Jun 28 '20 at 16:38
  • that's not quite true @Rajagopalan, if you remove the `str += "?"` line, the method does in fact change the passed string – Mario F Jun 28 '20 at 16:39
  • 1
    There are two ways to do what you want to do. One is write `def change_me(str); str.replace(str.upcase + "?"); end`, which uses the method [String#replace](https://ruby-doc.org/core-2.7.0/String.html#method-i-replace). The other is to change the operative line of the method to `str[0..-1] = str.upcase + "?"`, which uses [String#\[\]=](https://ruby-doc.org/core-2.7.0/String.html#method-i-5B-5D-3D). – Cary Swoveland Jun 28 '20 at 16:55
  • @MarioF Nope! Ruby is pass by value, please read this answer https://stackoverflow.com/a/10330589/9043475 and very importantly read the comment below this answer. – Rajagopalan Jun 28 '20 at 16:57
  • @CarySwoveland She is not asking to achieve the result, she is asking why that works that way! – Rajagopalan Jun 28 '20 at 17:07
  • @Rajagopalan, yes, I know, but Mario answered that. This is just a comment. – Cary Swoveland Jun 28 '20 at 17:09
  • I edited my answer. The problem is that `str` is being reassigned to a new value, so it does not point to the string passed as an argument anymore. Thus the initial string is left unchanged. If you don't do that, the value of the passed string is in fact modified. Bottom line is, passing a string to a function does not make a completely new copy of it. – Mario F Jun 28 '20 at 17:11
  • 1
    You can run the methods I suggested in my previous comment and @Mario's "side-effect" method [here](https://repl.it/@CarySwoveland/WirelessTestyLearning). – Cary Swoveland Jun 28 '20 at 17:12

3 Answers3

5

Does 'upcase!' not mutate a variable in Ruby?

It is impossible for a method to mutate a variable. Ruby is an object-oriented language, so methods can mutate objects (more precisely, a method can mutate its receiver), but variables aren't objects in Ruby. (Like most other languages as well, there is almost no language where variables are objects.)

The only way to mutate a variable is through assignment. Note that we generally don't talk about "mutating" variables, we talk about "re-binding" or "re-assigning".

I'm just trying to make sure I understand what's happening here. I get that += is reassignment so maybe that's why str isn't modified but why doesn't upcase! modify str here either?

Again, you are confusing variables and objects. upcase! modifies the object that is referenced by str, but it does not modify str.

It sounds like you expect Ruby to be a pass-by-reference language, but it is not. Ruby is purely pass-by-value, always, no exceptions. More precisely, the value being passed is an unmodifiable, unforgeable pointer to an object.

Here's what happens, following the flow of execution:

question = "whats your name"

  • The string literal "whats your name" is evaluated, resulting in a String object with the content whats your name.
  • The local variable question is initialized with an immutable, unforgeable pointer to the string object created in step #1.

change_me(question)

  • The local variable question is dereferenced, resulting in the immutable, unforgeable pointer to the string object created in step #1.
  • A copy of that pointer is made.
  • The copy from step #4 is placed into the argument list of the call to change_me

str += "?"

  • Inside of the change_me method body, the parameter binding str is bound to the copied immutable unforgeable pointer from step #4 and #5.
  • This line desugars into str = str + "?", so what happens is:
    • str is dereferenced, resulting in the copied immutable, unforgeable pointer from step #4, #5, and #6.
    • We follow the pointer and send the message + to the object with an immutable, unforgeable pointer to the string object created by evaluating the string literal "?" as an argument.
    • String#+ returns a new string (or, more precisely, an immutable, unforgeable pointer to a new string).
    • str is re-bound to the new immutable, unforgeable pointer returned by the call to str+("?").

str.upcase!

  • str is dereferenced, resulting in the new immutable, unforgeable pointer from step #7c #7d.
  • We follow the pointer and send the message upcase! to the object.
  • String#upcase! will mutate the receiver object (in this case, the newly created string from step #7c) to make all letters uppercase.
  • String#upcase! will return either an immutable, unforgeable pointer to the receiver object itself (i.e. the pointer that was used to call the method) if it did any changes to the receiver, or it will return an immutable, unforgeable pointer to the object nil if the string was already uppercase or didn't contain any letters.

Back to change_me(question)

  • This return value, however, is just ignored, it is thrown away, it is not printed, not assigned to a variable, not passed as an argument to a different method, not stored in a data structure.

puts question

Okay, I will save the details now that the variable is dereferenced, etc.

The crucial part is: the variable question was never touched, it was never re-assigned, so it still contains the exact same thing it contained the whole time: the immutable, unforgeable pointer to the string object from steps #1 and #2.

We assigned this object to the variable, and we:

  • never re-assigned the variable, so the variable still points to the same object
  • never asked the object to mutate itself, so the contents of the object are still the same

Therefore, the object is still unchanged, and the variable still points to the same object, and thus we get the result that nothing has changed.

We changed the binding for the str parameter binding inside of the change_me method, but that binding is local to the method. (Parameter bindings are effectively equivalent to local variables.) Therefore, it ceased to exist the moment the method returned.

And we changed the newly created string object, but since we never obtained a pointer to this object, there is no way that we can reach it. One pointer was stored in str, but that is gone. Another pointer was returned from change_me, but we threw that away, so that is gone, too. Since there is no reference this string object, the object is unreachable.

In fact, the change_me method doesn't do anything at all that can be observed from the outside. It creates a string object, then mutates it, but no reference to this object ever leaves the method. Therefore, it is as good as if the mutation never happened, and the string object never existed in the first place.

In fact, a sufficiently clever compiler would be able to optimize your entire code to this:

puts "whats your name"
Jörg W Mittag
  • 363,080
  • 75
  • 446
  • 653
  • Hi, very nice explanation, I am rereading again and again for more understanding. Can you tell me which language is allowing pass by reference? – Rajagopalan Jun 28 '20 at 18:17
  • 1
    @Rajagopalan [C#](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/passing-parameters) allows you to do so, although I can't say what's actually going on under the hood. – BobRodes Jun 28 '20 at 19:09
  • @Rajagopalan: [C# is pass-by-value by default, but supports pass-by-reference if you explicitly mark both the parameter and argument](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/classes#reference-parameters). C++ and PHP are also pass-by-value by default and pass-by-reference by choice. FORTRAN II is pass-by-reference. [Here is a comparison of passby-value and pass-by-reference in C#](https://stackoverflow.com/a/51364123/2988). – Jörg W Mittag Jun 28 '20 at 19:45
3

when you are doing str += "?" you are creating a new string, so str points to a different string than the one you are passing as an argument.

What you are doing is essentially this:

def change_me(str)
  new_str = str + "?"
  new_str.upcase!
end

That is why your previous string is not being changed. If you want the function to have side effects, you should do:

def change_me(str)
  str << "?"
  str.upcase!
end

However, I think modifying strings in place is a bit questionable. I think it would be safer to return a new string and overwrite your reference if needed.

Mario F
  • 45,569
  • 6
  • 37
  • 38
  • But then, shouldn't upcase! then alter the new string object as well? I still expect a new str object whose value is "WHATS YOUR NAME?". – Subscription Services Jun 28 '20 at 16:45
  • it is doing so. If you store the new string in an extra variable you'll see that it was modified – Mario F Jun 28 '20 at 16:57
  • He is not asking a way to achieve that! – Rajagopalan Jun 28 '20 at 16:58
  • 1
    @SubscriptionServices: What makes you think it doesn't return an altered str that should say "WHATS YOUR NAME?" You never look at the return value, you don't print it, you don't assign it to a variable, you don't pass it as an argument, you ignore it and throw it away. How would you know what it does and doesn't return? – Jörg W Mittag Jun 28 '20 at 17:06
  • Thank you for the answers, I really just needed to find out where the flaw in my logic is. It's less important to me to make this work irl. – Subscription Services Jun 28 '20 at 20:08
  • @MarioF : _I think modifying strings in place is a bit questionable._ Indee; at least it smells bad. However, if the OP would rename the method into `change_me!` , I think it is OK, because we can see that something funny is going on. – user1934428 Jun 29 '20 at 10:26
0

Let's see if I can boil all this down a bit for you. First, have a careful look at Mario's "what you are doing is essentially this" code example. Understand that you are calling your #upcase! method on an entirely new object, since you reassigned str to a new object when you tried to tack a ? onto it.

Now, have a look at this:

def change_me(str)
  str.upcase!
  42
end

x = 'hello'
puts x # => hello
change_me(x)
puts x # => HELLO

As you can see, this code returns 42. Now, as Douglas Adams has told us, 42 is the meaning of life. But if so, the meaning of life is entirely irrelevant here, because as Jörg has been trying to explain to you, you don't do anything with the return value of your method call.

You will also see that your str object does get mutated here. That's because in this case, you haven't reassigned the str variable to a different object inside your method, as your code does. (Again, look carefully at Mario's first example.)

Now, if, in your method, you want to tack something onto the end of the object that you send into your method, you need to use << instead of +. Look at Mario's second code example, and give that a try.

To dig down into this and learn it thoroughly, the #object_id method is very useful. Try running this code:

def change_me(str)
  p str.object_id
  str += "?"
  p str.object_id
  str.upcase!
  p str.object_id
end

def change_me_2(str)
  p str.object_id
  str << "?"
  p str.object_id
  str.upcase!
  p str.object_id
end

If you spend some time evaluating object ids, you'll sort this out for yourself pretty quickly.

Finally, I second Mario's point of view that modifying strings in place is a bit questionable in practice. Unless there's some reason that you can't do it this way, I would do this:

def change_me(str)
  str.upcase + '?'
end

And then:

question = "what's your name"
question = change_me(question)

Or simply:

question = change_me("what's your name")

Finally, here's a little quiz. Take your code and change the way you call it so:

def change_me(str)
  str += "?"
  str.upcase!
end

question = "whats your name"
puts change_me(question)

Why does this do what you intended? Now, change str.upcase! to str.upcase, and you will see that it also does what you intended. Why doesn't it make any difference whether you use the ! or not?

BobRodes
  • 5,990
  • 2
  • 24
  • 26