Video

Want to see the full-length video right now for free?

Notes

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.

Mutation vs Reassignment

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.

Containers vs references

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.

Common ways to mutate in Ruby

Strings

string = "abc"
string << "def" # appending
string.clear # setting to empty string

Arrays

array = ["a", "b", "c"]
array << "d" # appending
array[1] = "g"

Default Args

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]}

Constants

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

Guidelines

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]}