Side effects require a firm grasp of a bigger picture than just the function causing them for programmers to do things predictably and correctly.
From my standpoint, it's a balancing act if mutations/side effects are involved. If you spread the logic of mutating your central application state across many teeny functions, then the functions themselves become easier to comprehend individually but can make it more difficult to reason about what's happening to the application state combined together as a whole.
Bigger pictures are difficult to grasp when fragmented into small pieces.
It's like fragmenting a picture into many small puzzle pieces you have to put together to figure out what's going on when the relevant picture you need to grasp to do things correctly would have been easier to grasp if it wasn't fragmented into many teeny pieces. In such cases, the small picture becomes easier to grasp but the big picture can become harder to grasp.
And as long as your functions aren't pure and invoke side effects (this includes methods that change an object's internal state), then you generally have to be able to comprehend many functions taken together as a whole (a bigger picture) in order to effectively reason about your code from a bird's eye view (and especially so if your codebase is multithreaded). If you fail to understand the bigger picture, you risk finding yourself in integration hell where everything seems correct taken individually and your unit tests pass but they malfunction in confusing ways when combined together.
... but pure functions don't require such a grasp of a bigger picture.
If, however, you write lots of teeny pure functions that cause no side effects, then I think it no longer becomes so important to comprehend what they're doing when combined/interacted together to reason about the bigger picture (most blatant example but not limited to this: to reason about the thread-safety of calling your functions from different threads).
A temporal coupling example:
Take a basic example of temporal coupling with an object that requires calling method A
before method B
can safely be invoked. That order dependency that allows them to fail when called in the wrong order -- even if they're correctly implemented individually -- only exists because it was diced into two separate smaller functions that involve side effects (mutations to state shared between the two).
If we eliminate the mutations to state shared between the two, then there's no order dependency between the two. If we keep the mutations, then the way to eliminate the temporal coupling here is to actually combine A
and B
together into a larger AB
function. There is an argument to be made for fewer and larger functions when side effects are involved, even if they compromise readability a bit, but I think the ideal approach is to try to limit the number of functions that have side effects in the first place.
Personal experience:
What I have found most productive over the years is to favor the minimum of functions that cause side effects in large-scale codebases. That may or may not imply larger functions, but definitely fewer of them. Then we can go happy designing as many small utility functions as we like provided they are pure and free of side effects. One-liner functions are fine and productive to me as long as they're pure functions. People's mileage will likely vary. But for my field and domain, it has been invaluable to reduce the number of functions causing side effects to the bare minimum (ex: to have objects with as few methods as possible that change their state, which makes it easier to reason about their ability to maintain invariants), and even if that sometimes comes at the cost of writing a function that spans a whopping 100 lines of code. But I try most of all just to avoid writing functions with side effects in the first place.
As a caveat, I work in a real-time field where it's vital to keep at least 30+ FPS (ideally 60+ FPS) for the user experience. I'm also among the most paranoid types on teams when it comes to testing and being able to reason about what we're doing from a bigger integrated picture than what our unit tests can usually suggest. We also do a whole lot of multithreading. Multithreading combined with some heavy demands for efficiency especially adds pressure to reduce the number of functions/methods with side effects in the system in our case. We would probably be in a constant state of panic and constantly tripping on race conditions if we didn't reduce the number of functions in our system that cause side effects to our application state to the bare minimum. I share similar thoughts and experiences with John Carmack here:
The real enemy addressed by inlining is unexpected dependency and mutation of state, which functional programming solves more directly and completely. However, if you are going to make a lot of state changes, having them all happen inline [commentary: he is talking about using bigger, fewer functions referring to manual inlining] does have advantages; you should be made constantly aware of the full horror of what you are doing. When it gets to be too much to take, figure out how to factor blocks out into pure functions (and don.t let them slide back into impurity!). [...] Currently I am leaning towards using heavyweight objects as the reasonable break point for combining code, and trying to reduce the use of medium sized helper objects, while making any very lightweight objects as purely functional as possible if they have to exist at all. [...] Besides awareness of the actual code being executed, inlining functions also has the benefit of not making it possible to call the function from other places. That sounds ridiculous, but there is a point to it. As a codebase grows over years of use, there will be lots of opportunities to take a shortcut and just call a function that does only the work you think needs to be done. There might be a FullUpdate() function that calls PartialUpdateA(), and PartialUpdateB(), but in some particular case you may realize (or think) that you only need to do PartialUpdateB(), and you are being efficient by avoiding the other work. Lots and lots of bugs stem from this. Most bugs are a result of the execution state not being exactly what you think it is. -- John Carmack