Side Effects – true story bro

  • by
woman sitting painting

This post is inspired by true pull request 😎

A long time ago during one of many code reviews. One developer in the body of a function that returned description of a path, just for one case, decided to open a browser. To make it less abstract let’s say it was something like this:

func fakeFunction(_ input: Input) -> [Step] {
    switch input {
        case ...: return [.step1, .step2]

        case someInputCase(let assosiatedUrl):
            UIApplication.shared.open(assosiatedUrl) // <-- side effect!

            return [.step1, .step2]


        case ...: return [.step1, .step2]
    }
}

If you want to be picky then you can say that this code has a bit of a smell. For one now this function has more than one responsibility. For two this URL was opened only for this case. So it breaks any knowledge about this method by other developers.

Ok se me being me I had to leave a comment (more or less like this one):

This is a side effect. You should not do this in this method.

Only response I got was:

No it’s not.

I will not comment the comment to my comment. But this raises a very good question.

What is a side effect in programming?

So everything that changes any outside world is a side effect. fakeFunction has its own internal world that is made by any local variables and constants and any arguments it received. When this function opens another application then one could clearly say that the world did change. Something as simple as printing to the console is also a side effect.

If you are not convinced that those are side effects then I have another one. This time it may help you to clarify when you are dealing with this kind of function and behavior.

Pure functions, those that do not have any side effects, for the same input will yield the same output. This is a very nice property to have when writing any software. You can substitute a call to this function with the result value.

And that’s the clue. If I would replace the call to fakeFunction with the returned value [.step1, .step2] then the behaviour of the application would change! User no longer would see an opened page!

Why this is important?

I’m betting that you like when your code works the same no matter if it’s a normal day at work or Friday at 5 p.m. Now you can think only about this piece of code without the burden of taking the whole world as the context this code operates in. It’s way easier that way.

The second thing is testability. You get this basically for free. Just throw any input values to the function. Assert on the calculated output and it’s done! You can even think about snapshot testing in some cases.

Pure functions compose very nicely. When you have side effects from one function might affect how the next one operates. And if you are lucky, what if this function is in a different file, class, module… hunting those kinds of bugs is very hard.

Who needs side effects?

So if they are so bad than why not just get rid of them in total?

It’s not like that. Side effects are the reason any software gets run. Think about it. You want to have some output on the screen, a new record in a database or that networking call made. The tricky part is to move all of this mess as far as possible from the core logic.

How to spot side effect functions in Swift?

This time we are in luck. If any function is marked as returning Void then it must (should?) perform some side effect. Think about it if you are not running it for its returned value then you must run in for its side effects. It’s not good or bad but it’s worth remembering about it.

What I do on those codebases is that I introduce a simple type alias:

typealias SideEffect = Void

Now whenever I would have a function returning void I’m writing func someFunction() -> SideEffect as a reminder about something magical 🧙‍♂️ that is going on 😎

Leave a Reply

Your email address will not be published. Required fields are marked *