Understanding Programmatic Side-Effects

What are Side-Effects?

Side-effects are a concept I’ve been introduced to recently and when I examined my code it was surprising to see how much disruption they can cause.

To better understand a programmatic side-effect we should start with a text-book definition of what this means in computer science.

A function or expression that in addition to returning a value also modifies some state or has an observable with calling functions to the outside world.

If you read that a couple of times, it might make sense. Here is an example to make it clearer.

var count = 0;

function getResponse() {
    // Here we are creating a side-effect of calling getResponse()
    count += 1;
    
    return 'I have produced a side-effect';
}

var response = getResponse();

Our example shows a function that returns a string, but in addition to returning this value, it also modifies the state of the variable item.

Liberating Your Code From Side-Effects

Cleansing side-effects from your code is not complicated. Let’s take an example step by step.

// Global variables that are not needed
var cars = [],
    buses = [],
    automobiles = [];

function main() {
    // no return value in a getter function
    getCars();
    
    // no return value in a getter function
    getBuses();
    
    // what is this function actually doing?
    calculateAutomobiles(); 
}

// sets a global variable instead of returning data 
function getCars() {
    cars = [{
        color: 'red',
        wheels: 4
    }];
}

// sets a global variable instead of returning data 
function getBuses() {
    buses = [{
        color: 'blue',
        wheels: 8
    }];
}

// sets a global variable instead of returning data 
// internal assumption of logging that circumvents implementation choice
function calculateAutomobiles() {
    automobiles = cars.concat(buses);
    console.log(automobiles);
}

main();

We have a main method that retrieves cars, buses, and then calculates the automobiles. Although you can read the main method and understand what’s happening, the function names are misleading. They imply false assumptions.

  1. Why does a function named getCars not have a return value? How about getBuses?
  2. Would you assume a method called calculateAutomobiles would set a global variable?
  3. Why does calculateAutomobiles handle the program’s output via console.log?
  4. Why are there so many global variables?

Let us start by solving #1. Why do our getter functions not return a value? I have no idea! A getter function’s entire purpose is to return a value. Time to fix that up.

var cars = [],
    buses = [],
    automobiles = [];

function main() {
    // set variable with new return type
    cars = getCars(); 
    
    // set variable with new return type calculateAutomobiles();
    buses = getBuses(); 
}

// return array rather than setting variable
function getCars() { 
    return [{
        color: 'red',
        wheels: 4
    }];
}

// return array rather than setting variable
function getBuses() { 
    return [{
        color: 'blue',
        wheels: 8
    }];
}

function calculateAutomobiles() {
    // ... 
}

main();

Now our getter functions are returning a value which is handled by the function caller’s implementation. That is a good start. Time for #2. We removed a couple of global variable references just now, and it’s time to do it again for the calculateAutomobiles function. At the same time, we can decouple the console.log and fix #3.

var cars = [],
    buses = [],
    automobiles = [];

function main() {
    cars = getCars();
    buses = getBuses();
    
    // set variable here with new
    automobiles = calculateAutomobiles();
    
    // decouple console.log from the calculateAutomobiles method
    console.log(automobiles);
    
    return data
}

function getCars() {
    // ...
}

function getBuses() {
    // ...
}

function calculateAutomobiles() {
    // return data instead of setting global variable 
    // remove console log from functional requirements
    return cars.concat(buses);
}

main();

The last step, #4, is to evaluate our code and assess whether or not we need all these global variables.

Hint: We don’t.

As it stands now, our code could not be modularized or unit tested because calculateAutomobiles has a reference to global variables within the same package. Not only would you have to read the function’s internal code to see its required parameters, but it’s dependent on parameters that are accessible by anything else within the package. Now we can replace the global variable with scoped versions and pass these into calculateAutomobiles.

function main() {
    // replace global variables with scoped versions 
    var cars = getCars(),
        buses = getBuses(),
        
        // pass in function parameters instead of global variables 
        automobiles = calculateAutomobiles(cars, buses);
        
    console.log(automobiles);
}

function getCars() {
    // ... 
}

function getBuses() {
    // ...
}

function calculateAutomobiles(cars, buses) {
    return cars.concat(buses);
}

main();

By removing the global variables and instead passing them as functional parameters, we can now unit test calculateAutomobiles without unseen dependencies.

Moving Forward

In my work so far I find that removing side-effects increases the testability and readability of my code. These are massive gains for only the small cost of learning what side-effects are.

Add side-effect prevention to your refactoring and peer review process. I’m confident you’ll see the benefits as well.