- Intro
- Explain mutation vs re-assignment
- Discuss Gotchas
- Mutating a value in a list
- Array constructor
- Hash constructor
- Mutating constants
- Freezing only works a single level
- Ruby 2.3 frozen string pragma
- Ruby 3 default frozen string literals
- Other personal experiences
- Best practices
- AVOID mutating arguments
- PREFER freezing constants
- PREFER
Enumerable
methods to raw manipulation - PREFER non-bang Ruby methods
- AVOID the shovel operator
- PREFER returning over mutating
- PREFER the block format for default array/hash values
- Discuss our experience avoiding mutation
- Discuss FP's lack of mutation
- Discuss benefits of avoiding mutation in Ruby
Mutating references in a list
[1] pry(main)> list = ["foo", "bar", "baz"]
# => ["foo", "bar", "baz"]
[2] pry(main)> bar = list[1]
# => "bar"
[3] pry(main)> bar.clear
# => ""
[4] pry(main)> list
# => ["foo", "", "baz"]
Copying around the same reference.
You want to default all the values in the array to "foo" but then want to make the last one empty.
[1] pry(main)> list = Array.new(5, "foo")
# => ["foo", "foo", "foo", "foo", "foo"]
[2] pry(main)> list[4].clear
# => ""
[3] pry(main)> list
# => ["", "", "", "", ""]
You want to default all keys to an empty array and then append to it.
[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]}
Mutating 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
# => {}
Freezing only works one level. Children can always be mutated unless they are frozen too.
[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"]
Re-assigning is different than mutating. Both lead to a sad place.
AVOID mutating collaborators or arguments
def process_image(config)
width = configr[:width]
height = config[:height]
puts "processing image #{width}x#{height}"
end
PREFER Enumerable
methods to manipulating arrays/hashes directly
# 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 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]}
huh, I did not see this coming. I'm surprised that
another_list[1].clear
clears all the items in the array while the first example does not.