Ruby: Why is my method non-destructive? - ruby

I'm trying to create two methods, one of which will destructively add "i" to any string, and one of which will do the same non-destructively.
def add_i(string)
new_string = string + "i"
end
def add_i!(string)
string = string + "i"
end
My questions are:
Both of these methods are non-destructive, even though I do not replace the argument in the second method with a new variable. Why?
In general, how do I convert a non-destructive method into a destructive one and vice versa?

The answer lies in the scope of the vars and the behavior of the methods/operators. the left hand side (left to the = )string inside add_i! is a different string than the one passed in (the right side string and the method arg). The old string continues to live on but the string var points to the new one.
to make the 2nd method "destructive" you could do something like:
def add!(string)
string << "i"
end
as a rule of the thumb, you need to understand if the methods/operators you are applying are operating on the data itself or are returning a copy of the data (for example the '+' upstairs returns a copy)
an easy way of dealing with string and making sure you don't destroy the data is to use dup() on whatever is passed in and after that operate on the copy.

The problem is + returns a copy of the string. I guess this is analgous to adding numbers which will return two numbers. string += 'i' also makes a copy. This a bit surprising, but it is the same as what numbers do.
You can see this by checking the object_id at each point.
def add_i!(string)
puts "string passed into add_i! #{string.object_id}"
string = string + "i"
puts "string in add_i after + i #{string.object_id}"
end
foo = "blah"
puts "string about to be passed into add_i! #{foo.object_id}"
add_i!(foo)
puts "string after calling add_i! #{foo.object_id}"
string about to be passed into add_i! 70364940039240
string passed into add_i! 70364940039240
string in add_i after + i 70364940039020
string after calling add_i! 70364940039240
Note that string in add_i after + i has a different object id.
<< and concat both append to the existing string. concat should probably be concat! but it isn't.

Related

Modifying arguments in Ruby

I apologize for the excessive length, I just wanted to make sure I actually understand what's going on here. This is a follow up on my previous two questions Does 'upcase!' not mutate a variable in Ruby? and Destructive and non destructive methods in Ruby.
def changeMe(string)
string + "?"
end
phrase = "how are you"
puts changeMe(phrase) #how are you?
puts phrase #how are you
When changeMe is invoked with the phrase passed in as an argument, the parameter string points to the same object as phrase. When we change the line to string + "?" we are creating a new object different from the one the string parameter points to, the same if we assigned the newly created object to a variable.
def changeMe(string)
string += "?"
end
phrase = "how are you"
puts changeMe(phrase) #how are you?
puts phrase #how are you
If I do this -
def changeMe(string)
string + "?"
string.capitalize!
end
phrase = "how are you"
puts changeMe(phrase) #How are you
puts phrase #How are you
When changeMe is invoked with phrase passed in as an argument, the string + "?" creates a new object different from the one #capitalize! is called on in the next line. #capitalize! is called on the object that the variable phrase is referencing, the same object the string parameter points to but not the same object returned by string + ?. If we reassign it to a variable,
def changeMe(string)
string += "?"
string.capitalize!
end
phrase = "how are you"
puts changeMe(phrase) #How are you?
puts phrase #how are you
string += "?" will create a new object that is assigned to a variable called string. That new object has #capitalize! called on it. The method is invoked with phrase passed in as an argument and returns a new object different from the one the variable phrase references so the original value for the variable phrase is unchanged.
Are there flaws or misconceptions in my logic. Am I accurately explaining/understanding things?
That's largely correct, but perhaps a more complicated path to understanding than necessary. In Ruby one thing that helps a lot is calling object_id on a given object to see which object it is. Every object has a unique object_id.
For example:
"test" == ("te" + "st")
# => true
"test".object_id == ("te" + "st").object_id
# => false
Or more specifically for a method that creates a new copy:
x = 'test'
y = x + '?'
x.object_id == y.object_id
# => false
You can see how in-place modifications work:
x = 'test'
y = x << '?'
x.object_id == y.object_id
# => true
Where this allows you to differentiate between in-place modifications and methods that produce new objects or copies.
Remember that every Ruby expression returns an object. If this object is not captured into a variable or used as an argument will often can be discarded if not already used.
In other words there's a huge difference between this:
def add
1 + 2 # Computed and discarded
:three # The actual return value
end
And this:
def add
1 + 2 # Computed and returned
end
Though this depends on that return value being captured, as calling the function computes the value and throws out the results again unless it's captured or used.

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

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
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"
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.
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?

Does it matter which way a string method is used?

Codeacademy teaches that you can chain multiple methods together as such:
user_input.method1.method2.method3
However, in a later lesson they display some methods like this:
user_input = gets.chomp
user_input.downcase!
I combined them:
user_input = gets.chomp.downcase!
When I use it this way:
user_input = gets.chomp.downcase!
if user_input.include? "s"
...
I receive an error "undefined method `include?'". If I change it to the following, it works fine:
user_input = gets.chomp
user_input.downcase!
if user_input.include? "s"
...
I'm at a loss. I'm concerned whether or not this is a quirk with their console or if this is just how I should be doing it in Ruby. If someone could tell me which way is right, I'd appreciate it. If both are right, that's OK too.
Firstly, in case you do not yet fully understand, assignment of values to variables are done through =, and that you could inspect what variable type it is by appending .class to anything.
Consider the following:
name = 'John'
puts name
# => John
puts name.class
# => String
Now, secondly, it should be noted that the return values of ALL methods are ALL different. But all of them can be identified into two types:
Methods that:
return self
return anything other than self
Example for 1.
-- methods that return self, which you could say methods that return the same type of object which in our specific case, a String
name = 'John'
puts name
# => 'John'
puts name.class
# => String
downcased_name = name.downcase
puts downcased_name
# => john
puts downcased_name.class
# => String
deleted_downcased_name = downcased_name.delete('h')
puts deleted_downcased_name
# => jon
puts deleted_downcased_name.class
# => String
# All of the above can be just simplified into:
deleted_downcased_name2 = 'John'.downcase.delete('h')
puts deleted_downcased_name2
# => jon
puts deleted_downcased_name2.class
# => String
Notice that deleted_downcased_name and deleted_downcased_name2 are the same, because you could treat the chained methods as if you are chaining the return values which is 'John' -> 'john' -> 'jon'.
Example for 2
-- methods that return anything but self, which you could say methods that return a different type.
In our specific case, String's downcase! returns either a String or NilClass (reference here)
returning String if the string changes, or
returning nil if string is already downcased to begin with (no change).
or another String's method: start_with? (reference here)
returning true or false
This is where chaining of methods will not work (raises an error), when you try to use a String method as a chain to nil value.
Consider the following
name = 'mary'
puts name
# => 'mary'
puts name.class
# => String
downcased_name = name.downcase!
puts downcased_name
# => nil
puts downcased_name.class
# => NilClass
downcased_name.delete('h')
# => This will raise the following error
# NoMethodError: undefined method `delete' for nil:NilClass
The error above is because downcased_name is a type of NilClass where you are expecting it to be a type of String. Therefore you cannot chain any string method on it anymore. You can only chain String methods on a String type of value. Similarly, you can only chain Number methods on a Number type of value.
Whenever in doubt, you could always check the documentation to check what a method does, and what its return value and type.
The problem you are encountering is with the bang method downcase!.
This is basically saying "mutate the original string so that it is downcase".
The important part is that this returns nil. As such you are actually calling include? on nil.
If you use the non bang method downcase instead, it is saying "downcase the previously chained thing but do not mutate the original". The key difference is that it returns the result rather than nil.
Here is an example:
str = "ABCD"
str.downcase!
=> nil
str
=> "abcd"
str = "ABCD"
str.downcase
=> "abcd"
str
=> "ABCD" # Note str is still unchanged unless you set str = str.downcase
Welcome to Ruby! While your apprenticeship at Codeacademy may be limited, you'll continue to refer to language API documentation throughout your career. API documentation is a description of what the language (or a library) does for you. In this case, you're using downcase! which, as one commenter points out, does not always return a String. When it takes no action, it returns nil. Nil is an Object in Ruby (like everything else), but the 'include?' method isn't defined for nil, which explains your error. (It's one of the most common errors in Ruby, learn its meaning.)
So, in fact, what's breaking here isn't your method chain. It's that one of the intermediate methods isn't returning a value of the type you expect (nil instead of some kind of String).
Chaining non destructive methods like:
string.chomp.downcase...
has the advantage that the code is concise, but is not efficient if you are not interested in the original state of the object, and just want the final result because it creates intermediate objects during the chain.
On the other hand, applying destructive methods sequentially to the same object:
string.chomp!
string.downcase!
...
is more efficient if you do not need to keep the original state of the object, but is not concise.
Combining methods that may return an object of a different class (particularly nil) as:
string = gets.chomp!.downcase!...
is wrong because the result can become nil at some point in the chain.
Applying a potentially nil-returning method at only the last position as you did:
string = gets.chomp.downcase!
is still not useful if you expect string to always be a string, and can easily lead to an error as you did.
If you want to chain these methods in you example, perhaps you can do this:
user_input = gets.tap(&:chomp!).tap(&:downcase!)
if user_input.include?("s")
...

Exclamation mark and assignment inside of a function

Can someone explain why this:
def do_something str
str = "bar"
end
​
str_main = "foo"
do_something str_main
​
puts str_main
displays foo?
And this:
def do_something str
str.capitalize!
end
​
str_main = "foo"
do_something str_main
​
puts str_main
displays Foo?
Because of the way Ruby passes arguments.
When the method is being called, you have two references, str_main and str, to the same object "foo".
In the first example, when you use str = "bar", you are just changing what the str reference points to. So now you have str_main -> "foo" and str -> "bar". Therefore, the original object is not changed.
In the second example, you didn't change the str reference and changed the string in place with a mutator method, thus changing the same object that str_main points to.
The exclamation mark or bang operator modifies the original value. It is a destructive method. For example, let's say you had a string
string = "hi";
If you call the upcase method, you will get the following
string.upcase
=> "HI"
However, if you call string again, you will get the initial value.
string
=> "hi"
Now, let's say you use the destructive method upcase!
string.upcase!
=> "HI"
Now, if you call string again, you will see that the value was mutated.
string
=> "HI"
In Ruby, references are passed by value. So, a reference to str_main is passed to method do_something, a copy of reference is present in variable str.
This, however, does not mean that value that is referred to by both variables also has been copied around - there is still a single copy of referred to value, which is the string defined in Main.
Hence, when you assign a new value to str, this does not alter the value of str_main. However, when you modify the value that is referred by str, its changes are visibble outside.
All ruby methods return the last thing evaluated. However, object assignment stays within the scope of the current code block. Assigning str_main to a new value within a method will not affect str_main, unless it was an instance variable (#str_main). Doing such allows you to reassign an object across scopes, or depths, of your program. This is why your first method outputs 'foo' instead of 'bar'.
Now, the second example. #capitalize is a method called on a string object. It returns a new String instance, where its value is original object capitalized.
string = 'foobar'
string.capitalize # => 'Foobar'
puts string # => 'foobar'
Notice how string is only modified temporarily, and when called again it is back to normal.
Many methods in ruby have counterparts ending in !. This convention is the same as: object = object.some_method. Instead of creating a new instance of an object, these methods edit the original object's value. In the case of #capitalize!, the string is capitalized and modified for future calls.
string = 'foo'
string.capitalize! # => 'Foo'
puts string # => 'Foo'
Back to your second example. Using the #capitalize! method within the scope of do_something allows you to modify the str_main object. In a similar way to making str_main an instance variable.

Classes in case statements

I'm unable to determine why these two functions behave differently. I could just use symbols or my own constants, but I have a deep desire to know what's going on here (and if I'm doing something bad).
def convert(value, type)
case type
when Integer
value.to_i
when String
value.to_s
else
value
end
end
def convert_with_if(value, type)
if (type == Integer)
value.to_i
elsif (type == String)
value.to_s
else
value
end
end
n = 4.4
p convert(n, Integer) #=> 4.4
p convert_with_if(n, Integer) #=> 4
case calls ===, the case equality operator. Module#===, and by extension Class#===, actually tests if the given argument's class is the receiver or is one of its descendants. String === object is practically equivalent to object.kind_of? String.
convert 1, String
Would be equivalent to:
case String
when Integer
1.to_i
when String
1.to_s
end
That's like asking is the String class an Integer?, or is the String class a String?. The answer to both questions is No, it is a Class..
In terms of code, String.class returns Class, which is not related in any way to Integer or even String itself. If you introduced a when Class or when Module clause, it would be executed every time.
In your convert function you are getting the type Integer which is Class. That's why it's different from what you expect.
The convert method is not matching anything and defaulting to the else which is why you get 4.4.
Also, if you expect the method to output 4.4 then you would need to use decimal and not integer.

Resources