This is a bit tangential to the post, but I've seen a couple comments in this thread that amount to "Clojure is hard to read", but I think that's a really unfair comparison, because people are unwittingly comparing apples and oranges. It seems to me the reason people think Clojure is hard to read is that the language is so powerful and expressive, that when you're reading Clojure code, you're typically trying to understand the actual problem that's being solved, as opposed to just reading a bunch of code that's necessitated by the lack of power afforded by the language. For example, I can easily "read" a bunch of nested for loops in Java, but then I still have to try and understand what the actual problem is that's being solved. People seem to confuse those two things when comparing Clojure with other languages.
I worked in a large company where clojure was used. After the original developers moved on from that project (they always move on), it was a giant struggle to get people who could work in the code base. They ended up rewriting it in something more standard. Whatever benifit clojure provided was outweighed by the high cost of maintaining the code base and the inability to find devs who wanted to work on it.
It saddens me to hear such stories as they show up from time to time. I can't help but feel that a lot of programmers just decide to stop learning at some arbitrary point in their career (my observations suggest that point is around the end of their university term or their first job). The industry too seems biased heavily in favor of using lowest-common-denominator, dumbest possible solutions they can get. And not "dumbest" in the sense of "simple therefore robust" - but in the sense of "barely suitable for the job at hand". It's like wanting to dig a pool, and hiring a lot of people with spades, even though you can get excavators for free - but what, you couldn't be bothered to find a few drivers? And the spade crew comes and is all, "no way we're going to learn to operate an excavator, that's too much, let's dig by hand".
I can't explain it any other way. Learning Clojure, or another Lisp, to the point of being comfortable around a codebase, is a matter of weeks for an experienced programmer. It's comparable to the time you need to even figure out how to rewrite an existing mid-sized codebase, and since you have to read it to understand it, why not learn the language along the way? Then suddenly the rewrite isn't needed. But it feels like a concept of learning a new language on the job isn't even considered by people.
> I can't help but feel that a lot of programmers just decide to stop learning at some arbitrary point in their career (my observations suggest that point is around the end of their university term or their first job).
Indeed.
I had the frustrating, but ultimately fortuitous, situation of having my first job be one where I was asked to build a solution with woefully under-powered tools. I'm not even talking about the difference between hand tools and power tools - I was essentially asked to use kid toys.
That situation made me very angry, and I was constantly working overtime to make up for the lack of productivity of my tools - even then we were going to miss our deadline.
Finally, I managed to convince management to let me and my team use real tools - nothing super advanced, but well suited for the job. Very quickly we turned around the release, I started working normal hours, and when I left that job shortly after the release my employer found that they didn't need to hire a replacement because the workload was so much less.
This experience taught me that the power of your tools does matter, and that the initial pain of technology transition can easily be outweighed by the benefits of using something better.
For me, the quality I look for in my tools is: does it reward mastery? I'm not (primarily) looking the initial growth curve and how long it takes before I get to my current productivity. Rather, I'm looking at where the ceiling is.
Funny, I have the same story from the other side. They could have chosen to not alienate and lay off the clojure team. We were pretty happy, working on interesting projects, independent enough to get things done. But, turns out paying more for a smaller team of experts who know what they’re doing isn’t compatible with Quarterly-Earnings-Driven-Development.
If I were seeking a Clojure job I would probably avoid companies that only use it for a single app while 90% of the active development is in more mainstream languages, especially if at any point during the interview process I get a feeling like I was being hired because all the other devs on the project jumped ship.
I think this is a valid point. We talk about the right tool for the job, but forget that the evaluation needs to include the people using the tool, the people paying for the job to be done, and other elements of the environment.
Excellent points! From my personal experience I'd also like to add that there is a bit of preference involved. I find Lisp like languages much much easier on the eyes compared to Javascript or other C-like syntax. This is also partially due to the excellent support of s-expressions in editors with things like paredit [1], parinfer [2], or clever-parens [3]. With that functionality the syntax becomes nearly invisble and you're editing a /tree/ rather than characters. Coupled with autocomplete and auto-indent [e.g. 4], the code is much easier to manipulate in my experience (but again, might be preference)
It took me a long time to figure out what's going on in that code - I remember realizing how it would have been much simpler to read in a popular statically typed language like C#, types are a lot like compiler verified documentation and in undocumented code they help a lot. To be fair this is not the norm - I've seen many cases where Clojure removed incredible amounts of boilerplate and made reading the code straightforward, and the nicest thing about it was that it made simple things simple.
I haven't worked with Clojure in couple of years, the reason I left it is because I felt it wouldn't scale well in the real world (in terms of developers and teams) but it really helped me "get" LISP and FP so I don't regreat learning it
That source file you're referencing is a fairly low-level library doing lots of interop with Java. That's not really what most Clojure code looks like.
And furthermore: it's just a library, but this library is adding the concept of go blocks (as seen in Go) to the Clojure language. Are you even able to add a new control structure like go blocks to the C# langauge by including a library?
It's not really about Java interop, it's about low level procedural code - Clojure doesn't really look nice when doing it. It does a lot of stuff nicely but there are cases where it just feels wrong to use.
But your second point is pretty much the reason why I wouldn't use Clojure in a bigger environment.I had the misfortune of working C# on a project where some smartass decided it was cool to use Maybe monad in C# using LINQ (something like https://github.com/louthy/csharp-monad)
First it provided no actual value as the code was harder to read, but ignoring that - most of the developers didn't get it and just used it enough to get shit working, it was a hell to debug and maintain.
core.async is public and in core - but what about when your team meber X decides to implement that - and now he sprinkles that shit all over your codebase - people keep using it because they don't want to argue. Clojure really lends it self to that kind of a thing where you do that "smart thing" to solve "that one problem" that ends up being a pointless mess that's hard to maintain and reason about.
I think my issue with Clojure is that it's hard to learn how to write good Clojure. There is very clearly a way that it should be written (to keep the code clean, readable, and concise) but unless you are very familiar with working with Lisps then it's easy to create a giant mess
You could say that for a lot of other languages too, but you usually have some familiar base to work off of. That's less true with Lisps as they are so syntactically different than more mainstream languages
This is getting better but unfortunately your best bet is still to read some examples of good open source projects.
If you or anyone interested in some good starting places for good Clojure see:
- https://github.com/gothinkster/clojurescript-reframe-realworld-example-app for a great webapp example
- All of https://github.com/ztellman work but specifically:
- https://github.com/ztellman/manifold for more advanced Clojure
- https://github.com/reagent-project/reagent for interop with JS and advanced Clojurescript usage
This is my experience as well. I started working pretty heavily with Clojure about 2 years ago and just now feel that I am getting the hang of it in terms of style, yet I still feel like I have a ways to go.
This has not been my experience with languages like Java, Python, Go, etc. where I feel that I can write good, clean code in a matter of weeks from reading through style guides and understanding language conventions.
I just inherited a Clojure project, one which I'm quite well acquainted with the business logic, and I find it extremely hard to read. It's very different from the many other languages that I'm comfortable with already. I'm sure with sufficient learning I can get comfortable parsing what's going on, but it is a problem.
If you can, I'd suggest use spec to improve your understanding and verify behavior. I certainly do know that Clojure can get very dense if you're not careful.
Just out of curiosity, do you have any previous experience or familiarity with FP? Is it the List syntax, the project organization, or the immutable constraints that are the most challenging?
I've built some pretty large apps with Elixir (FP and also has a pipe operator like Clojure). I guess it's the lisp syntax, the deeply nested parenthesis, the prefix notation, etc.
I don't think it's hard to read, I think it's extremely difficult to analyze and almost impossible to debug.
Lazyness is one of those "cool" features, IMO, that have more cons than pros. Yes, certain algorithms look more "elegant", but it's also harder to reason about/debug.
Most of the time, you can ignore the fact that this awesome language runs on the JVM, until you see a barely-comprehensible stack trace containing nothing but `org.clojure.Iseq` something-or-other and no user code in sight.
Looking at how a "real" LISP displays errors and allows you to debug it and comparing it to Clojure makes me feel really sad inside.
Sadly, these are the reasons I tend to favor Go for work-related projects. It's less "fun" to write than a LISP, but makes pinpointing and fixing errors a breeze in comparison. And when my job is on the line, that's the killer language feature for me.
I don't think laziness is really that much harder to reason about, unless you're trying to fit it into a mental model based on eager evaluation. Evaluation occurs when the result is needed; that's so hard about that? And it's not really about making algorithms look "elegant" but rather about composiblity and reuse: it's much easier to add eager evaluation to non-strict interfaces than it is to get non-strict evaluation from strict interfaces. In the former case you're adding additional behavior, which can often be accomplished with a wrapper; in the latter case you're faced with removing something baked in to the API.
Eager evaluation is merely a performance optimization, one which assumes that the data being evaluated will actually be used. When the data is not used it ceases to be an optimization and can even result in incorrect behavior (unnecessary exceptions / non-termination). Either way, making eager evaluation the default is a form of premature optimization.
What makes lazyness difficult for me isn't the mental model, it's the fact that when you get an error, the error is far from its call-site, and the truly helpful information that I could've had in the stack trace is gone. I'm looking at the error - but which lazy operation in the sequence caused it? Where? When?
I by no means claim to be a Clojure guru, and I'm sure people who have more experience can just look at it and know, but for me, the time lost to pinpoint that error is too much. And when I have something in production, I need to be able to pinpoint the error in minutes and deploy the fix quickly.
>Either way, making eager evaluation the default is a form of premature optimization.
I don't see how that's true - it's the default way CPU's operate on the silicon level. Millions upon millions of lines of non-lazy code have been written and put into production and they perform real, valuable work.
I'm not against lazy evaluation or against Clojure - all I'm saying is - it's hard for me to reason about it when things go wrong.
If they don't go wrong (almost never for me) - it truly is a thing of beauty.
> when you get an error, the error is far from its call-site, and the truly helpful information that I could've had in the stack trace is gone
I'll grant you that one. Languages with non-strict evaluation (or tail calls, or inlining, or extensive use of macros) rarely have useful stack traces. You can see what was being evaluated but not necessarily how it got there. The ability to leverage the stack as a debugging aid is a feature of certain runtime conventions more applicable to imperative programming paradigms than to functional ones. In modern Haskell there is a way for functions to receive extra information about their call site as an implicit parameter, which helps with error reporting. I'm not sure what tools Clojure might offer along these lines.
I will point out that tracing the source of incorrect or corrupted data is no less of a problem for programs written in strict, imperative languages. Functional programming adds to this by emphasising functions as a form of data, and non-strict evaluation carries that still further by treating all data as closures. On the other hand, strong static typing limits the kinds of runtime errors which lay hidden within those closures to a manageable set (assuming a pure functional language like Haskell).
>> Either way, making eager evaluation the default is a form of premature optimization.
> I don't see how that's true - it's the default way CPU's operate on the silicon level.
That's not really true, given the existence of speculative and out-of-order execution. In any case, how the CPU operates at the silicon level has very little to do with whether a high-level language optimization is premature.
> Millions upon millions of lines of non-lazy code have been written and put into production and they perform real, valuable work.
I'm not disputing that, but it's beside the point. Code which has been prematurely optimized can still "perform real, valuable work". It just may take a bit longer to run or require more resources to develop (for example by being harder to reuse, requiring more code to be written to accomplish the same result).