Yes, Clojure is dynamically typed, but I think you're missing the gains you get from FP and expressiveness in terms of reducing labor.
He touched upon what I think is essential. The biggest things to lose by giving up Java is the tooling. Using the IDE I have many possibilities to learn about code, without ever needing to run it. For example by using find usages/implementations. The types on methods often give out hints about the big picture. Like, if a method passes a JAXB context, than I know I'm in the infrastructure layer. If I'm looking for a business rule "how is the price calculated", I can very quickly cut off a large portion of code which does not do it. After few iterations, it's almost certain I will get to the relevant code part.
Then I typically launch an application and put a breakpoint, to test my assumptions. After that, I have a lot of confidence about how the application behaves in that spot, even If I saw it for the first time.
If anybody asks me "Tell me if we can extend the functionality of calculating user score to include expert rating", I can provide a good effort approximation, even in a never seen before application. I just know, that given the tooling, I can filter out unrelated code parts and concentrate only on the relevant one.
The above concerns only code comprehension of a large project. The tooling does give you also power to do macro level restructuring, like breaking a large monolith into smaller modules. The compile time errors are like a high accuracy automatically generated todo-list.
Since it takes so little effort to make large changes, you gain the ability to do it multiple times and refine. If I broke something into two modules and a dependency cycle came up, I see it right away and can do everything again, a little bit better with the extra knowledge I got.
I have never seen anybody doing the same in Ruby, maybe because I've been working only about two years commercially in it, but a lot of times I've seen something quite opposite: introducing more hacks decreasing code quality, just to work around some conceptual error. While working in Java, I have seen a lot of situations like "we'll doing a hack here and the real solution will come with the next big release" and really the solution does come.
High level changes typically break a lot. With tooling, you gain confidence you can fix them, without introducing a lot of regression bugs. In Ruby, I saw the people just give up and leave monkey patches around. Then they just leave the project for the next unfortunate programmer.
So, back to the topic. You say that the change name refactoring is very simple in Clojure, because the impact of change is local to the method. My question is: How does Clojure tooling aid You in changing a place with a global impact? Like, changing the argument list of a very often called method (e.g. the method to serialize a structure to string to require the caller to provide an explicit encoding)? Can You, right after doing the change, quickly estimate how much work is there to fix all affected places? Do You have confidence that the list you provide is complete and doesn't miss anything critical? How certain can you be without running the code, only analyzing?
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.
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.
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)?
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.
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.
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.
Another important thing is to not expose APIs. APIs suck. Use eDSLs instead - they give much more flexibility to a code. You won't have to modify the caller code when you change anything in the implementation. It's an order of magnitude more powerful encapsulation than anything you'll find in the OOP.
And, this way your code discoverability will be nearly on par with fully static languages, without the need in fancy heavyweight tools.
Can you explain in more detail or point at an example? How do you provide typing across modules? With type hints?
eDSLs instead of APIs - not changing client code... This sounds a little too good to be true. :) Can you provide an example. let's say for a UI form case?
By only exporting simple functions (which only take DSL commands and return DSL commands) out of the modules - it works well with Racket and some other Scheme implementations, for example. In Clojure you can simulate this with namespaces and an explicit "(require ...)" which can be easily followed by the static tools.
This way every name is either locally defined or coming from one of the modules you explicitly required, and with no higher order functions allowed in between modules you can track everything to its source.
> let's say for a UI form case?
That's the most classic example of this approach, used more widely than any other message DSL case.
An UI frontend is forming messages (like "(button-clicked 'Save)" for the low level approach, or, for higher level, like "(ui-do-save)").
A backend is receiving nothing but a stream of DSL commands from the frontend (i.e., no API calls in between, the stream can even be serialised as text), and execute them one by one, replying with another stream of DSL commands for the frontend (like "(go-to-dialogue 'Choose-File)", or "(hide-save-options)").
The more abstract your command stream is, the less changes need to be propagated either way.
What is common with the Kay view of OO (and different from all the modern OO languages) here is that messages are asynchronous, and this is a very important aspect.
Right now I am working on a back-end API, and with the REPL and Intellij+Cursive I can't imagine a smoother experience.
To your point, by using clojure I can startup a debug repl, and launch the web server from within the REPL. At that point, I can
a) breakpoint at any line of code and see the state of the system.
b) manipulate my system in real-time and see changes in real-time without needing to recompile anything
c) if I see a function and I want to see the definition, or follow the call stack to figure out which lib this particular function came from, I can do that with a single keystroke via cursive.
Plus, you get all the features of inline docs, source code of function, etc (available via REPL or cursive).
It's really a treat.
[0] https://cursiveclojure.com/Of course, renaming symbols in Clojure is not such a big requirement as in Java IDE, because most of the things are isolated within a single function in Clojure. Consequently, renaming symbols is just a convenience feature, not a must-have feature for Clojure development.
I've seen a lot of Clojure code where keywords have project wide semantics.
A fundamental point in the philosophy of Clojure is the emphasis on raw data. This is actually the reason why we love the language, because it lets us work directly with data, instead of dealing with language related complexity, such as API, type, syntax, etc. The Clojure community only realizes this advantage a couple of years ago. This Rich Hickey's talk in JavaOne gives a flavor of this idea: https://www.youtube.com/watch?v=VSdnJDO-xdg
The workflow I have grown to love in clojure is an initial freewheeling, REPL-driven, dynamically typed phase in which you explore the problem you're working on, followed by a period of stabilisation that results in nicely factored, mostly-typed code. I personally think this can often give you the best of both worlds - quickly find the right design for a feature using clojure as a dynamic language, then robustly integrate it with the rest of your project using clojure more statically.
The intellij plugin for clojure called Cursive also has support for project wide renaming.
I think it was on google groups somewhere but I can't find it offhand, but one of the early big contributors of prominent clojure libraries has since completely quit clojure specifically for that reason.
The only one I can think of, the complaint was the contribution/review process (I think it was Zach Tellman who contributed some work only to have Rich rewrite the entire thing out of the blue 6 months later). Unless you're thinking of a different case?
EDIT: switched to haskell? Was it https://github.com/bitemyapp ? I didn't see the conversation you're referring to, but the comments he's made about clojure more recently made me ignore him; they weren't very constructive :(
Haskell was never really an option though because of libraries (and I'm not sold on Haskell anyway), so it led me to F# and Scala, both of which I "ideally" liked a lot. But lack of tooling and 3rd-party code, long compilation times, and obvious minority status on their respective VMs (e.g. nulls show up where the language wouldn't allow them) eventually drove me back to C# and Java. I don't like these languages quite as much, but all-told, when you throw in the ecosystem concerns, it seems like these are the languages that have the least friction against "just getting stuff done", and the ones I'm most productive in. For now at least.