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.