That means that your program should be made of parts that when modified do not break anything else. This, in turn, means you don't really ever need to refactor anything major.
In practice, this has held true for most of my code bases.
Now, my second answer, because sometimes there are some small refactors that may still be needed, or you might deal with a Clojure code base that wasn't properly decomplected, you would do it the same way you do in any dynamic language.
The two things that are trickier to refactor in Clojure are removing/renaming keys on maps/records, and changes to a function signature. For the latter, just going through the call-sites often suffice. The former doesn't have that great solutions for now. Unit tests and specs can help catch breakage quickly. Trying out things in the REPL can as well. I tend to perform a text search of the key to find everywhere it is used, and refactor those places. That's normally what worked best for me.
It helps a lot if you write your Clojure code in a way that limits how deep you pass maps around. Prefer functions which take their input as separate parameters. Prefer using destructuring without the `:as` directive. Most importantly, design your logic within itself, and so keep your entities top level.
> Prefer functions which take their input as separate parameters.
In practice, it's better to avoid positional arguments and extensively use maps and destructuring. Of course, there's a risk of not properly passing a key in the map, but in practice that doesn't happen too often. Besides - Spec, Orchestra, tests and linters help to mitigate that risk.
> In practice, it's better to avoid positional arguments and extensively use maps and destructuring
We can agree to disagree I guess. In my experience, especially in the context of refactoring, extensive use of maps as arguments causes quite a lot of problems. Linters also do nothing for that.
Positional arguments have the benefit of being compile errors if called with wrong arity. I actually consider extensive use of maps a Clojure anti-pattern personally. Especially if you go with the style of having all your functions take a map and return a map. Now, sometimes, this is a great pattern, but one needs to be careful not to abuse it. Certain use case and scenarios benefit from this pattern, especially when the usage will be a clear data-flow of transforms over the map. If done too much though, all over the app, for everything, and especially when a function takes a map and passes it down the stack, I think it becomes an anti-pattern.
If you look at Clojure's core APIs for example, you'll see maps as arguments are only used for options. Performance is another consideration for this.
Doesn't mean you should always go positional, if you have a function taking too many arguments, or easy to mix up args, you probably want to go with named parameters instead.
And inside it calls a bunch of auxiliary functions where you pass either `student` or `age` depending on what those functions do, then someone says: "oh we need to also add an address", and have address verification in the midst of that pipeline. And instinctively programmer would add another positional argument. And to all auxiliary functions that require it. The problem with the positional arguments - they often lie, they're value depends on their position, both in the caller and in the callee.
It also makes it difficult to carry the data through functions in between. The only benefit that positional arguments offer is the wrong arity errors (like you noted). And yes, passing maps can cause problems, but both Joker and Kondo can catch those early, and Eastwood does that as well, although it is painfully slow. With Orchestra and properly Spec'ed functions - the missing or wrong key would fail even before you save the file. I don't even remember the last time we had a production bug due to a missing key in a map args.
But of course it all depends on what you're trying to do. I personally use positional arguments, but I try not to add more that two.
That's a bit of a different scenario then I was thinking.
In your case, you're defining a ___domain entity, and a function which interacts on it.
Domain entities should definitely be modeled as maps, I agree there, and probably have an accompanying spec.
That said, still, I feel the function should make it clear what subset of the entity it actually needs to operate over. That can be a doc-string, though ideally I'd prefer it is either destructuring and not using the `:as` directive, or it is exposing a function spec with an input that specifies the exact keys it's using.
Also, I wouldn't want this function to pass down the entity further. Like if study needs keys a,b but it then calls pass-exam which also needs c and d. This gets confusing fast, and hard to refactor. Because now the scope of study grows ever larger, and you can't easily tell if it needs a student with key/value c and d to be present or not.
But still, I feel since it's called "study", it feels like a side-effecting function. And I don't like those operating over ___domain entities. So I personally would probably use positional args or named parameters and wouldn't actually take the entity as input. So if study needs a student-id and an age, I'd just have it take that as input.
For non side-effecting fns, I'd have them take the entity and return a modified entity.
That's just my personal preference. I like to limit entity coupling. So things that don't strictly transform an entity and nothing else I generally don't have them take the entity as input, but instead specify what values they need to do whatever else they are doing. This means when I modify the entity, I have very little code to refactor, since almost nothing depends on the shape and structure of the entity.
By writing smaller functions and avoiding positional arguments. Refactoring/restructuring in Clojure - most of the time is about breaking bigger functions into smaller ones. But if you try to write smaller functions to begin with - that significantly simplifies the task of maintaining that code.
The same way you refactor any other mid size project?
Refactoring 2nd ed by Martin Fowler mainly uses Javascript and Refactoring (Ruby edition) by Jay Fields are both dynamic.
Tests cover you when refactoring not types.
TDD? Testing in Clojure has such a nice feedback loop in Cursive, clj-kondo is a nice sanity checker, and most editors have rename functionality
Long term I can see a more intelligent feedback system powered by codeq helping with static analysis and refactor feedback, like unison but better (using a real DB and open for extension)