Are there enough posts about unnecessary side effects? Unlikely - you still see them everywhere. Here is a sales pitch for this post:
Some posts describe side effects in the context of functional programming (and JavaScript): one, two, three and a wiki. You don't have to study functional programming to grasp this concept. Unfortunately, unnecessary side effects are prevalent in all coding paradigms, especially OOP.
Let’s take a simple function that computes the absolute value of a number:
def abs(x):
if x < 0:
return -x
return x
What comes out from the return
statement is called a “primary effect”. Now let’s take a look at another function:
def abs_effectful(x):
if x < 0:
return -x
twitter_api = TwitterAPI(some_twitter_id())
chat_bot = LanguageModel()
tweet_content = twitter_api.read_first_tweet()
response = chat_bot.make_response(tweet_content,
f"{x} is important because ")
twitter_api.respond(response)
twitter_api.close()
return x
Here we also have the same primary effect returned by the abs_effectful function. But a section in the middle performs pretty surprising actions and influences the external world: it has side effects.
You may ask: "Why do you have a weird unrelated piece of code in your function??" This contrived example is not so far from what happens in large codebases. It’s just a set of refactorings, misunderstandings, and a stream of people joining and leaving teams. Time passes, and simple code accumulates unnecessary baggage.
Here are some real examples of side effects that I've encountered in my Python ML career:
publishing a number to the Weights-and-Biases server out of the loss function
dumping debug images into a home folder from a computer vision algorithm
changing an environmental variable inside a function
dropping to pdb
on if
condition
changing a global variable
calling sys.exit
inside an algorithm
sending a Slack message if nan
is encountered during training
... and so on.
After seeing many of those, you get a Spider-Sense for spotting side effects. If you get surprised by what function does, quite often the function has a side effect. In fact, the code with unnecessary side effects is not as simple as it could be. See a great post from Eric Elliot about simplicity, surprises and side effects.
Side effects are unavoidable because software must have some influence on the world. But are unnecessary side effects harmful? If there are no visible differences, is there a reason to prefer one script over another?
How can abs
crash? It pretty much can’t. What about abs_effectful
? It might crash in almost every line:
Every side-effect code line might crash since it deals with things you don't control. Now, from the real examples above:
So the code with side effects is more fragile.
Every side effect might bring computational costs. But most importantly, it brings timing variability into your software. Because side-effect code typically deals with uncontrolled external resources, you get uncontrolled slowdowns. Every side-effect code line in abs_effectful
can bring you a massive slowdown:
In real-world code, similar things can happen:
The bottom line is that the code with side effects is slower.
You must be careful about any state you modify in concurrent (or even worse parallel) software to avoid data races and deadlocks. Code with side effects modifies the external state. It will often require a significant redesign to be used in a concurrent context. You need to ensure that only one function will use a global resource simultaneously and that the order of operations doesn't break the logic.
The timing variability mentioned above makes it even worse. It's a big topic, so here is a good post discussing concurrency and shared state access in depth.
Optimizing a function that doesn't modify anything externally is more straightforward. You can reorder operations inside it and cache the computation results. Since you're not constrained by an external call for a side effect, you can choose any tool for the job. Many side-effect-free functions can be easily rewritten in numpy
/torch
/jax
and Pybind
.
So on top of being slow, code with side effects is hard to speed up.
What if I ask you to make a docstring for abs_effectful
? You will definitely struggle to write a concise story about the purpose of that function.
A function with a single return statement like abs
achieves a single result. At a minimum, you can concisely write a docstring as
'''This function computes an absolute value.'''
If there are side effects, you have to add them all one by one:
'''This function computes an absolute value
AND opens Twitter API and creates a model
AND generates and sends a tweet
AND closes connections.
'''
In a large company, many people are joining and changing teams. New people have to understand obscure side effects. A company with less-readable code will waste days of cumulative onboarding time. A large docstring will not help: the longer the doc, the greater the chance it will be ignored completely.
The code with side effects interacts with stuff which is not under your direct control. After thinking about all imaginable failure modes of abs_effectful
, you might still find that it crashes after the release.
There is a crazy variability in the external world. It is tough to reproduce, debug and fix all edge cases of side effects. Often it happens stochastically, and you just can't reproduce a single crash in-house (e.g., maybe when abs_effectful
runs on an old Windows version, GPU kernels behave differently?).
The code with side effects takes much longer to release and debug.
Writing exhaustive unit tests for code with side effects is very hard. On top of the main result of abs_stateful
, you should better test that all side effects behave as expected: did the person get a tweet? Was the tweet parsed correctly? Not to say of some weird unit test interactions: side effects from one test could make other tests fail.
Interacting with the external world on a test server is often impossible. Developers have to resort to mocking all external entities. See more in this beautiful post from CodeWhisperer.
What if your colleague wants to compute the absolute value of a number? There is no problem with abs
. You just import the function and use it! What about abs_effectful
? They wouldn't know a priori about any side effects (ignoring the hint in the name :). The code can happily work until some unfortunate day when some crazy issues from above start coming up.
After finding the root cause, they'll anticipate all the problems we mentioned. Your colleague will just leave such a function alone and write their own simpler version.
A codebase that relies on side effects would typically have much less code reuse.
Hopefully, you’re convinced that side effects are also the root of all evil (just like premature optimization). Now here is a compilation of all the reasons above:
Code with side effects
For more discussion, please see the following great thread on StackExchange.
Meanwhile, thank you for reading!
Also published here.