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"