Want to see the full-length video right now for free?
Mutation-related bugs are some of the most common in the Ruby world. Joël and German explore many of the gotchas related to mutation, how to avoid them, and discuss when mutation might not be necessary at all.
In mutation, the value of an object changes but its identity remains the same:
[1] pry(main)> variable = "abc"
=> "abc"
[2] pry(main)> old_variable_object_id = variable.object_id
=> 70334336923120
[3] pry(main)> variable << "def"
=> "abcdef"
[4] pry(main)> new_variable_object_id = variable.object_id
=> 70334336923120
[5] pry(main)> old_variable_object_id == new_variable_object_id
=> true
In reassignment, the variable points to a new object. The old object remains unchanged.
Here we re-assign the same value but have changed variable
's identity:
[1] pry(main)> variable = "abcdef"
=> "abcdef"
[2] pry(main)> old_variable_object_id = variable.object_id
=> 70352878923600
[3] pry(main)> variable = "abcdef"
=> "abcdef"
[4] pry(main)> new_variable_object_id = variable.object_id
=> 70352866744560
[5] pry(main)> old_variable_object_id == new_variable_object_id
=> false
Both mutation and reassignment can easily burn you.
Variables are often introduced as "containers" for data. In Ruby (and many other languages), variables are references to an object. This means there can be many variables that all point to the same object. Mutating the object means all variables now point to an object whose state may be different than expected.
Strings
string = "abc"
string << "def" # appending
string.clear # setting to empty string
Arrays
array = ["a", "b", "c"]
array << "d" # appending
array[1] = "g"
Arrays
[1] pry(main)> list = Array.new(5, "foo")
# => ["foo", "foo", "foo", "foo", "foo"]
[2] pry(main)> list[4].clear
# => ""
[3] pry(main)> list
# => ["", "", "", "", ""]
all the "foos"
are the same object so mutating it causes all the references to change.
Hashes
[1] pry(main)> master_list = Hash.new([])
# => {}
[2] pry(main)> master_list[:evens] << 2
# => [2]
[3] pry(main)> master_list[:odds] << 3
# => [2, 3]
[4] pry(main)> master_list
# => {}
[5] pry(main)> master_list[:evens] <<= 4
# => [2, 3, 4]
[6] pry(main)> master_list
# => {:evens=>[2, 3, 4]}
[7] pry(main)> master_list[:odds] <<= 5
# => [2, 3, 4, 5]
[8] pry(main)> master_list
# => {:evens=>[2, 3, 4, 5], :odds=>[2, 3, 4, 5]}
Given:
def process_image(config)
width = config.delete(:width)
height = config.delete(:height)
puts "processing image #{width}x#{height}"
end
[1] pry(main)> DEFAULT_CONFIG = { width: 640, height: 480 }
# => {:width=>640, :height=>480}
[2] pry(main)> process_image(DEFAULT_CONFIG)
# processing image 640x480
# => nil
[3] pry(main)> process_image(DEFAULT_CONFIG)
# processing image x
# => nil
[4] pry(main)> DEFAULT_CONFIG
# => {}
Ruby will happily allow mutating constants. It will only yell if you try to reassign them.
Using Object#freeze
will cause Ruby to blow up if anyone tries to mutate your object.
Use Object#dup
to pass copies of your object into methods you know are going to mutate.
References all the way down
dup
and freeze
only work for one level. Objects contained in your array or hash are still the same
accross copies and can still be mutated.
[1] pry(main)> CONSTANT = ["foo"]
# => ["foo"]
[2] pry(main)> CONSTANT << "bar"
# => ["foo", "bar"]
[3] pry(main)> CONSTANT.freeze
# => ["foo", "bar"]
[4] pry(main)> CONSTANT << "baz"
# RuntimeError: can't modify frozen Array
# from (pry):4:in `__pry__'
[5] pry(main)> CONSTANT[1] << "baz"
# => "barbaz"
[6] pry(main)> CONSTANT
# => ["foo", "barbaz"]
Ice Nine gem for deep freezing
AVOID mutating collaborators or arguments
def process_image(config)
width = configr[:width]
height = config[:height]
puts "processing image #{width}x#{height}"
end
PREFER freezing constants
PREFER Enumerable
methods to manipulating arrays/hashes directly
Anti-Pattern: Iteratively Building a Collection
# Good
evens = [1,2,3,4].select(&:even)
# Bad
evens = []
[1,2,3,4].each do |n|
evens << n if n.even?
end
evens
PREFER non-bang ruby methods
# good
numbers = [1,2,3,4]
evens = numbers.select(&:even)
# bad
numbers = [1,2,3,4]
numbers.select!(&:even)
numbers
AVOID the shovel operator <<
PREFER returning over mutating
# good
def add_exclamation(string)
string + "!"
end
# bad
def add_exclamation(string)
string << "!"
end
PREFER the block format for array/hash constructors
[1] pry(main)> master_list = Hash.new { [] }
# => {}
[2] pry(main)> master_list[:evens] <<= 2
# => [2]
[3] pry(main)> master_list[:odds] <<= 3
# => [3]
[4] pry(main)> master_list
# => {:evens=>[2], :odds=>[3]}