Avoid shallow functions

  Friday, May 14, 2021

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

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’re easy to understand and depending on the language you use, they may even perform well. For example in languages like Java they work well with the JIT, as there’s a good chance they get inlined.

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 its parts. The fewer parts a system has, the less complex. Think of a puzzle. One with a hundred pieces is easier to solve than one with thousands.

Another problem is the indirections they can create. If you read the definition of a function a and see a call to function b, are you able to infer what b does and skip reading its definition, or do you need to jump to it and read it as well? The former would be good if the function communicates semantics and creates a new precise understanding. The latter can be a sign of a bad abstraction, causing indirection and reducing readability.

Shallow functions which are similar to existing APIs are especially bad. You as reader wonder why c wasn’t used instead. You get 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 without being obvious because it’s too similar to existing functions.

I 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. They might be acceptable if 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 having less code to type, instead of focusing on the readability. Or people are 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…

Conclusion

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’s 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 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. If you don’t like books, you could watch one of his talks instead.