Using Gremlins (1984) to Understand Non-Mutating vs Mutating Methods in Ruby
With Halloween just a couple of days away, what better time to use a horror (okay, comedy horror) film to help us understand a programming concept? While studying for my upcoming Launch School RB109 assessment, the idea popped into my head to use the movie Gremlins as an analogy for non-mutating and mutating methods.
If you haven’t seen Gremlins, here is the information you need to know before continuing:
Mogwai are cute little creatures; in the movie, the main character Billy (a human) is given one as a gift. His name is Gizmo and he is sweet and adorable!
There are three rules Billy must follow to care for Gizmo:
1. Do not expose him to light (it will kill him!)
2. Do not let him come into contact with water.
3. Do not feed him after midnight.
Well, Gizmo has water spilled on him accidentally, which causes multiple mogwai to spawn from his back. They are cute, like Gizmo, but devious.
The ring leader of these new mogwai tricks Billy into feeding them after midnight. Soon after enjoying their cold chicken, the mogwai form cocoons, out of which they hatch and enter the world again as gremlins- horrible, ugly creatures who wreak havoc on the town!
Okay, that is all you need to know to follow my analogy. Let’s continue.
Non-Mutating Methods
Non-mutating methods in Ruby do not perform any modifications on the object the method is called on. Instead, invoking a non-mutating method on an object creates a new object, which is a copy of the original, and then modifies that object. The original object is unmodified and remains the same as it was before the method call.
In my mental model of non-mutating methods, I see the method invocation as a two step process:
1. A new object is created, which is a copy of the original caller.
2. Some modification of the new object is performed by the method, resulting in an object that looks different than the original.
This two-step process is just like how a gremlin is created!
1. A mogwai comes into contact with water and spawns a new mogwai, a copy of the original.
2. The new mogwai eats after midnight and mutates into a reptilian creature, much different from the cute mogwai it spawned from!
Here is a visual representation of the mogwai — gremlin process:
First, there is the original mogwai, Gizmo.
The first step to create a new gremlin while the original mogwai remains the same is to have the original mogwai touch water. This creates a new mogwai, a copy of the original.
The second step of the process is for the new creature, the copy, to eat after midnight. Cold chicken, anyone?
Only the new creature ate after midnight, so it was the only one to mutate. Look at Gizmo! Still as cute as ever.
What would this look like in code?
gizmo = 'mogwai'
new_creature = gizmo + ' gremlin' # non-mutating methodgizmo # => 'mogwai'
new_creature # => 'mogwai gremlin'
First, local variable gizmo
is initialized to a String object with the value 'mogwai’
.
Then, local variable new_creature
is initialized to the return value of calling String#+
on the object referenced by gizmo
with an argument of ' gremlin’
.
Here is what this method call looks like in the two step mental model:
Start with the original object:
First step of the method invocation is to create a copy of the original:
Second step is to modify the copy:
The second step of the process acts only on the new object, so there is no modification to the original!
Mutating Methods
Now let’s see what using mutating methods looks like. If we continue with our Gremlins analogy, calling a mutating method would be equivalent to Gizmo, the original mogwai, eating after midnight and becoming a gremlin himself. (Of course, he would never! He is too smart. But hypothetically speaking.)
Our mental model for understanding mutating methods should be that when calling a mutating method, there is only one step in the process. Unlike non-mutating methods which create a copy of the caller before performing modification on the copy, mutating methods perform modification directly on the caller.
This example will look similar to the one used to describe non-mutating methods. Remember, we ended up with two String objects after calling String#+
on the object referenced by gizmo
. How many objects will we have after calling String#<<
on the object referenced by gizmo
?
gizmo = 'mogwai'
gizmo << ' gremlin'gizmo # => 'mogwai gremlin'
String#<<
is a mutating method and appends the string passed to it as an argument, ' gremlin’
, to the calling object, the object referenced by gizmo
. This permanently modifies the object referenced by gizmo
, which now has a value of 'mogwai gremlin’
. We started with one object and ended with one object.
Before method invocation:
After method invocation:
Arrays and Hashes
When called on an array, a non-mutating method will create a new array and then on that new array perform some sort of operation depending on the method, leaving the original array unmodified. Again, it is a two step process.
A mutating method called on an array will perform its modification directly to the array, permanently changing it. There is only one step; no new object is created.
The same mental model applies to calling methods on hashes. Two steps for non-mutating, one step for mutating.
Let’s call the non-mutating method Array#map
on an array of strings.
array = ['gizmo', 'stripe']new_array = array.map { |name| name + ' mogwai' }p array # => ['gizmo', 'stripe']
p new_array # => ['gizmo mogwai', 'stripe mogwai']
First array
is initialized to an Array object with a value of [’gizmo’, 'stripe’]
.
Then new_array
is initialized to the return value of calling Array#map
on the object referenced by array
with a block passed to the method as an argument.
First, Array#map
creates a new empty array, where it will place some object (determined by the return value of the block) on each iteration. It will return this new array once the method has finished iterating through the original array.
Next, Array#map
iterates through each element in array
, passing each one to the block where the block parameter name
is assigned to the element. In the block, String#+
is called on the element referenced by name
with an argument of ' mogwai’
. This returns a new string object. Since this is the last line evaluated by the block, this return value is also the return value of the block. Array#map
places this return value into the new array.
We now have two Array objects, with array
remaining unmodified after the method invocation. Modifications were only performed on the new array.
Let’s do the same thing but with Array#map!
, a mutating method. Remember, this method invocation will skip the copy step and will act directly on the calling object.
array = ['gizmo', 'stripe']array.map! { |name| name + ' mogwai' }p array # => ['gizmo mogwai', 'stripe mogwai']
I won’t describe everything that’s happening here because it would just be copying and pasting most of the description for Array#map
. The only difference is that no new array was created; the return value of the block replaces the current element that was passed to the block. array
has been permanently modified.
Conclusion
I hope my Gremlins analogy will help you remember what happens when we call non-mutating and mutating methods. Non-mutating method calls are two step processes: spill the water first to create a copy, then have the copy eat after midnight so that it is the only thing that mutates. Mutating method calls are only one step: the original eats after midnight and mutates himself.
One last look at the super cute Gizmo, doing a little dance. Adorable.