I’m going to disagree a little with the others. In my experience it can get messy to pass down maps, but the answer I think is slightly more nuanced than what you’ve described.
For me, it’s more about simplicity. Let’s remember what Rich Hickey considers simple, something is simple when it is untangled. That means it is an independent unit, where changes to other parts of the system do not affect it or break it and where if it changes, other parts of the system are also not affected or broken. It means you can move the unit from one place to another without needing to drag the whole system along. It means you can have multiple things depend and reuse the unit for different purpose without each user of the unit needing to be aware of all other users and their intricacies.
In that sense, you already hinted at some of the issues with passing a map down, all functions above it must make sure that they manipulate the map in ways that won’t break the functions below. If a function above is responsible for providing the correct key/values for a function below, than the chain of execution is coupled so that you cannot remove the above function without breaking the below functions.
You can get into messy scenarios where fn
A adds key
:a and fn
B adds key
:b and finally fn
:b on the map where
C. The whole chain is complected in this case by the map weaving its way along it.
So, for me, we have to go back and discuss the “unit”. If the map I’m passing down is a part of my application’s domain model, like say a user entity, or a database entity. And if the functions I’m passing this map down to are all logically operating on that entity, than it is fine to pass the entity map down to them as is. But this map being an entity means that I’ll have a strong model for it, probably a spec with the invariants for it at various point in time. When you do it that way, each function down is only coupled to the entity itself, not really the functions above, because the shape and invariants for the entity are not implicitly defined by the chain of functions above, they are defined in the application domain as a Spec for example. In that case, I have made the choice that those functions are not independent of my application domain model, but very much coupled with it, so the “unit” would be the entities + all the functions that operate over them.
If I’m not passing such domain entity down, then it depends on if you expect the below functions to simply serve as a way to factor the above function into something more readable and testable? Or do you expect the below functions to be reusable by other things and shared across many callers?
If the former, than it is fine to pass the map down, since the “unit” in this case is the full set of functions involved in the chain, the below functions are simply factored out chunks for the above function. On the other hand, for the latter, I would say it is not good to pass the map down, because you want each function to be its own independent unit.
Now I think there is a common use case where you might start with a domain entity, and as you execute your business logic, you generate new data or you acquire it from a database or another service, and your logic after needs this new data as well as the original data from the entity. It is tempting here to keep adding along this new data to the entity map, but I think this is often what eventually leads to messy code where you can’t understand as well what is where. My recommendation for this, and for a lot of the issues with passing down a map is simply to change how you code things so that your chains are as shallow as can be. Instead of having
A add new data to the map it received as input and passing that to
B, have an
A' that takes the map and returns the new data, then have
A be a function that calls
A' and then calls
B. If you do this enough, you end up with instead of having a chain of
A->B->C->D->... you get a shallower topology of
A->A', A->B, A->C, A->D, A->...