Minimizing Mutable State and Reducing Side Effects

Although Ruby is object-oriented, and therefore relies heavily on mutable state, we can write nondestructive code in Ruby. In fact, many of our Enumerable methods are inspired by this.

For a trivial example, we can consider the use case for Enumerable#map. We could write our own naive map implementation rather easily:

def naive_map(array)
  array.each_with_object([]) { |e, arr| arr << yield(e) }
end

When we run this code, it has the same results as Enumerable#map, as shown here:

>> a = [1,2,3,4]
=> [1, 2, 3, 4]
>> naive_map(a) { |x| x + 1 }
=> [2, 3, 4, 5]
>> a
=> [1, 2, 3, 4]

As you can see, a new array is produced, rather than modifying the original array. In practice, this is how we tend to write side-effect-free code in Ruby. We traverse our original data source, and then build up the results of a state transformation in a new object. In this way, we don’t modify the original object. Because naive_map() doesn’t make changes to anything outside of the function, we can say that this code is side-effect-free.

However, this code still uses mutable state to build up its return value. To truly make the code stateless, we’d need to build a new array every time we append a value to an array. Notice the difference between these two ways of adding a new element to the end of an array:

>> a => [1, 2, 3, 4] >> a = [1,2,3] => [1, 2, 3] >> b = a << 1 => [1, 2, 3, 1] >> a => [1, 2, 3, 1] >> c = a + [2] => [1, 2, 3, 1, 2] >> b => [1, 2, 3, 1] >> a => ...

Get Ruby Best Practices now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.