Friday, May 14, 2021

Avoid shallow functions

Over the last couple of years I’ve developed an intuition that shallow functions can be problematic and here I’d like to share some thoughts on why that is the case.

Shallow functions

A shallow function is a function with a tiny body and little to almost no logic. The opposite would be a deep function, a function with complex logic and possibly large body. Your first reaction might be that small functions are good. They are easy to understand and depending on the language you use, they may even perform well - in languages like Java they work well with the JIT, because there’s a good chance they can be inlined.

So what’s not to like about them?

One of the problems with shallow functions is that they increase the API surface. If you think about what causes a system to be complex, one property is the number of parts. The fewer parts a system has, the less complex it is. Think of a puzzle.

Another problem is the indirections they create. If you read the definition of a function a and encounter a call to function b, you may infer what it does based on the functions name and may be able to skip reading its definition, and that would be good - if the function introduces some specific semantics and creates a precise understanding. But with shallow functions that is often not the case. Instead it causes some indirection and the reader is inclined to jump to the definition to see what it really does. This is especially the case if the reader is familiar with the existing APIs of a system and wonders why c wasn’t used instead - the reader gets curious and needs to know what b does additionally or differently. Often it turns out it doesn’t do anything more, other than calling c with a fixed argument. Worse is if it does something different, and if that is not obvious from the name because it is too similar to other existing functions.

I also like to refer to these functions as “convenience functions”: Functions that don’t really contain logic, but instead make it easier to call some other functions. This may be okay if they’re only used internally, but if they become part of the public API you’re signing up for trouble: It becomes hard to change or remove them once users start to use them, and they make the API more complex due to the larger API surface.

I fear this tendency to create such tiny convenience functions is coming from a focus on making it easier to type code, instead of focusing on the readability, and also by taking the “Do not repeat yourself” (DRY) advice too literally. DRY was coined by Andy Hunt and Dave Thomas in their book The Pragmatic Programmer and Dave Thomas had to say the following in a later interview about it:

We’ve also looked at the reaction to various parts of the book, and discovered that we weren’t really communicating as well as we thought we were some of the ideas that we had. A classic one is DRY. DRY has come to mean “Don’t cut and paste”, but the original “Don’t repeat yourself” was nothing to do with code, it was to do with knowledge. So we’ve had to go through and update that…


This doesn’t mean that all small functions are bad, or that everything that adds convenience is bad. I too do like small functions if they’re doing something unique.

Sometimes improving the ergonomics of a construct is necessary to make it viable. The do notion in Haskell is such an example. It is only syntax sugar, but writing the same logic without it would often be too painful to make it viable.

The gist of the post is that you should think twice about adding tiny functions if they only add a little bit of convenience. A function should carry its own weight and contain enough logic to justify the indirection and increase in API surface.

All problems in computer science can be solved by another level of indirection … except for the problem of too many layers of indirection

I also recommend to read A Philosophy of Software Design by John Ousterhout as a follow up, because it wasn’t until I read it that I started to understand my own intuition better.