As a clojure fan, I think this is a really well balanced and fair assessment of clojure. It's a bit surprising they didn't get into the whole "hygenic macro" business, which seems like the most obvious differentiator between the two languages to me. I would argue the macro system in scheme is very complicated (even more so in the extensions found in many scheme implementations) and this somewhat undercuts the minimalism appeal of the scheme language. (Though the scheme macro system is still extremely cool from a computer science standpoint.)
Also, the post states that you can't have nested "tail-call optimized" loops in clojure, which is only partially correct: Using nested loops is commonly done in Clojure, the only thing you can't do is have the loops call each other mutually-recursively, which is a far less common use case (but still a feature I hope to see in Clojure some day).
> As a clojure fan, I think this is a really well balanced and fair assessment of clojure.
Thanks for the kind words. I tried to keep my biases out of it as much as possible and tried to avoid making it too ranty. And on the whole, I like Clojure; it's just not really my first choice as far as Lisps go.
> It's a bit surprising they didn't get into the whole "hygenic macro" business, which seems like the most obvious differentiator between the two languages to me.
I debated including a bit about macros, but in the end decided that this would be a bit too in-depth. Clojure uses namespaces in a somewhat clever way to work around the bulk of the hygiene issues you get in CL with defmacro, but the macro system itself is rather uninteresting. I think their custom syntax for gensym is a nice touch, even though it's yet more syntax.
Scheme's macro system is very advanced and relatively complicated indeed, and the R5RS/R7RS standard system is only pattern matching/rewriting based.
Maybe I'll do a separate piece on the macro system if people are really interested.
Hey, I also enjoyed the article... and would definitely be interested in reading more about macros.
But unrelated to that, I wanted to comment on this observation:
> In my current project, it takes almost 30 seconds to boot up a development REPL. Half a minute!
I work mostly with JVM languages and the JVM startup nowadays is actually very fast for a VM, under 100ms for your code in main to be running from a cold start. 30 seconds to boot is completely unheard of in my experience. If whatever you're using is open source, I wouldn't mind having a look to figure out what the problem may be.
That's Clojure, it compiles .clj files on demand into jvm .class when you require them. Here probably his user.clj is loading the whole application. You can do some dynamic things to delay the loading/compiling until after your REPL starts, so that you get a repl in 1-2 seconds. https://gist.github.com/dustingetz/a16847701c5ad4a23b304881e... In production you would ahead-of-time compile.
I also shaved 35% of my "time to CIDER REPL" by passing a few parameters to the JVM (-client, -Xverify:none, plus two tieredcompilation-related settings I copy/pasted).
I’m curious what you mean by “ Clojure uses namespaces in a somewhat clever way to work around the bulk of the hygiene issues you get in CL with defmacro”. CL and Clojure are, in my experience, basically equivalent here: CL’s solution to hygiene issues is a combination of packages (“namespaces”), gensym. The main difference is that, as a Lisp-2, hygiene issues tend to be easier to avoid because you can’t use LET to shadow functions (which, in my experience writing Clojure, is the most common problem: accidentally declaring a variable called “name”, or similar).
Because Clojure relies so heavily on namespaces, almost all identifiers in a macro's output are going to be fully namespaced already, especially because syntax-quote does this expansion already. Therefore, unintended variable capture is highly unlikely.
> And on the whole, I like Clojure; it's just not really my first choice as far as Lisps go
... Yet ;)
I think reading your article, familiarity seemed to be the source of a lot of your pain points. I wonder if the more you use it, to the point it becomes as familiar as Scheme, if you'd change your choice. Maybe not, but I think that's a possibility.
I've wrapped my head around the CL/Clojure sort of macro but never really grasped the essence of Scheme's hygenic system, always happy to read more about Lisp macros.
> [...] the R5RS/R7RS standard system is only pattern matching/rewriting based. Maybe I'll do a separate piece on the macro system if people are really interested.
I'd like to throw in a "me too" too.
I'm comfortable enough using Scheme's syntax-rules macros, and I've seen a couple examples where syntax-case macros do something beyond, but I'd love a walkthrough of how syntax-rules maintains hygiene and a concise explanation of how Clojure nearly accomplishes the same goal with namespaces.
> Also, the post states that you can't have nested "tail-call optimized" loops in clojure, which is only partially correct: Using nested loops is commonly done in Clojure, the only thing you can't do is have the loops call each other mutually-recursively, which is a far less common use case (but still a feature I hope to see in Clojure some day).
I guess you are referring to recur (https://clojuredocs.org/clojure.core/recur), and I also think that it's a quite cheap way to get ~90% of the benefit of TCO without actually implementing TCO. It's also more explicit than TCO which is a plus over TCO.
I think what the OP meant about nested loops was that you can't directly write an inner loop that conditionally continues with the outer loop. In Scheme you'd write a named LET inside a named LET, with the inner one invoking the outer in a branch of an IF. With Clojure's loop/recur, as I understand it, you'd have to make the inner loop return a flag for the outer loop to test again about whether to recur, because the only construct you have for continuing a loop is 'recur'.
This is not a rare situation. Am I missing something about how Clojure does it?
My Lisp dialect (Cant) has a construct like the named LET, but with the name being optional (defaulting to 'loop'). This makes simple loops as concise as Clojure's, and fancier ones as flexible as Scheme's.
You got the right idea, but in my three years of using Clojure professionally I never ounce needed this, so I feel at least in some cases like mine, this is a pretty rare situation.
In my ten years of Clojure, half of them professionally, I can probably count the amount of times I've used recur (outside of playing around) on my fingers. I typically don't need to use it because the sequence functions provide a better, higher level alternative. I see recur as quite a low level building block and the last thing I'll reach for. I'm pretty sure I've never used a nested recur.
Also, the post states that you can't have nested "tail-call optimized" loops in clojure, which is only partially correct: Using nested loops is commonly done in Clojure, the only thing you can't do is have the loops call each other mutually-recursively, which is a far less common use case (but still a feature I hope to see in Clojure some day).