Maybe a broader version of this point: clojure tries to be something suited both for industry use at scale and for hobbyists wanting to do freaky-deaky stuff at the same time, but those are fundamentally incompatible.
I say this as someone squarely in the latter category who has exactly the opposite problem from you: I love clojure the language and its features, but all the major libraries seem to be written by refugees from Java working at big companies who have a will to massively overengineer everything.
I'm thinking of libraries like mount and such which everyone uses for everything, and which I experience as, like, "did you want to inject your dependencies in your injected dependencies? Well first you have to do a quadruple axel inversion of control at the fifth abstraction layer out and use the fooflarb design pattern to delegate your inheritance and you thought you could even find the place in the code where it talks to the database? Good luck, sucker." Like, I always thought the point of a functional language was to not have to do any of that stuff...
Like, I vividly remember the first time I used a clojure web framework that brought in all that stuff, and I literally, I'm serious here, could not find where in the code it was actually talking to the database. Just indirection on top of indirection on top of indirection. And when the application I was building on top of that, which was supposed to be quick and dirty, started mysteriously losing data, the only thing I could do was just tear it down and rewrite with Python and flask.
I'm surprised to hear you had such a bad experience with mount--it's always struck me as one of the greatest exemplars of what Clojure allows you to do that would be impossible in Java. Managing fundamentally stateful components was always a nightmare for me in Java and Java-related languages: you'd never know which class was in charge of initializing what, would always be tearing your hair out trying to keep track of when you can assume certain components have been successfully initialized, etc.
With mount on the other hand, you tell it how to start and stop the component (and stores a reference to the component in the same var where you specified this), keeps a reference to the component in exactly one place, and then it automatically makes some reasonable assumptions about the order in which they need to be initialized, while allowing you to opt out as needed. Re: your example of finding a reference to the db, shouldn't it just have been in the var set up by a call to `defstate`? (Also, which web framework was it? Luminus?)
It's not fair to point to specific overengineered libraries and say this is the language idiom. Clojure is a small, stable language (few changes over the years) that encourages creation of DSLs through macros. It provides solid ground on which to build the abstraction you want, so it's possible to build these EE-ish indirection nightmares if that's what you need.
Clojure's design allows it to serve the needs of both hobbyists and enterprise, but the latter requires strong, disciplined developers who understand to avoid loose, hobbyist code that the language allows, and who will agree on which patterns to use in the codebase (something the hobbyist gets for free mostly). It's a punishing language for a project with high developer turnover.
If anything, I'd say that Clojure discourages the creation of DSLs through macros compared to most other Lisps, and encourages the use of plain data and functions first.
Fair enough---I guess part of my dissonance is that's also a lisp, and I feel like 99% of the people attracted to lisps are us wild-eyed hobbyist types who really just want to build an individualized DSL for every project.
The s-expressions in lisps and clojure are one of my favorite attractions. There's so much less cognitive load when the syntax is so simple. Elixir, another great language, has a painfully complicated syntax (plus a lot of syntactic sugar and cases of there's more than one way to do something).
Clojure is a bit noisier than lisp in terms of syntax, but it's also more expressive. I find the balance and design choices pretty reasonable.
Using a functional language doesn’t make reality and the complexity of state go away (unless you are writing a compiler or a program/system that fully and purely mathematically controls its inputs and outputs).
A good functional language lets you clearly define, manage, and trace where your code meets reality, though.
That kind-of needs strict static type systems, rather than loosey-goosey dynamic tag checking, though.
That's IMO the main reason statically type checked languages like C++/Java/etc work much better at large scale than runtime tag checked languages like Python/Clojure/etc.
I think "needs strict static type systems" is a hypothesis but one with certainly many anecdotal examples to refute it. Most of the studies that have been done (and admittedly these studies are extremely difficult to do well) show lower or similar bug counts in dynamic languages vs static languages.
If you've built large scale system with statically type checked languages that solve all these problems, I assume you never had any bugs right? Never needed to write any unit tests?
Yeah Python culture is generally pretty "direct". I did mostly python the last decade and got so used to "no problem to read the source of pytorch or whatever" that I meanwhile really feel challenged when I look at Java, C# whatever framework/library code to figure out where the abstractions end and the actual work is happening.
My own code also became less and less abstract over time.
I say this as someone squarely in the latter category who has exactly the opposite problem from you: I love clojure the language and its features, but all the major libraries seem to be written by refugees from Java working at big companies who have a will to massively overengineer everything.
I'm thinking of libraries like mount and such which everyone uses for everything, and which I experience as, like, "did you want to inject your dependencies in your injected dependencies? Well first you have to do a quadruple axel inversion of control at the fifth abstraction layer out and use the fooflarb design pattern to delegate your inheritance and you thought you could even find the place in the code where it talks to the database? Good luck, sucker." Like, I always thought the point of a functional language was to not have to do any of that stuff...
Like, I vividly remember the first time I used a clojure web framework that brought in all that stuff, and I literally, I'm serious here, could not find where in the code it was actually talking to the database. Just indirection on top of indirection on top of indirection. And when the application I was building on top of that, which was supposed to be quick and dirty, started mysteriously losing data, the only thing I could do was just tear it down and rewrite with Python and flask.