Hacker News new | past | comments | ask | show | jobs | submit login

Would you recommend learning Common Lisp over Clojure?



The author? Most likely

https://twitter.com/stevelosh/status/1034147772440760320?s=1...

I have a similar experience. Started out learning Clojure because it was 'practical', stayed with Common Lisp because I had less tooling setup to deal with. To be fair, that was because the Emacs integration for clojure depended on a specific CVS revision of Swank and I was starting out with Emacs. The situation has greatly improved regarding Clojure tooling. Still not as good as CL.

With time I've found CL to be much more flexible than Clojure and less opinionated so you'll be able to explore different paradigms.

That said if you want to write an SPA, go with ClojureScript. It has a good dev UX story (Figwheel <3) and Webpack will have already lowered your expectations regarding build systems to so setting up a ClojureScript project will seem less of a hassle.


Clojure is really your better bet. It is highly practical with a good community and excellent Java interop. You never have to worry about finding a good library. The syntax is also a bit more easily parsable than CL. If you don't touch the Java interop stuff, it's just as elegant as CL.


No, Clojure is not as elegant (or flexible) as Common Lisp, not even close. Also, how can Clojure be highly practical when it ties you to the JVM? When it forces its worldview on state down your throat? When it prematurely optimizes with concepts like STM?

Common Lisp is all about choices and flexibility. Do you want pattern matching? You can get it but it's not shoved down your throat. Same for STM. Same for immutable data structures. Same for Java interop.

On the other hand, Clojure is lacking fundamental features of Common Lisp that are extremely powerful: conditions & restarts, programmable debugger, programmable reader, programmable compiler through compiler macros, dumping images & native code compilation not to mention advanced code analysis and optimization capabilities.


>Also, how can Clojure be highly practical when it ties you to the JVM?

The JVM allows you to use a ton of libraries, on a very performant platform. CL libraries just aren't anywhere near as feature-complete as in more popular languages, and this matters when you actually need to be productive. I don't have time to reinvent the wheel constantly. This practicality is what motivated Clojure in the first place, and why it is far and away the most popular lisp.


I guess your definition of practical differs from mine. For me practicality is tied directly to usability in solving a widely disparate set of problems. This means that flexibility is key. The language I use should not only be able to bend and adapt to the problem at hand but also _not constrain my thinking_. The latter is also known as 'metalinguistic abstraction' and is strongly expressed in SICP as the philosophy of Lisp. Clojure mostly bypasses that since it has a very opinionated but also very constrained view on the problem-solving design space.

As an example of metalinguistic abstraction, some of the tasks I've successfully deployed Common Lisp at include creating a high-performance network stack that runs on ARMv8 and is based on JIT compilation and an entire assortment of solutions that sit on opposite ends of the highlevel-lowlevel spectrum. Concepts such as tight control over memory, stack allocation and one-to-one mapping with generated instructions were critical. Common Lisp allowed me not only to successfully investigate the domain but also to use the resulting code in production. For these tasks, Clojure would have been a total miss both due to implementation decisions (JVM) and its constraining nature -- in this case, immutability, memory and compilation model -- not allowing me to come up with a 'language' that will let me think the right thoughts.


I don’t know of any collected data to support the claims in this Quora answer, but I think it does a decent job highlighting the different interpretations of “popularity”: https://www.quora.com/Which-is-the-most-popular-Lisp-dialect...

What’s your definition of popularity when you say that Clojure “is by far and away the most popular lisp”?


Number of github repos, which likely correlates to number of users. It also is probably a better gauge of the number of maintained, relevant libraries available. Clojure is used in production quite a bit nowadays, and there are excellent libraries for all kinds of modern tasks.

https://redmonk.com/sogrady/2018/03/07/language-rankings-1-1...


I've spent a few months learning Clojure - never gave CL a try. Here are a few thoughts on Clojure:

1. Excellent syntax and library support. It is my first Lisp, but I can't how I programmed without macros and persistent data structures.

2. I hate the stack traces. I've used both Clojure and Clojurescript (mainly cljs), and the stack traces for errors are nearly indecipherable. To be fair I am using React (not Reagent), but I don't find it too much better with other libraries.

3. I hate the build system. It is fractured and there are too many mediocre options. shadow-cljs is the best one I've used, and it works okay not great. I also hate that I can't distribute standalone binaries. I have used pkg, which distributes Node project as binaries, but the binaries are huge (something like 85MB+)

4. The core functional language is excellent, but protocols, types etc. seem much more ad-hoc and not well-designed. I'm used to object oriented (Python) and I find the lack of focus on an object-system a bit unsettling.

I have no clue if Common Lisp fixes these issues or brings new ones.


I'm not sure it's fair to bring up a fractured build system when talking about ClojureScript, and comparing it to Common Lisp, which you would not be compiling to JavaScript and interop'ing with NodeJS etc.

With (JVM) Clojure, leiningen has to be 90+% of the Clojure projects in the wild. And in that world, the equivalent to a native binary is an uberjar, which is rather easy and pretty much standardized.

The stack traces could be improved though.


> Common Lisp, which you would not be compiling to JavaScript and interop'ing with NodeJS etc.

https://jscl-project.github.io

https://common-lisp.net/project/parenscript/


That's great, are you using these? How is the interop? How does that compare with ClojureScript and shadow-cljs? How can you make a single binary?

Maybe I worded wrongly the first time. I definitely didn't mean to imply that Common Lisp can't do something. That would be as foolish as saying Emacs can't do something ;)

I was just pointing out that we aren't comparing like-with-like once you bring in a totally different ecosystem such as JavaScript's (which is historically neither Common Lisp's nor Clojure's main focus).


> That's great, are you using these?

Not often. In Common Lisp I don't have to use them.


To date I have not come up with a situation needing protocols or objects in Clojure that could not be just done with functional programming.

The real killer superpowers come from extend-type and extend-protocol. You can modify existing java/clojure libraries functionality like magic. The functional equivalent is with-binding if I remember correctly.

Some Clojure libraries use protocols heavily and IMHO not just necessary.


> The core functional language is excellent, but protocols, types etc. seem much more ad-hoc and not well-designed. I'm used to object oriented (Python) and I find the lack of focus on an object-system a bit unsettling.

I guess CLOS was well designed.

What Clojure offers is a subset of CLOS.


Clojure multimethods actually can do more than CL multiple dispatch, because they can dispatch by value rather than just type.


CLOS can dispatch by value with eql specifier...


You can implement Clojure multimethods as an extension of CLOS: https://github.com/pcostanza/filtered-functions


> 2. I hate the stack traces. I've used both Clojure and Clojurescript (mainly cljs), and the stack traces for errors are nearly indecipherable. To be fair I am using React (not Reagent), but I don't find it too much better with other libraries.

Stack traces are already the wrong approach. Error handling is first. Stack traces may serve some purpose, but are secondary. Common Lisp was designed for interactive error handling.

An example. Let's say we have a function FAK and it returns the wrong result "0" for 0.

  CL-USER 21 > (defun fak (n)
                 (if (zerop n)
                     "0"
                   (* n (fak (1- n)))))
  FAK
Now we call it with the argument 10:

  CL-USER 22 > (fak 10)

  Error: In * of (1 "0") arguments should be of type NUMBER.
    1 (continue) Return a value to use.
    2 Supply a new second argument.
    3 (abort) Return to top loop level 0.

  Type :b for backtrace or :c <option number> to proceed.
  Type :bug-form "<subject>" for a bug report template or :? for other options.

The first thing you see: no stack trace. We get a clear error message and three options what to do: CONTINUE, supply a new argument, ABORT.

But we also get another REPL, but this time one level down and the original REPL is still there, one level up. This means we can do some computations in the error REPL:

  CL-USER 23 : 1 > (fak 0)
  "0"

  CL-USER 24 : 1 > (fak 1)

  Error: In * of (1 "0") arguments should be of type NUMBER.
    1 (continue) Return a value to use.
    2 Supply a new second argument.
    3 (abort) return to debug level 1.
    4 Return a value to use.
    5 Supply a new second argument.
    6 Return to top loop level 0.

  Type :b for backtrace or :c <option number> to proceed.
  Type :bug-form "<subject>" for a bug report template or :? for other options.
OOPS: we have another error. (fak 1) already does not work. Now we have more options to continue, while we are another error level deeper: 2.

So we decide to go one error level up and explore the problem there further. :c 3 chooses the restart to go to debug level 1.

  CL-USER 25 : 2 > :c 3
Now we choose the CONTINUE restart from level 1. We just return 1 from the call, which was causing the error. The value 1 would be the correct result. Lisp now asks us for the value to use and we type 1:

  CL-USER 26 : 1 > :c 1

  Supply a form to be evaluated and used: 1
  3628800
So we explored the problem and got useful result without ever using a stack trace. Instead we used the tools: debug repls, clear error messages, restarts recovering from errors.

Sure we can also get a stack trace - but the stacktrace is active in the context of the error - it's now a post-error stack trace - it's an in-error stack trace. Means we can, while we are in the error, see the stack trace and use it: change variables, restart start frames, return from stack frames, set break points to stack frames, ...

Let's say we are in the error again. Let's get a quick backtrace:

  CL-USER 58 : 1 > :bq

  ERROR <- * <- FAK <- FAK <- FAK <- FAK <- FAK <- FAK <- FAK <- FAK <- FAK <- FAK <- EVAL
  <- CAPI::CAPI-TOP-LEVEL-FUNCTION <- CAPI::INTERACTIVE-PANE-TOP-LOOP <- MP::PROCESS-SG-FUNCTION
Now we can move in the backtrace down three times:

  CL-USER 59 : 1 > :n
  Call to *

  CL-USER 60 : 1 > :n
  Interpreted call to FAK

  CL-USER 61 : 1 > :n
  Interpreted call to FAK
Let's see the variables in this stack frame:

  CL-USER 62 : 1 > :v
  Interpreted call to FAK:
    N : 2
Okay N is 2. So FAK from 2 should be 2. Let's try to return it. We call :ret 2, which will return the value 2 from the current stack frame.

  CL-USER 63 : 1 > :ret 2
  3628800
This gave us the correct result. We used more tools: printing a stack trace overview, moving down the stack, looking at a stack frame's bindings and returning a value from a specific stack frame.

There are lots of ways to work with a stack trace while we are in the error - the display of the stack trace is only a minor feature.


I agree with this. The error messages in most Common Lisp environments are just so much better than you get in languages like Clojure or C# or Python.

I even have a good example to illustrate this. A while back someone had emailed me because he was trying to implement a terrain generation algorithm called Diamond Square, which I had written a blog post about. It was mostly working, but would crash with an IndexOutOfRangeException at one point. He had tried debugging it and emailed me to see if I could point him in the right direction.

Getting an array out of bounds exception didn't surprise me, because in Diamond Square the algorithm will try to read outside of the bounds of the array by default -- part of implementing the algorithm correctly is detecting that case and making sure to handle it, either by just ignoring that cell or by wrapping it around to the other side of the array. So I assumed he had just forgotten to do this. But then I read his code, and no, he had definitely added a check for `if (x >= array.length)`.

This code was using Unity, so it was written in C#, but the point applies to most languages. Look at the errors given back by C# and Clojure for this problem:

    user=> (nth [:a :b :c] x)
    IndexOutOfBoundsException   clojure.lang.PersistentVector.arrayFor (PersistentVector.java:153)


    float[] fs = new float[5];
    Console.WriteLine(fs[x]);

    Unhandled Exception:
    System.IndexOutOfRangeException: Index was outside the bounds of the array.
      at MainClass.Main (System.String[] args) [0x00009] in <fdc0f271de194e269b9b6c61b644f47b>:0
I went down the rabbit hole trying to figure out how he could possibly be indexing into his array and missing the bounds check. It took my brain hours to untwist itself and see the answer, which came to me in a flash as I was trying to fall asleep. Have you figured it out yet? Here's what Common Lisp would have told me for this error:

    [SBCL] CL-USER> (aref #(:a :b :c) *x*)

    debugger invoked on a SB-INT:INVALID-ARRAY-INDEX-ERROR in thread
     #<THREAD "main thread" RUNNING {10005505B3}>:
       Invalid index -1 for (SIMPLE-VECTOR 3), should be a non-negative integer below 3.
Well there's the problem! He had added the check to make sure the index didn't go past the end of the array, but the algorithm also tries to reach past the left side of the array too! I replied, he added the check for `x < 0` and everything was fixed.

If I had been able to get this running in a debugger I probably could have figured it out, sure. But Common Lisp's nice error messages made that unnecessary -- instead of telling me "You tried to access an invalid index" it says "You tried to access X, but I was expecting Y". It's so much nicer, and in a lot of cases (like this one) instantly makes the error completely obvious.

I don't know exactly why Common Lisp's error messages tend to be so much better than most other dynamic languages. Maybe it's a culture of good error writing from the very start. Maybe it's because the language itself makes it easy to provide good messages, thanks to things like CHECK-TYPE, ECASE, ETYPECASE, etc (and their `C` variants, which is a whole extra layer of goodness on top of all that). I don't know. I just know that hitting an error in most languages feels like hitting a brick wall (especially in JVM languages, good god are those stack traces awful) but in Common Lisp it feels like the system is at least trying to do its best to help me.


I'm not very deep into Clojure - but I find that you end up having to use Java libraries which feels kinda clumsy. Java is a stateful language and Clojure has it's own ideas about how state is managed - so I'm never quite sure how to mix the two (based on some very limited experience... so I might be off-base)


I'm also not very deep into Clojure, but I've had the opposite experience so far. I've used it for some real-world projects and the most Java I've ever needed to pull in was a BufferedReader that I wrapped and forgot about.


Is it reasonable to do Clojure and not have to touch Java?


It's very possible. I use Clojure on a daily basis, and I only rarely need Java interop to use pretty obscure libraries. It's not that bad, it's not any worse than using Java directly.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: