Encapsulation is a technique where we restrict access to some local state to only be accessible from within its local scope, effectively protecting it from any access from outside that scope.
So, what does this have to do with side effects? It affects one particular special case of side effects: mutable state.
Consider one of our first examples, a sum of all the numbers in an array:
let sum = function(array){
let result = 0;
for(let i = 0; i < array.length; i++){
result += array[i];
}
}
sum([1, 2, 3])
//=> 6
Function sum
internally uses mutable state. It has two variables result
and i
whose values change during the
program execution. By all means, it is a typical imperative piece of code.
However, we can’t really tell that when we call that function:
- we give it an array
- it returns the value
- the return value depends only on that array and nothing else
- it does not modify the array
- it causes no other visible side effects
By all definitions, when observed from the outside, the function is pure. That’s because all its side-effecting
variable mutating code is well encapsulated. You simply can’t access result
or i
from the outside.
As a counter-example, we’ll now show a contrived version of the same example that uses global mutable state:
let numbers = [1, 4, 5, 6, 7]
let result = 0;
let updateSum = function(){
for(let i = 0; i < numbers.length; i++){
result += numbers[i];
}
}
We still have variable mutation, but now one of the variables being mutated, result
, is no longer encapsulated.
The function updateSum
is clearly not pure and any code from the outer scope can mess with the result leading to
potentially weird situations.
We already used encapsulation to make the mutable state local in the chapter on Memoization:
let memoize = function(f){
let lookup = {};
return function(a){
let existingResult = lookup[a];
if(existingResult){
return existingResult;
}
let newResult = f(a);
lookup[a] = newResult;
return newResult;
}
}
We wanted our memoized functions to have access to a mutable cache in the form of a variable called lookup
,
however we wanted to protect that cache from any outside interference, so we encapsulated it in a closure.
With memoize
, we managed to make sure that the produced functions are theoretically pure, despite them using
mutable state, simply because the state is well encapsulated.
The takeaway here is that, whenever mutable state is unavoidable, local mutable state is better (or at least less bad) than global mutable state.