Yes I guess it seems like a paradox at first, but it makes sense when you dive into the details of what is mutable and what is immutable.
Your application logic and data is immutable all throughout, as much as it can be, and your boundary is mutable. Your programming environment is mutable as well.
That’s the context like you said which matters here.
A function reference being mutable allows you to hot-swap all references to it from one function to now use another inside of a running program without needing to restart or lose your runtime state. That’s really great when you want to iterate interactively at the REPL on the implementation.
Being able to add new functions into an existing module (aka namespace in Clojure) that is already compiled and loaded at runtime is also great when you want to interactively and iteratively expand a program capabilities as it is running.
The ability to load new modules at runtime is generally called dynamic linking, as opposed to static linking. Dynamism is the property of a system to change over time.
The concept of a dynamic programming language was born out of this idea of a program that can change itself as it is running. Lisp was the first attempt (that I know of) at creating a dynamic programming language.
While we tend to think of a dynamic language as referring to dynamic types vs static types, a real dynamic language is really one with the ability to change itself as it is running. What that means is if you have such a language, you can extend a program as it is running, you can start the creation of any program by first starting an empty program, like a blank page, you run this empty program and as it is running you add more functionality to it so it changes and becomes something more, maybe a text editor, a backend service that processes crypto trades, a command line for searching through files, etc.
Even the REPL is not truly what matters, what matters is that Clojure, like prior Lisps, is trying to be a dynamic programming language, that means one that can change itself as it is running.
Even the types being dynamic in nature is a consequence of this, how can you type check a program that can change as it runs?
The REPL is just the user interface to the dynamism of your running program. It is what you use to make changes to it as it is running, it allows you to sculpt.
Of course, with great power comes great responsibility. Changing the very design and nature of a program as it runs is great when you’re developing your program, it lets you iterate ideas quickly, see immediately the effects of your change, it lets you explore and poke around, etc., giving you that live interactive development that is so fun, like how a band jam session is more fun than a serious composition of music on sheet paper. But it is very dangerous once the program is to be used by your users, once people rely on its behavior for production use cases they depend on, they want a stable program, not one that changes as they use it. So Lispers know not to make changes to it when it is being used by users, unless absolutely necessary.
But this is about changing the program code and structure at runtime, and its benefit is when developing a program for the developer.
When it comes to modeling the business data and business logic, if you have various pieces of logic all changing the same data, it gets very easy to get the time and place wrong, and now you have bugs. So making those immutable and pure brings better correctness.
Now also at development, when you change the program, add/remove/modify the variables and functions inside namespaces and the namespaces themselves, how do you mentally keep track of the shared data between them that they’d be using if they were impure and mutable? As you change the program they might each leave corrupted state behind that your new functions don’t expect as they possibly would not create that same state. So even there it helps a lot that the application data and logic remains pure and immutable.
So in conclusion, this is where sometimes you’ll see people say that Java has a REPL too, that Python has a REPL too, that Haskell has a REPL too. But they’re missing the point, it’s not because you have a command line interface to send a chunk of code and have it compiled and the result printed back that you also have a program which can change itself as it is running. What the “REPL” really is referring too is a fully dynamic program, one whose functionality can change as it runs. In theory you should be able to start a Clojure Tetris game and slowly modify the entire code and memory of it, all as it is running, so that you end up with a calculator.
I hope that helped, I got a bit carried away there, but I think it’ll be an interesting read.