In general I think any pure function has the potential to be understandable.
You just have to load the structure of the incoming data into your brain and understand what’s happening in the steps. So this is the main point for me, make it possible for the reader to follow.
Imagine someone telling a story in plain english. If you lose track of the (grammatical) subject, you can’t understand the story. So in a (pure) function, the subject is some data coming in, transformed with the help of some other data structures.
There are two basic techniques already mentioned to make this easier:
While a defp adds some mental load, it also has the potential to reduce it. It is almost always worth it, when there is a clear transformation of data in the block refactored to a defp that can be neatly described by the function’s name.
If I don’t want to defp I somethimes just add a comment with an example of the current shape of the data.
Sometimes there is so much going on that a new module is of need. Then definitely add a @spec which greatly helps with understanding whats going in and out.
Reducing the possible structures of data flowing through your functions by using structs (and in the first step, understanding whats there) immensly reduces the mental load of reading a function.
The rest I think is “just” naming things (hard) and using the tools correctly.






















