11 ms·
One Major Difference Between Clojure and Common Lisp
- kyllo 11y agoThis is not a Clojure problem, it's a JVM problem--no tail call optimization. Armed Bear Common Lisp has the exact same limitation.
- eru 11y agoAnd didn't tail call optimization only become standard in Lisps with the advent of Scheme? In any case, at Standard Chartered we didn't have tail call optimization with our Haskell dialect either. It didn't matter too much in practice, because you should be using combinators anyway. And when you are calling foldr or map, you do not care that somewhere hidden away they are implemented with a loop in C++, as long as they behave right.
- jfoutz 11y agoYup. The CL spec does not require TCO. Some implementations only do TCO on self calls, so no mutual recursion. And really i think the "Classic" lisp way to loop is loop, not recursion. Scheme forces the point and requires full TCO.
- hga 11y agoAnd really i think the "Classic" lisp way to loop is loop, not recursion. Both, I think (from long ago memories). The "modern" loop macro (which is probably Turing complete like I seem to remember people saying format is :-) is I think a relatively new thing, I overhead a lot of discussion about its design in the early '80s. Although it probably had precursors, mainline Lisp, now Common Lisp, is decidedly multi-paradigm, there were even sops thrown to FORTRAN programmers as I recall, probably back from when there were only a very few computer languages in existence (heck, LISP's first implementation, on a vacuum tube computer, was as FORTRAN subroutines). So overt things like loop are in theory idiomatic as well as recursion.
- lispm 11y agoMost CL implementations will do more TCO than on self calls or mutual recursion. Most support full TCO, with language limitations. The ones that don't provide any form of TCO are old Lisp Machine implementations and ABCL on the JVM. SBCL, OTOH, provides full TCO. Some older overview about Common Lisp implementations and their TCO support: http://0branch.com/notes/tco-cl.html http://0branch.com/notes/tco-cl.html
- ohyes 11y agoI seem to recall it depending on compiler settings whether sbcl does tco. Which means you probably don't want to rely on it in general unless you are okay being locked to a specific implementation and specific optimization settings that may-or-may not seem magical to the uninformed user.
- lispm 11y agoThere is Common Lisp software which needs TCO and only runs in TCO supporting implementations.
- hga 11y agoIndeed, and as far as I know Scheme JVM implementations, which I haven't looked at in a long while, either do it per the spec and are slow, SISC reputedly, died about the time I might have started using it, or go through contortions like Kawa to do the best you can. Don't know about JScheme, it was dead before then, and I just noticed Bigloo will compile to the JVM, adding one to the list of 4.
- kyllo 11y agoWell SC's proprietary compiler is probably different, but GHC is self-hosted (the runtime system is C but the compiler is implemented in Haskell) and the map and fold functions in Prelude are recursive. Here's the source code for `map` in Prelude: map :: (a -> b) -> [a] -> [b] map f [] = [] map f (x:xs) = f x : map f xs But it is definitely still true that explicit recursion is discouraged as being too 'low-level' for most Haskell code and it's preferable to use higher-order functions instead.
- eru 11y agoYes, Standard Chartered's compiler a bit different from ghc. That's mostly for historical reasons. Yes, GHC can and does just use the recursive goodness, and compile it away to no-stackframe-adding jumps.
- SeanLuke 11y agoWait, what? Kawa does tail call optimization just fine. And it's a JVM language. https://www.gnu.org/software/kawa/Restrictions.html https://www.gnu.org/software/kawa/Restrictions.html
- Volundr 11y agoThe --full-tail-calls flag is not on by default, partly because it is noticably slower (though I have not measured how much), and partly I think it is more useful for Kawa to be compilatible with standard Java calling conventions and tools. Well, for some definition of just fine. Well implemented TCO is a performance boost, not hit on most platforms. The lack of TCO built into the JVM means that JVM languages like Scala and Kawa generally have to roll their own on top of the JVM, resulting in a performance hit.
- SeanLuke 11y agoPerformance is irrelevant I think to the discussion. The claim is being made that Clojure and ABCL don't do full tail call elimination because of stack restrictions in the JVM. That sounded unlikely to me. Kawa does full tail call elimination and it's a JVM language. Hence, this claim can't be true, right?
- Turing_Machine 11y agoWell, if you ignore performance, all Turing-complete languages are identical, right?
- Guvante 11y agoKawa fakes it. The JVM doesn't support it so if you use a normal function call you don't get tail recursion. You could avoid calling functions and instead do your own stuff but that doesn't change the fact that the JVM doesn't support it. That is what they mean by performance, you lose performance because you can't do naked function calls in those cases. Since except mutual recursion (which is difficult to detect tail recursion for correctly) the benefit of tail recursion is a performance boost it is ignored when the cost of implementing it kills your performance.
- codecurve 11y agoFor all the gripes surrounding the lack of TCO on the JVM, Clojure really does provide a great set of tools to deal with iteration in a functional way. It's a rare thing when I need to fall back to using loop & recur.
- hga 11y agoThis is why I call Clojure a "Lisp", whereas I somewhat pedantically refer to previous languages in this family as "LISPs", as originally coined from "LISt Processor". But it's still a Lisp, I went straight from old half-remembered mainline LISP and Scheme to Clojure in a recent small web server project without difficultly. The dynamic style of development is the same as is the typing, if you're not using lists, then the OPs problems don't come up (idiomatic Clojure web programming uses maps, key value pairs), the syntax is still s-expressions, albeit polluted by arrays denoted with square brackets where it makes sense. And I believe the article is wrong in one sense, the JVM treats non-tail recursion like other languages, growing the stack. It's tail call optimization (TCO) that's the issue: mandatory in Scheme, don't know about its prevalence in Common Lisp implementations, and it's awkward in Clojure but wasn't much of a jump from SICP for the typical cases. And I think it might be a good idea to require signaling when you intend to tail recurse, it's easier and much quicker to find in compilation than when you blow the stack running it.
- olewhalehunter 11y agoThere's something very off about working with a lisp and not being able to inject stateful inline expressions for prototyping. In other lisps you find the fluid abstraction/computational nature of sexps holding up very nicely while in Clojure you find yourself having to rewrite larger portions of functions just to test or fix something. A lot of my turnoffs from Clojure is that it takes away the whole "geometrical logic glue" aspect of sexps and leaves behind what feels like a neutered stack-based language for the JVM in lisp's clothing.
- hga 11y agoHmmmm; I haven't worked with Clojure enough, and that after a decade break from programming and several from serious LISP programming, but ... while I like it, I don't find it very tasteful in many ways, including that very sort of way. It's a language I like to program in, but not one I think I'll ever fall in love with like mainline LISP and then Scheme. While I very much like the first class syntax for arrays, maps, and sets, there's a tremendous advantage to having one main or even exclusive built in composite data type, e.g. I hear that a strength of Lua, which I gather has been very successful. LISP's DNA, as well as Scheme's I'm pretty sure, was established in the days when that was lists.
- treerex 11y agoLisp != Common Lisp. The very thought that you can cut-n-paste code from one Lisp dialect to another is daft. The author obviously didn't take the time to acquaint themselves with the basics of Clojure. The lack of TCO in JVM hosted languages is besides the point. It is also possible for the Clojure compiler to do TCO in certain cases: Rich Hickey made the conscious decision to not do it.
- hga 11y agoIt is also possible for the Clojure compiler to do TCO in certain cases: Rich Hickey made the conscious decision to not do it. If I read your statement as Rich Hickey having the opportunity but passed on it, I'd reply that he did that with recur. Unless you mean silent TCO like e.g. Scheme does.
- treerex 11y agoYes, silent TCO. Isn't that what most people mean about TCO when applied to recursion?
- hga 11y agoPerhaps, but that doesn't mean it's accurate. TCO literally just says tail calls are optimized, and I personally see an advantage to making your intent to make a TCO-ed tail call explicit, so like in Clojure you get a compilation error rather than blowing your stack at runtime. It certainly looks more elegant to do it silently, but Clojure doesn't strive for that sort of elegance.
- dkersten 11y agoTCO is broader than what recur does. Recur only optimises recursive tail-calls to the function you are in, TCO generally implies that any tail-calls (recursive or not) can be optimised (including mutually recursive calls, for which clojure made the trampoline function or just calling one function at the tail of another). Personally, I like clojure's approach as IMHO recur makes intent clear, but recur is a subset of what TCO optimises in other languages.
- kzhahou 11y agoI'm not a clojure developer, but I'm intrigued. I was gonna post the question "Why should I learn clojure?" but that's easy to google for and get good articles. I was then gonna post "What's a good book for learning Clojure?" but I can get that on Quora. So my real question is: what can I read about Clojure that will get me up to speed on its unique awesomeness? I don't need a tutorial that shows me how to add two ints or invoke a function. Show me the good stuff!
- brudgers 11y agoIn my opinion, Halloway's Programming Clojure is a good beginner's book because it hits the right mix of Clojure newbie with experience programming. Among free online tutorials, Aphyr's Clojure from the Ground Up is my recommendation. A little deeper into the language, Fogus's Joy of Clojure hits more technical topics.
- zorked 11y agoI would recommend something that seems weird but isn't: start with Joy of Clojure just to get a sample of what the language really is about, and if you like what you see go back to one of the introductory books and learn it.
- lispnewb 11y agoI'd recommend against Programming Clojure and instead recommend Clojure Programming from O'Reilly. I read a fair amount of both and the former explains the subject matter too superficially IMHO. +1 for Joy of Clojure which is a nice read in parallel.
- brudgers 11y agoThat was my impression of Programming Clojure at first, it seemed rather lightweight. What I came to believe is that Halloway's presentation focuses on simplicity but achieves reasonable depth. [1] The high level of accessibility reflects Halloway's background in the training industry and the his expertise in Clojure. His book is efficient in the same manner as Clojure. Joy of Clojure is a good book. It goes deeper while assuming more of the reader. The technical detail is useful and interesting, but for me, the narrative seems a bit less cohesive [disclaimer: I have the first edition, not the second]. I haven't read Clojure Programming. [1]: Edit. For example the running exercise is porting a Java build system to Clojure. That's full on software engineering, not a let's-pretend.
- wtbob 11y agoThe Common Lisp version he has is pretty poor; I think this is more idiomatic, and is two lines as well: (defun build (list1 list2) (loop for x in list1 append (loop for y in list2 collect `(,x ,y)))) or: (defun build (list1 list2) (mapcan (lambda (x) (mapcar (lambda (y) `(,x ,y)) list2)) list1)) Still more verbose, of course, but not at all bad.
- lispm 11y agoTo make a statement about Common Lisp from simple tutorial code is a bit much. If one looks at music software in Common Lisp, the code is on a higher level than building lists like that. > Secondly, this code tries to handle lists in the classic Lisp way, with recursion, and that's not what you typically do in Clojure. Neither is it done in Common Lisp. > Those 7 lines of Common Lisp compress to 2 lines of Clojure The actual difference is that Common Lisp uses LOOP and not FOR, and that LOOP needs two nested LOOP forms: CL-USER 11 > (defun build (list1 list2) (loop for e1 in list1 append (loop for e2 in list2 collect (list e1 e2)))) BUILD CL-USER 12 > (build '(1 2) '(a b)) ((1 A) (1 B) (2 A) (2 B)) Two iteration forms in a LOOP don't nest, but provide iteration bindings similar to LET* and LET: (loop for i in '(1 2 3) for j = (expt i 2) collect (list i j)) and (loop for i in '(1 2 3) and j in '(1 4 9) collect (list i j)) Common Lisp's ITERATE macro also needs nested forms, but slightly improved over LOOP: ITER-USER 26 > (iterate outer (for i in '(1 2)) (iterate (for j in '(a b)) (in outer (collect (list i j))))) ((1 A) (1 B) (2 A) (2 B)) The author of Clojure knows these differences very well, since he was a heavy Common Lisp user for a few years. > But, at the same time, if you're looking to translate stuff from other Lisps into Clojure, it's not going to be just copying and pasting. Beyond inconsequential, dialect-level differences like defn vs. defun, there are deeper differences which steepen the learning curve a little. That's true. Clojure is not a Lisp, but partially derived from it, with many other influences from Haskell and other languages. It's mostly incompatible to Lisp: software can't be shared or copied, it needs to be complete rewritten. Thus basic Lisp literature is only of use when it's about features which got copied. For example the book 'On Lisp' might help to understand macros in Lisp and Clojure, whereas books like 'Practical Common Lisp' or Norvig's PAIP aren't very useful for Clojure programmers.
- taeric 11y agoI'm not sure calling "Clojure not a lisp, but partially derived from it" really makes sense. That code from other lisp implementations needs rewritten to be ported, is really not that much different from how many other lisp implementations were to each other. CL and Scheme did a lot to unify things, such that any scheme should run another scheme's code. Same for any CL implementation. But, for example, running emacs lisp in any other lisp just isn't going to work.
- PuercoPop 11y agoHis build implementation uses tail recursion which is not preferred in CL. One can build in a similar way that as shown at the end. (defun build (list-1 list-2) (loop :for elem-1 :in list-1 :for elem-2 :in list-2 :collect (list elem-1 elem-2))) A much bigger difference imho between Clojure and Common Lisp is that the former is built on abstractions, conj, while the latter is built on concrete cons cells. There are more other significant differences but I don't want to flame/argue.
- EdwardDiego 11y agoIs it even recursing in the tail position though? Reading the code it looks to me like the recursion result is used as an argument to a subsequent function call (append).
- rachbowyer 11y agoSpot on! The code fails as the author has used nil? rather than empty? The recursive call is not from the tail position so tail call optimisation is not possible in any language.
- PuercoPop 11y ago> tail call optimisation is not possible in any language. Afaik it is possible in racket. Read the note at the end here: http://matt.might.net/articles/lexers-in-racket/ http://matt.might.net/articles/lexers-in-racket/
- PuercoPop 11y agoYou are right, should have said recursion instead.
- raspasov 11y agoFrom my perspective, the three distinguishing features of Clojure from other languages/Lisps in general are: 1. It's a lisp, which means homoiconic, i.e. build software just like Lego (different from non-lisps) 2. Immutability and purity in the core library + sane, managed concurrency via atoms, refs, core.async, i.e. write serious multithreaded code without putting your hair on fire; helps on the front-end via ClojureScript as well (different from most other Lisps) 3. The JVM, which means very good performance in the general case + build once, run everyone + a lot of libraries
- Grue3 11y agoI might be wrong, but the biggest difference is that lists are immutable in Clojure which makes some data structures which are based on shared conses difficult to construct, or less efficient.
- devin 11y agoThe conclusion of this article is: "Clojure is not the same as Common Lisp." Well, yeah, it's not the exact same language. How is this surprising? I wish the author would have spent a little bit more time researching. The comparison is just plain sloppy. Why is Clojure's `recur` not mentioned, for instance? This is documentation that's not that difficult to find by googling, and is mentioned in (I believe) every Clojure book available.
- jwdunne 11y agoTo make a point, I thought about 'recur' and I haven't written a single line of Clojure in my life. In comparsion, CL's looping constructs were not mentioned, which crop up often enough.
- nemoniac 11y agoThe reason why some people say that Clojure isn't a lisp is that some of its design decisions such as those mentioned in the blog posting detract from the essence of lispiness. It goes too far to say that it's not a lisp but it's certainly less lispy than Common Lisp or Scheme.
- thom 11y agoThis doesn't seem like a useful comparison, analysis or conclusion. Clojure is philosophically a Lisp, but as others have pointed out, it isn't usefully the same language as any other Lisp that preceded it or exists today. If this particular example is useful to people who might otherwise think they could reuse existing Lisp code, then I suppose it's saved some effort, but that just seems like a straw man. Either way, if you get the terminal condition of a recursive function wrong, you will blow the stack. It's true Clojure isn't built to do TCO, but that's a non-sequitur. The idea that you don't typically use recursion in Clojure is ridiculous - for is a macro, it's syntactic sugar over Clojure's recursion primitives, loop and recur. Even if you didn't want to use that, change the nil? check to empty? and it works. I'm not going to claim that Clojure's list/sequence semantics are necessarily the cleanest (the empty _sequence_ in Clojure is nil, an empty list/vector/set etc is not), but they're not hard to learn if you make any effort at all. Again, your existing Lisp experience may or may not help you understand Clojure's data structures, but I am surprised that this is a surprise to anybody.
- copsarebastards 11y agoYeah, I got to the following line and stopped reading: > First, this code assumes that (if (null list1)) in Common Lisp will be equivalent to (if (nil? list1)) in Clojure, but Clojure doesn't consider an empty list to have a nil value. That sentence makes it very clear the writer either hasn't been arsed to read through even one Clojure tutorial or is being intentionally dense. If they have an educated opinion, great, but I have no patience for this level of ignorance, especially if it's feigned ignorance.
- nextos 11y agoIn practice recursion is not something that you use that often I think. I've always seen it as a low level construct. The filter/map/reduce idiom or, alternatively, list comprehensions get you a long way. Not to say recursion is not important. Without it lambda calculus is not Turing complete. But Clojure does provide explicit TCO. Saying this is a big difference is excessive IMHO.
- thom 11y ago
- dunkelheit 11y agoWhat is the name for illustrating the point with deliberately unrelated example? His clojure code fails because he did not swap nil? for empty? (and he even states this in the article) but he then uses it to illustrate the absence of TCO in clojure.
- TazeTSchnitzel 11y agonil? is a pretty big thing. Is Clojure even a Lisp if nil is not the empty list? Edit: Jesus Christ, Clojure doesn't even have conses. Why the hell do people call this a "Lisp"? > (= () nil) false > (('a . 'b) . ('a . 'b)) java.lang.RuntimeException: Unable to resolve symbol: . in this context > (cons 'a 'b) java.lang.IllegalArgumentException: Don't know how to create ISeq from: clojure.lang.Symbol
- dewitt 11y agoSure, it does have cons, but your second argument wasn't a sequence. Try: > (cons 'a ['b]) (a b) > (doc cons) ------------------------- clojure.core/cons ([x seq]) Returns a new seq where x is the first element and seq is the rest.
- tokenrove 11y agoIn other Lisps, a cons can be an arbitrary pair.
- TazeTSchnitzel 11y agoThat's not a cons, that's a linked list. As tokenrove mentions, a cons in a Lisp is just a tuple (`cons` is the name of the type and the function constructing it). It can contain any two values.