Side Effects in Programming
Published: 07/07/2020
A few years ago, I was showing code I'd written to a friend in order to get feedback and one of the things he pointed out to me was that a function I'd written had side effects. This was the first time I'd heard this term.
After the call with my friend, I googled around to try and understand what he had meant. I started with the Wikipedia page and followed that up with a few blog posts. Nothing that I read was helpful in explaining what side effects in programming were.
Last year while reading Will Kurt's Get Programming with Haskell is when I finally understood what side effects in programming are and why they were considered a code smell and create grounds for poor software design.
This edition of Neat Examples is dedicated to the example that Will shared in his book along with some added details that I wrote down to help explain the concept even better. Let's start!
Suppose you're reading through a code base and you come across the following lines of code.
tick() if (timeToReset) { reset() }
On seeing this code snippet, it is reasonable to draw the following conclusions:
-
Both
tick
andreset
are functions that don't take any arguments and possibly don't return any value -
It is not a long shot to suppose that
tick
is incrementing a counter and thatreset
restores that counter to its starting value -
Given (1) and (2),
tick
andreset
must be accessing a value in the environment, and since they don't return a value, they must be changing a value in the environment -
It's likely that both
tick
andreset
are accessing a global variable (a variable reachable from anywhere in the program)
This is a classic situation of a block of code with side effects. A programmer looking at this code cannot be sure how its execution alters the state of the program. This, in programming, is considered as poor design.
Side effects in programming make it hard to understand what a simple, well-written piece of code really does. Programmers usually call such chunks of code “hard to reason about”.
Side Effects City
Let's look at an example to understand how side effects in programming lead to a lot of pain for programmers. Consider the following code snippet:
myList = [1, 2, 3] myList.reverse() newList = myList.reverse()
There are three completely different answers for this code. Let's break down line by line why this happens.
In Python, the first call to reverse
updates (mutates) myList
ie. updates the program's state. The second call reverses the list again but does not return a value. This is why newList
is None
.
# Python myList = [1, 2, 3] myList.reverse() # myList updated to [3, 2, 1] newList = myList.reverse() # myList updated to [1, 2, 3] # newList is None since reverse() returns nothing
What do you expect the value of newList
to be? Take a minute to figure out your answer before reading ahead.
The program above is valid in Ruby, Python, and JavaScript, and so, it seems reasonable to assume that the value of newList
should be the same for the three languages. Right? Well, here are the answers:
Ruby -> [3, 2, 1] Python -> None JavaScript -> [1, 2, 3]
JavaScript does the same thing as Python but returns the value as well and that's why newList
is [1, 2, 3]
in this case.
// JavaScript myList = [1, 2, 3] myList.reverse() // myList updated to [3, 2, 1] newList = myList.reverse() // myList updated to [1, 2, 3] // newList is [1, 2, 3]
Clearly, when calling reverse
in Python and JavaScript, unintended things happen to the state of the program — side effects!
In Ruby, on the other hand, a call to reverse
creates a copy of the original list, reverses the copy and returns it.
# Ruby myList = [1, 2, 3] myList.reverse() # myList is still [1, 2, 3] newList = myList.reverse() # myList is still [1, 2, 3] # newList is [3, 2, 1]
This is exactly how reverse
should function. It should always return a reversed version of the list or collection you call it on, no matter what.
Back to Tick and Reset
When we called reset
and tick
earlier, the changes they made were invisible to the programmer. This is similar to our experience with Python and JavaScript.
Without looking at the source code (or documentation in the case of standard library functions like reverse
), a programmer would have no way of knowing exactly how the program's state is being updated by tick
and reset
.
This is exactly why side effects in programming should be avoided and instead, code that clearly explains what it is doing should be striven for by all programmers.
This post is part of a larger series of posts under the name 'Neat Examples'. The idea behind this series is to provide examples and analogies to help programmers understand programming concepts with the highest possible chance of retention.