1) That's up to the tests, just as in any language.
2) Some of the changes you describe are no less trivial to experiment with in an environment with a REPL. I routinely trial changes to small functions simply by running them in the REPL as discrete units before attempting to re-start an entire server project.
3) Good FP strategy rarely means dealing with anything with a global impact. About the only exception is client-side ClojureScript, where keeping a single "state" atom is largely idiomatic, and even then, actually using it is a last resort action reserved for things like routing and user session, preferring instead local state unique only to a single file for anything else that requires it.
I think this is a case where you need to experience just how big the difference is between an expressive FP approach and something like Java is to really understand why it's not a problem, and indeed, amounts to something like a category error.
When a Java programmer asks me about refactoring Clojure features, I can't help but think about stuff like how the entire messaging system for the app I worked on over the summer fits on maybe 3 or 4 printed pages of code and only took a week and a half to complete, compared to say, EnterpriseGradeFizzBuzz ... The tools you describe sometimes don't exist, it's true, but that's because no one needs them. They exist to automate tedium and complexity that is largely idiomatic to Java and other similar OOP languages, but not expected, desired, or necessary in other paradigms of programming.
I know it's hard to believe, but there are other ways of doing things. We do alright, I promise.
After I reread my own comment I realized it sounded much more harsh than I wanted. Good you were not put off by the tone. Sorry for that.
I have to admit that my biggest pet peeve with working in Java projects are those high impact changes. I try as much as I can to propose "extending" changes, but working in a team means someone certainly at some point will propose "changing" how something works, without ever realizing all the consequences.
If by using Clojure you get the same benefits of what the best OO practices can give, than I'm willing to give it a try.
I do remember a presentation about working on immutable snapshots of reality, it was something along the lines "How do you normally check if a runner had both feet above the ground?". The example there was, that a typical imperative programmer just checks the first foot and then the other. Naturally, the second check happens some time after the first one. The selling point was that, in Clojure, that you operate on a immutable snapshot of a runner in time, so the feet are by default in the same point in time.
So, I'm liberated from thinking about a whole category of potential errors. I liked it a lot.
There are some other categories of potential errors as well. I mentioned them in my comment and you cleared that up for me.
Still I have another doubt, regarding a typical code reuse scenario. A simple example follows.
Let's say a programmer needs to ask the user for input. He writes a code, which pops up a user interface dialog and wires up an action to the buttons callback. All is fine. As the time passes, there is a need to provide more questions to the user. The dialog code grows. Someone files a bug, that, in a multi-monitor setup, the dialog shows up always on the wrong screen. The bugs get fixed. More dialogs are introduced. Another project starts up and wants to use the nice UI code. We designate it for a separate library. The first project must isolate and make everything generic. The requirement is that both projects must work on top of that library and the first one cannot loose any functionality.
This is where the tooling excels most. You just make the duplicated UI code look the same and use the "extract method" refactoring. The IDE tells you "I just found 5 duplicates, should I replace them all?". You answer yes. The IDE even tells you if there are any potential side effects. If there are none, you proceed. Then you use "use interface instead of method call" refactoring. In that way you can nicely extract contract between the client code from the library code. You move the stuff around, so that the library code lands in other project.
Then, You can just wire the library as a dependency in the Project B. It happens, that some needed functionality is not exposed. So you change the library and immediately see the impact on the A Project.
The case here is: you do not have to understand the A Project as a whole, since the tooling takes care of so much of the static dependencies. You just pull out the fun stuff and patch out the wounds. It does not require a lot of skill or knowledge, so a new project member can also be designated with a such task - not only the knowledgeable rock-star programmers that have the whole project in their head.
If you can assure me that in Clojure you can do the same in a safe manner than I'm all in. :) I'd appreciate any articles or stories explaining such scenarios in detail.
In Clojure, one doesn't need to rely on tooling to do refactoring, because things are isolated, loosely-coupled to begin with. There isn't much "unintended side effects" when one change Clojure code, because Clojure code consists of a bunch of functions working on immutable data structures.
The feel of programming in Clojure is very very different from what you are describing in a language such as Java. I have programmed Java since 1997, ever since I switched to Clojure, all those pains and fears of changing code has evaporated. I am not afraid of changing things in Clojure at all, because I know the effects of my changes are local.
Of course, Clojure toolings do include some convenient features such as "extract function", "rename symbol", etc, but these are just some niceties that save some typing and editing. They are not indispensable like those in Java.
Ok, so there are a lot less unintended side effects. I got that.
What about the intended ones - how do You approach them? Let's say You've written a ClojureScript webapp using the Angular framework and you need to migrate it to V2. AFAIK there were some breaking/conceptual changes which force significant rework. How do You make sure that after changing it everything works - even the most obscure option that only one user uses (as in the notorious example of Search Keyboard Shortcut in Outlook by Bill Gates)?
> You've written a ClojureScript webapp using the Angular framework
I know this is an example but Angular is terrible with Clojurescript. Clojure has an opinionated idea of how state should change over time and that opinion does not include two-way bound attributes. The reason the cljs community is virtually all React is because dom diffing makes the UI effectively a projection from state and that is epochal-time compatible.
> How do You make sure that after changing it everything works - even the most obscure option that only one user uses?
Depends how your app is designed. In the general case where, say, you're doing the equivalent replacing Angular with Ember you just have to do it and lean on tests/QA. I have just recently finished such a refactor and it's about as fun as you're implying.
With the refactor, I have a better plan going forward! We're now on the re-frame model. All state is now in the global atom (instead of mostly being in the atom but some being component-local), dataflow is unidirectional, and we're capturing all non-determinant actions as event params. Together, it means serializing the state atom and a sequence of events allows playback on the changed code. At least in theory. I wound up having to ship rewrite delayed features and haven't taken the time to build recording/playback support.
One would normally use a name space to group together code that deals with a foreign library. So, upgrading to a new version of the library is just a matter of 1. update the dependencies by changing the version number of the library in project.clj; 2. load the library; 3. changing the name space (a file) that deals with the library, and 4. changing the corresponding test name space (a file), and make sure all tests pass. That's it.
The Clojure coding process is REPL driven, so basically one tests every changes. Also, one codes in a bottom up fashion, starting from simple and small functions, and composing them into big functionalities. There' no big design up-front. It's an exploratory process, and it's fun.
> Another project starts up and wants to use the nice UI code. We designate it for a separate library. The first project must isolate and make everything generic. The requirement is that both projects must work on top of that library and the first one cannot loose any functionality
You are in luck! You used clojure and so 90% of your code is already made up of pure functions! And it's easy, with existing tooling, to move those functions to another namespace.
Of the remaining 10%, because you aren't running both UIs in the same process, the parts that will cause you problems are the parts that are Project A specific AND critical to the operation of the library. These are usually top level setup functions.
These functions, a half dozen for a complex UI, will need manual changes without much tool based refactoring assistance. Luckily because they tend to be setup style functions, existing tests and playing around at the repl will allow you to quickly confirm the refactoring was correct.
So in the end even with a complex refactoring like you described you only need to manually worry about maybe 100-200 loc in a 10kloc codebase. And it should take you a lot less time and effort than refactoring the code would if it was in Java.
2) Some of the changes you describe are no less trivial to experiment with in an environment with a REPL. I routinely trial changes to small functions simply by running them in the REPL as discrete units before attempting to re-start an entire server project.
3) Good FP strategy rarely means dealing with anything with a global impact. About the only exception is client-side ClojureScript, where keeping a single "state" atom is largely idiomatic, and even then, actually using it is a last resort action reserved for things like routing and user session, preferring instead local state unique only to a single file for anything else that requires it.
I think this is a case where you need to experience just how big the difference is between an expressive FP approach and something like Java is to really understand why it's not a problem, and indeed, amounts to something like a category error.
When a Java programmer asks me about refactoring Clojure features, I can't help but think about stuff like how the entire messaging system for the app I worked on over the summer fits on maybe 3 or 4 printed pages of code and only took a week and a half to complete, compared to say, EnterpriseGradeFizzBuzz ... The tools you describe sometimes don't exist, it's true, but that's because no one needs them. They exist to automate tedium and complexity that is largely idiomatic to Java and other similar OOP languages, but not expected, desired, or necessary in other paradigms of programming.
I know it's hard to believe, but there are other ways of doing things. We do alright, I promise.