I only worked with Clojure professionally for a couple of months, but the experience was less than stellar. Most of it probably had to do with the codebase being written by people without any experience, but a couple of problems could be traced back to the language itself.
I'm preparing to write a lengthy post comparing Clojure and Racket, the other Lisp I have been using for personal projects for a couple of years now. My opinion is that Racket is a better language overall, although it may be less practical in terms of writing production code and it certainly lacks some of the nice features Clojure brings. On the other hand, many of these nice features are available as libraries (not only in Racket - in most Lisps, including Elisp and Common Lisp).
For the last two years, I used StumpWM as my window manager and so I learned some Common Lisp. Again, as a language, I think Common Lisp is still better than Clojure, although it's definitely stranger.
I use Emacs as my default "computing environment", so I naturally learned Elisp, too. This is the only Lisp I used which I'd consider inferior to Clojure, but even then it works quite well for its use-case.
All in all, from the perspective of Lisp family of languages, Clojure doesn't seem to be exceptional in any aspect. It does have nice features, and I bet it feels much better compared to Java, but it also has some downsides to it which are irritating when coming from Racket or Common Lisp.
This experience is extremely common. I have lost track of the number of people who have formed a negative opinion of Clojure because they were forced to pick up the pieces of a half-baked project written by someone who wasn't familiar with Clojure or its idioms.
It should go without saying that this should not actually reflect poorly on Clojure as a language.
The problem is that problem clojure code is the gift that keeps on giving, and it boils down to the problem I have with clojure in general:
Bad clojure code is unmaintainable spaghetti code; which gets worse over time, as people attempt to 'patch on' fixes without doing the heavy lifting of trying to figure out:
- What was the original author actually trying to do?
- Why the heck did they do it like this?
- How do we create the same functionality and prove it works with these rubbish tests that only test the individual units of work, not the application function?
- Why is it all in one giant file?
I've never seen code bases descend into chaos as fast as our clojure ones have.
Nice, elegant clojure is a pleasure to work with for personal projects, but I'm never using it professionally again.
You might argue it doesn't reflect on the language, but I think it does. Given what I've seen, I'd argue that clojure has an inherent complexity that results in poor code quality outcomes during the software maintenance cycle.
...specifically, sections of bad code have a disproportionately negative effect (compared to other languages) on the surrounding code and negatively impact the entire project's code quality.
You hack something out for a deadline? You better go back and clean it up, because if you don't that codebase is screwed. shrug That's just been my experience over the last year on three different code bases.
This has been my experience as well. It allows you to write extremely dense code, without type checks to help you out. And it imposes a functional paradigm, again without type checks to help you out. And it makes new developers think in ways they're not used to, again without type checks to help them out. So the combination of these makes certain that some way or other, teams will write contorted code that is in major need of refactoring ... without type checks to help them out.
I switched to F# and "get" FP much better now. I'd know what I was doing if I ever switched back to Clojure. But I have no good reason to personally, and all the aforementioned reasons not to professionally.
No language I've used is exempt from the possibility of people writing bad code evoking those questions.
In fact I've had these reactions at various times to code in every language I've worked in (including Clojure, to be sure), but I don't agree with your argument about Clojure having greater than average inherent complexity.
Once you get comfortable with parens, the core seq functions, and even a basic understanding of laziness & immutability, so much of the inherent complexity you find in many programs just goes away. Well -- most of the time. Again, Clojure is not going to stop you from negating these niceties with bad decisions.
The one thing I'd maybe admit to it being a little above average on is the temptation to over-engineer due to novelty or feeling clever. Let's rub some core.async on it! Parallelize all the things! It's actually easier to do this because of the lack of inherent complexity in Clojure -- as the author mentions, everything is oriented around simple maps & lists, so juggling them and squeezing them through Rube Goldberg machines of transformations and mystery macros is definitely a thing you can do.
But, ideally, you just learn to . . . not do that. Like abusing Ruby metaprogramming.
Tell me you've never picked up a piece of clojure code and gone, WTF does this do? What are these global channels for? Why is the whole application constantly updating a top level full-application-state atom? Why are we blocking indefinitely with some magical invisible state hidden in a closure as we iterate over a collection of objects to process?
If you have a beautiful clojure code base, it's fantastic...
But, ideally, you just learn to . . . not do that
How do you fix a bad code base when the functions aren't pure (global injections, global channels), or are 'pure' only in the sense the input is the entire application state, including non-pure objects like a database handle? Or your functions are non-deterministic due to some kind producer/consumer race condition?
I get it; don't do that.
...but what do you do when it's too late, and someone has already made those bad decisions?
Where are the debugging tools to help you figure out what's going on, and the refactoring tools to help isolate code units and replace them?
I mean, sure, you could argue that's an issue in any language, but all I can say is that I've used a lot of other languages, and the only other similar experience I've had was working with perl.
> What are these global channels for? Why is the whole application constantly updating a top level full-application-state atom? Why are we blocking indefinitely with some magical invisible state hidden in a closure as we iterate over a collection of objects to process?
I think something that would help the Clojure community improve, especially with regard to introducing Clojure to a team is to ask why this kind of thing doesn't happen in Python.
I know Python doesn't come with atoms, but you could certainly stick the whole application state in a global variable. You wouldn't though if you're a working programmer with a modicum of experience on your first Python project. Do people just see some new idioms and forget everything they already knew about programming?
Python does have closures, and you certainly could write one that blocks due to some hidden state or does something stupid involving multithreading. This isn't seen as a problem with the language though; it's seen as a sign that the person who did it might need to work more closely with someone more experienced or that the team needs better code reviews.
I suppose. When you update and depend on a global atom your functions aren't pure, at all, in any sense.
Why are you even using FP at that point?
Global application state should be represented in a single root storage; the application data store (ie. database), and the interactions with it should be controlled and sanitised.
If you have a thousand little places across your code base updating and reading from the database, that's horrible code too, in java or in clojure...
Modern UI frameworks are carefully controlled access and update to the display state for a UI; they happen in a controlled and orderly manner specifically to prevent the chaos you get otherwise; if not, you're doing it wrong.
(notice, you don't just reach out and update atoms directly by hand; there's nothing wrong with having a global application state; that's a good thing, but directly interacting with it is not)
You were closer to it the first time: it's usually a mistake to stick your whole application state in something resembling a god object. The client-side of a single-page application might be an exception under some circumstances when the language provides sane tools for dealing with it.
> No language I've used is exempt from the possibility of people writing bad code evoking those questions.
Of course there's no stopping a determined person from writing bad code, but there's a big difference between various languages in how easy it is to accidentally or unknowingly writing bad code.
It may be that you've been unlucky. I've been using Clojure for nine years now, and the majority of codebases I've worked on have been very clean. My experience is that Clojure tends to produce flat codebases, whereas OOP languages produce codebases that tend to be deeply nested.
I can't prove any of this; it's all anecdotal, but I thought I'd mention that your experience might not be typical.
I've only played with Clojure a little bit. Can you explain what makes bad Clojure code especially bad? Or why the badness leaks into the surrounding code?
(The impression I've gotten is that the persistent data structures are very nice, and the language has some good ideas to go with them, but overall it didn't entice me away from Scheme or Common Lisp. But that impression doesn't explain why you had this bad experience with troublesome Clojure code.)
I've been working with Clojure professionally for the past 6 years and I've had a very positive experience. My team finds that code is actually very maintainable.
Two main reasons for this are that Clojure is immutable by default, and it has minimal syntax. Immutability allows projects to be naturally compartmentalized. You can do safely do local reasoning on parts of the project. Meanwhile, simple and consistent syntax means that it's easier to read and understand the code written by others. There are far less language quirks to remember than in most languages.
I also find that the editor integration with the REPL is a huge plus as well. Whenever I run into code that I'm not sure about, I can just run it and see what it does.
>Bad clojure code is unmaintainable spaghetti code; which gets worse over time, as people attempt to 'patch on' fixes without doing the heavy lifting of trying to figure out:
Couldn't you say the same thing about most languages?
That would be my take on this too. If you get code written by people who don't know the language, the result is pretty much always dreadful. I know of no language that mitigates this.
However, I'd be interested in which parts of Racket and CL klibertp thinks are making them better languages than clojure.
> this should not actually reflect poorly on Clojure as a language.
Why not? If a language's goal is to be practical (as is Clojure's), then it should take practical concerns into consideration. This is something that the Java community definitely gets right.
Because someone who has never heard of map/reduce/filter/partial will need to do some studying to leverage the spine of the language in order to make living, breathing code-organisms.
God forbid someone would have to learn how to use map, reduce and filter - 3 functions present in Ruby, Python, Javascript, PHP, Erlang, Scala and many others.
I have a feeling this issue can be made more abstract and cover more languages, more paradigms than just that of Clojure. The real problem here does not seem to be Clojure, but finding developers as well as faith in new (or small) languages to build that solid, big project that has stood the test of time before you abandon it for something you have an easier time finding developers for, or faith in.
My team does code reviews and pair programming, especially when onboarding new devs. This helps new hires get comfortable with the style the team uses, and ensures we have clean code in our project.
We've hired a number of devs for our Clojure projects, and none of them knew Clojure when they started. We found that this process has worked very well for us.
Yeah, I'm surprised myself... FWIW I wrote it that way on purpose: I don't have the time right now to cover the specifics, and mentioning some things without proper explanation - in the context of PL advocacy - would almost surely lead to a flame war.
No judgement on your comment. It was a perfectly fine comment. I was just confused as to how other comments with objective examples were not higher up.
I've "shipped" a few racket projects in "production" now, but have never used clojure - only read about it, including posts like the above. I've had more than a few people tell me about how amazing clojure is as a language but never from people with experience in other lips. I'd love to read about your experience.
I'm surprised by my preference for old, cryptic, stranger languages. car cdr are better symbolic representation in my mind than first and rest are in clojure. Especially when trying to think recursively.. There's a mystique in languages.
The nice thing with CAR and CDR is that you can have combinations like CADR, which is (CAR (CDR A-LIST)). Many structures are rarely linear (say a s-exp repr of a JSON object), and the C*R family of macroes are really useful.
A user-defined function with a name indicating what you're reaching for. Occasionally you might want to write a generic cons handling function where all you can say is that it's the car of the car, but those instances are extremely rare. The only place you should be writing car and cdr compositions is in one line wrapper definitions, and there the benefits (and drawbacks; it doesn't really matter which you use) of the c*r composition functions don't really show themselves.
edit: It's the same logic as using first and rest when you're dealing with a list (that is represented with conses). If you're handling parsed JSON, why would you be talking about cars and cdrs?
The thing is one would be (rest (first (rest (assoc ..., the other is (cdadr (assoc ..., which are equivalent in effect (and the latter has way less parens). I dont understand why there's an ideological you should use this or you should use that thing going on. C*R stuff is an abstaction over combinations of FIRST/CAR and REST/CDR, and I dont see why they should be avoided at all. They've got their place, and all this thread is full of arguments backed by mere taste.
Ok, so I managed to add RSS-like thing to my blog. The format works with my RSS reader (some simple Chrome extension), but let me know if it doesn't work elsewhere. I even have a post about it: https://klibert.pl/posts/rewriting_the_blog_again.html ;)
I'm preparing to write a lengthy post comparing Clojure and Racket, the other Lisp I have been using for personal projects for a couple of years now. My opinion is that Racket is a better language overall, although it may be less practical in terms of writing production code and it certainly lacks some of the nice features Clojure brings. On the other hand, many of these nice features are available as libraries (not only in Racket - in most Lisps, including Elisp and Common Lisp).
For the last two years, I used StumpWM as my window manager and so I learned some Common Lisp. Again, as a language, I think Common Lisp is still better than Clojure, although it's definitely stranger.
I use Emacs as my default "computing environment", so I naturally learned Elisp, too. This is the only Lisp I used which I'd consider inferior to Clojure, but even then it works quite well for its use-case.
All in all, from the perspective of Lisp family of languages, Clojure doesn't seem to be exceptional in any aspect. It does have nice features, and I bet it feels much better compared to Java, but it also has some downsides to it which are irritating when coming from Racket or Common Lisp.