[HN Gopher] Clojure from a Schemer's Perspective
___________________________________________________________________
Clojure from a Schemer's Perspective
Author : todsacerdoti
Score : 128 points
Date : 2021-03-05 13:01 UTC (10 hours ago)
(HTM) web link (www.more-magic.net)
(TXT) w3m dump (www.more-magic.net)
| lukashrb wrote:
| I really enjoyed the read and think this is a really good, fair
| and down to earth perspective. Also reminds me how much there is
| to explore for me :)
| bitwize wrote:
| This is a really well balanced look at Clojure, much more
| charitable than I would give it. :) I've always considered
| Clojure's greatest strength to be its JVM integration, and that
| doesn't really give you anything Scheme (Per Bothner's Kawa) or
| CL (ABCL) don't. Otherwise Clojure is just weird enough to
| frustrate me without a compelling must-migrate payoff to the
| weirdness.
|
| But again, that's just me.
| karmakaze wrote:
| Nice writeup and a good summary/tour.
|
| The biggest advantage of Clojure over more succinct Lisps is that
| the extra verbage and built-ins promote a larger shared base and
| patterns on how things should be done. This is less likely to
| turn each project into its own language. Clojure skills are much
| more likely to transfer between companies.
|
| The other thing about being in the JVM ecosystem is that it
| promotes sharing so there's less reinventing the parts of the
| wheel that you need for 'this' project that's unsuitable for
| other use-cases.
| claytn wrote:
| I've been writing side projects in clojure for over a year now,
| and really enjoyed this assessment/review. The line, "At those
| moments, you're basically programming Java with parentheses" hit
| home for me. I'll always brag about the interop capabilities of
| clojure, but I will avoid using it as much as possible.
| elwell wrote:
| Anecdotally, I've been using Clojure professionally for about
| seven years and only very rarely need to interop with Java. Of
| course it depends on what you're trying to accomplish. On the
| other hand, interop with JS from ClojureScript is very common;
| shadow-cljs has made that experience more inviting.
| ARandomerDude wrote:
| Huge Clojure fan here, been using it professionally for 9 years.
|
| This is a good article, highlighting a couple of the warts, which
| I think is a missing element for most people who _like_ a
| language.
|
| I completely agree with the nil punning observation. One of the
| issues pointed out in the article that resonated most with me is
| functions in the standard library usually (but not always) nil
| pun. You get used to nil, so when the standard library does throw
| a runtime exception, you're surprised. For example:
|
| (:foo nil) => nil
|
| (int nil) => exception
|
| Overall great article.
| samatman wrote:
| Is this a case of Java leaking through again?
|
| As in, Clojure uses int rather than Integer, so it simply _can
| 't_ be nil aka null?
| ARandomerDude wrote:
| Yes, I think so. Given how other functions work though, I
| would expect nil in this case, since Clojure is a language,
| and not just a Java library.
| ballpark wrote:
| It can be indicative of the goodness of a language when you use
| it for 9 years and are still a huge fan. I'm in the same boat.
| casion wrote:
| If you want to nil pun like that, it's not difficult to do it
| where necessary.
|
| (some-> nil :foo)
|
| (some-> nil int)
|
| Of course this fails if your nil is actually a string, but
| that's a different issue.
| [deleted]
| kgwxd wrote:
| > You might just be using a completely different underlying data
| structure than expected, depending on which operations you've
| performed.
|
| I haven't had the pleasure of using Clojure in production, but
| I'm somewhat obsessed with the language and follow it fairly
| closely. Someone on r/Clojure[1] mentioned Specter[2] with a link
| to this video[3]. One of the first things the video covers is how
| the library deals with this concern.
|
| [1]
| https://www.reddit.com/r/Clojure/comments/loz77v/just_came_a...
|
| [2] https://github.com/redplanetlabs/specter
|
| [3] https://www.youtube.com/watch?v=rh5J4vacG98
| loevborg wrote:
| > vectors can easily be extended (on either end!) functionally
|
| It's true that you can extend a vector on either end
| user=> (def xs [2 3]) #'user/xs user=> (conj xs
| 4) [2 3 4] user=> (into [1] xs) [1 2 3]
|
| but only the first operation will be O(1). The second operation
| is O(n) and should be avoided in hot loops.
| hawkice wrote:
| For future readers: if you want to do this, I would first just
| accumulate into the seq backwards and reverse it into a vec. If
| that wasn't enough, use transients. Not _that_ different from
| other languages -- if you want to do something, and the obvious
| way is slow, (reverse (accumulate (reverse (map reverse
| vecs)))) is the obvious workaround for asymmetric performance
| (although I've not had this issue personally).
| loevborg wrote:
| Not sure I understand your comment. Is `accumulate` a scheme
| function?
|
| My comment was intended to say that Clojure vectors have some
| operations that are performant and some that aren't. It is
| part of Clojure's philosophy to encourage the operations that
| are natural for the respective data type. `conj` appends to
| the end when applied to a vector but inserts at the head when
| applied to a list.
|
| So you want to pick the appropriate data structure for the
| job and make sure only natural operations are used. For a
| vector appending is natural whereas inserting at the head is
| not.
| fnordsensei wrote:
| Interestingly, im-rs for Rust offers better than linear
| performance when inserting at either end:
| https://docs.rs/im/15.0.0/im/
| drcode wrote:
| 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).
| xiaq wrote:
| > 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.
|
| For mutual recursions there is always trampoline
| (https://clojuredocs.org/clojure.core/trampoline), FWIW.
| drcode wrote:
| Yes, 'recur is most commonly used in conjunction with 'loop,
| thanks for clarifying.
| abecedarius wrote:
| 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.
| didibus wrote:
| 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.
| sjamaan wrote:
| > 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.
| brabel wrote:
| 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.
| phyrex wrote:
| That's Clojure, not the JVM. In certain configurations it
| can unfortunately take that long. It's getting better
| though.
| dustingetz wrote:
| 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.
| didibus wrote:
| > 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.
| fiddlerwoaroof wrote:
| 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).
| samatman wrote:
| Definitely interested!
|
| 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.
| xscott wrote:
| > [...] 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.
| sometimesweenie wrote:
| > The -> macro is a clever hack which allows you to "thread"
| values through expressions. It implicitly adds the value as the
| first argument to the first forms, the result of that form as the
| first argument for the next, etc. Because the core library is
| quite well-designed, this works 90% of the time. Then the other
| 10% you need ->> which does the same but adds the implicit
| argument at the end of the forms. And that's not always enough
| either, so they decided to add a generic version called as->
| which binds the value to a name so you can put it at any place in
| the forms. These macros also don't compose well.
|
| This is the biggest criticism of clojure I agree with the author
| on. Trying to compose threading macros is extremely annoying even
| though it's not that difficult to refactor. Somewhere in the
| thread, you'll want to put the return value of the previous
| function in a different argument place. I am not sure the
| reasoning behind designing the macros this way. With how well the
| language is designed, this annoyance has always seemed out of
| place and somewhat of an after thought. I made my own macro [0]
| as an experiment to circumvent this annoyance... which was an
| interesting experience (both writing and using the macro). Still,
| I don't think it's a good enough solution so I'm still trying to
| think of better ways to implement thread macros in clojure
| because I love the idea.
|
| [0] https://github.com/cj-price/pipe.dream
| Straw wrote:
| I've found -<> and -<>> very useful- they by default put things
| in first and last place respectively, but let you override with
| <>:
|
| (-<> x (foo bar) (map baz <>))
|
| Found here: https://github.com/rplevy/swiss-arrows
| simongray wrote:
| In that case, why not go all the way and use the the exciting
| new ->->><?as->cond->! macro demonstrated in Every Clojure
| Talk Ever? ;-)
|
| https://youtu.be/jlPaby7suOc
| jimbokun wrote:
| That was amazing.
|
| One of the best tech talks I've ever seen.
| th0ma5 wrote:
| I came to Clojure after Kawa and while I appreciate the
| simplicity of Kawa, the more advanced Java integration and build
| system in Clojure is really great.
| codemonkey-zeta wrote:
| Great article!
|
| IIRC the necessity of loop/recur for TCO is a result of being
| hosted on the JVM and not a direct design decision for Clojure.
|
| If I really wanted to play devil's advocate there is an argument
| to be made that needing to use loop/recur _could_ make an novice
| programmer more conscientious about tail calls (since it won 't
| work if the recur isn't actually in tail position), whereas in
| Scheme you get TCO for free, but might not be aware when you're
| getting it or not.
|
| In any case I kind of enjoy the loop/recur syntax since I don't
| have to name an intermediate tail-recursive function in a let
| (this could be naivety WRT writing scheme on my part, but I
| assume that's what usually happens).
|
| The fact loop/recur cannot be nested is something that I've run
| into a few times and is a fair criticism. I also find that I end
| up converting anonymous lambdas to fn all the time, and I'm not
| the biggest fan.
|
| IMO the most ergonomic lambda syntax I've worked with is Scala. I
| miss it in every other language that has lambdas.
| dkersten wrote:
| > IIRC the necessity of loop/recur for TCO is a result of being
| hosted on the JVM and not a direct design decision for Clojure.
|
| and from the article:
|
| > I really started noticing mistakes that make additional
| features appear necessary: for example, there's a special macro
| called loop to make tail recursive calls. This uses a keyword
| recur to call back into the loop.
|
| While it may be necessary due to the JVM, having them was also
| a conscious design choice and many people (myself included)
| actually prefer being explicit about TCO, because normally when
| its used, its not used as an optimization but as a required
| feature to make the semantics work correctly (eg a recursion-
| based loop), with "recur", the intent that it be tail-call
| "optimized" is clear and if it cannot be tail-call optimized,
| then its an error (recur won't compile if its in a non-tail
| position). So, I personally would use recur even if normal
| recursion did have TCO.
|
| Also, TCO is more general than "recur" and I believe having
| recur was also somewhat meant to make the semantics clear. For
| example, recur can only recurse to the closest loop or
| function. Proper TCO can also handle cases like mutual
| recursion between two or more functions (ie f1 calls f2 which
| calls f1. If both calls are tail calls, then TCO can be used
| here. This is actually what the JVM has problems with: making
| TCO work only for the cases that recur covers is easily
| possible on JVM, but having recur makes it clear -- at least to
| me -- that its a specific feature with specific semantics
| rather than general TCO).
|
| So anyway, OP calls it a mistake, I call it a feature that I
| like. There are a few other places where OP says something is
| unnecessary or wrong or a mistake, but me, someone who has used
| Common Lisp and Scheme, but never in earnest, but has been
| using Clojure for ten years, I find them to be nice features,
| or make the code look cleaner to my eyes, or are otherwise good
| design decisions to me.
| casion wrote:
| > For example, sometimes you need a let in a -> chain to have a
| temporary binding. That doesn't work because you can't randomly
| insert forms into let, so you have to split things up again.
|
| Aside from the rest of the article, I find this one weird.
|
| Of course you can do that, you just supply a function which has
| whatever you need in it. That's how you'd ideally be operating in
| Scheme as well.
|
| -> is effectively _function_ chaining, not "do a bunch of things
| in this block" chaining.
| madhadron wrote:
| > there's a special macro called loop to make tail recursive
| calls. This uses a keyword recur to call back into the loop.
|
| This isn't so much a language designer choice as a consequence of
| targeting the JVM. Tail recursion on the JVM has been a problem
| for decades.
| hencq wrote:
| That's not true. The JVM doesn't optimize tail calls, so, yes,
| that is a limitation. However, tail recursion (a subset of tail
| calls) is easy to detect and can be turned into iteration.
| That's exactly what loop/recur does. That syntax is a choice
| though. They could have decided to have named lets and make
| calls in tail position do the same thing as loop/recur does
| now. Instead they decided to make the syntax more explicit, so
| the tail recursion is more obvious.
| curryst wrote:
| > The underlying design of Clojure's data structures must be
| different. It needs to efficiently support functional updates;
| you don't want to fully copy a hash table or vector whenever you
| add a new entry.
|
| I've actually been curious about this myself. No one seems to
| scream that functional languages are slow, so I'm curious what's
| going on under the hood. Like the author, I would presume they
| don't fully copy the hash table every time I add an entry.
|
| Is Clojure smart enough to know that the previous hash map is now
| out of scope and can be GCed, so it just mutates the hash map in
| place under the hood? In the tiny amount of Clojure I've written,
| that seems possible. On the other hand, it makes performance much
| harder to reason about since mutating a variable that stays in
| scope after the mutation presumably would still trigger a copy.
| Do they just implement some kind of a "fall-through"? I.e. if I
| add a key to a hashmap, it creates a new empty hashmap, adds that
| value and a reference to the original hashmap. And then when I
| retrieve values, it searches the new empty hashmap, and then the
| "parent" hashmap if it isn't found?
|
| I'm curious because a lot of functional programming seems like
| the kind of thing that would thrash memory in a GCed language. It
| seems to an outsider like you can't do anything without
| allocating memory, which you're often done using almost
| immediately. It would seem like "y = x + 1" would always take
| more time than "x = x + 1" because of the allocation.
|
| Maybe GC is just a lot better than I think. Or maybe the
| functional style, with it's less complicated scoping, makes GC
| trivial enough that it offsets the additional allocations. Does
| anyone have any idea, or maybe have a link handy? I'm not a
| language designer, nor a Java programmer, so I fear the source
| code may not be terribly useful to me. I'm also a terrible
| Clojure programmer, if Clojure is written in Clojure these days
| (although I'd like to get better one day, it seems like a really
| fun language).
| wging wrote:
| Here's how the persistent vectors work (old article series, but
| good. hopefully someone will tell us if it's out of date):
| https://hypirion.com/musings/understanding-persistent-vector...
| newlisper wrote:
| _so I 'm curious what's going on under the hood_
|
| https://en.wikipedia.org/wiki/Persistent_data_structure
|
| _Maybe GC is just a lot better than I think._
|
| Yes, Clojure relies heavily on the JVM's GC being very good. It
| trashes it like there is no tomorrow and it would be a lot of
| work and extremely hard for an implementation of Clojure from
| scratch to match the performance of Clojure in the JVM because
| of how good the JVM's GC is.
|
| Having said that, I have move 3 projects (10k-20k LoC) to JS
| from Clojure and don't plan creating new ones in Clojure, the
| JS projects ended up being faster, shorter and easier to
| understand. Idiomatic Clojure is very slow, as soon as you want
| to squeeze any little performance out it your code base will
| get ugly really fast. Learning Clojure is nice for the insights
| but I'll will pick nodejs first any day for new projects. Even
| if I need the JVM, my first choice probably will be Kotlin and
| then Clojure.
|
| Of course there are many more downsides to using Clojure. No
| ecosystem, the cognitive overhead of doing interop with over-
| abstracted over-engineered Java libraries(because of no
| ecosystem ;)), the horrible startup times, the cultist
| community and the interop is really not that good, sometimes
| you have to write a Java wrapper over the Java lib to make it
| usable from Clojure. The benefits over JS are minimal but the
| overhead and downsides are too much. Worth learning it but not
| worth using it for real production projects.
| zdragnar wrote:
| Is there anything in particular that you had to give up to
| make the move (aside from thr jvm)? I am assuming that for
| node.js to be shorter and faster you avoided the bigger
| frameworks (next, typeorm etc) and had to stick pretty close
| to basic middleware ala express.
| thom wrote:
| I'm intrigued by what kind of code you're finding is shorter
| and faster in JS. I've had the opposite experience, even
| limiting Clojure to a single thread.
| l_t wrote:
| Yep -- under the hood, "immutability" in Clojure is implemented
| with data structures that provide pretty good performance by
| sharing sections of their immutable structure with each other.
|
| For example, if you have a vector of 100 items, and you
| "mutate" that by adding an item (actually creating a new
| vector), the language doesn't allocate a new 101-length vector.
| Instead, we can take advantage of the assumption of
| immutability to "share structure" between both vectors, and
| just allocate a new vector with two items (the new item, and a
| link to the old vector.) The same kind of idea can be used to
| share structure in associative data structures like hash-maps.
|
| I'm no expert on this, so my explanation is pretty anemic and
| probably somewhat wrong. If you're curious, the book "Purely
| Functional Data Structures" [0] covers these concepts in
| concrete detail.
|
| [0]: https://www.amazon.com/Purely-Functional-Data-Structures-
| Oka...
| lenkite wrote:
| If Clojure offered a direct compile-to-native option for any
| platform (desktop/mobile/WASM) with no JVM in between, I suspect
| it would explode in popularity.
| fnordsensei wrote:
| It does, via GraalVM. Babashka, for example, leverages this:
| https://github.com/babashka/babashka
___________________________________________________________________
(page generated 2021-03-05 23:02 UTC)