I was looking for reader macros for recognizing {} as hash tables and [] as vectors. To me, that is one of the biggest innovations in Clojure over Lisps that came before, the expansion of the concept of S-expressions.
But to be honest, I think this {} stuff is an abuse of the hash structure. People are really looking to make quick anonymous classes/structs; you see this all the time in the perl world, and now we get to see this abuse promoted and propagated in Clojure. In that case, {} should really translate to a MOP-generated anonymous class.
Also, you are stuck with the decision some developer made for your hash's efficiency trade-offs. In my reader macro, I left things at make-hash-table defaults, but that might not be appropriate for your situation. Do you want to be able to read quickly from your hash in exchange for slow writes, or do you want to be able to write quickly to the hash in exchange for slow reads?
On another note, the [] vector reader macro is much easier to create. I just wanted to prove to myself that I could write {}. But this additional syntax is annoying. In practice when programming Lisp, it is easy to slap in a bunch of ))))) until the right alternate parentheses highlights. Now I have to remember to slap in ] and } at the right spots, leaving me with )]}]]}})) noise. Maybe that is why CL's in-place vector syntax is #().
Anyway, I would hesitate to call {} and [] an innovation.
1.[], {}, #{} are not the innovation.
2. The innovation is that they are on the same level as lists. All the cool list stuff you can do with lists you can do with all of the structures and you can add more yourself if you want. That allows to write stuff really generic.
3. If you don't like them use (vec ), (map ) and so on. Only the most imported ones have syntax there are many data structures.
P.S. Do you really finish up all your parans yourself???
This discussion came up recently over the usage of Christophe Rhodes' generic sequence protocol extension to CL. Pascal Costanza makes a good point about the appropriateness of having an overly abstract interface tower, and I agree with him.
> The innovation is that they are on the same level as lists.
I have heard this point when comparing to Scheme (Racket). I did not understand how it applied to CL. I never found an issue dealing with the different datatypes (other than initially learning what API worked with what and what was the proper generic stuff to use).
> P.S. Do you really finish up all your parans yourself???
Yes. I found paredit and its derivatives really annoying, particularly when dealing with strings and other entities. I am not always closing entire functions, so the autoclose feature was also useless to me. I can see how it can both help and get in the way when dealing with [] and {}. I do not have a strong objection to that noise. See below for my real objection to the {} syntax; it is less about the {} and more about the reason for using it.
"People are really looking to make quick anonymous classes/structs"
What wrong with that? If you have the need for an anonymous map, then you use a map. If you have the need for a named map then you use a record. Clojure gives you both options.
"Also, you are stuck with the decision some developer made for your hash's efficiency trade-offs."
In that case you just use a different type of hash map. In Clojure you have the choice of the literal syntax (which by the way manages efficiency concerns on your behalf), the hash-map, array-map, or sorted-map functions, or records, or Java maps for interop, or you can implement your own if none of those options work.
If you mean, "What is wrong with anonymous classes?", I have no answer. I do not mind the idea, so long as I can maintain the integrity of my data's interfaces.
If you mean, "What is wrong with implementing anonymous classes as hash maps?", then I point you towards issues of efficiency in memory and speed. Hash maps are intended to make key look-up quick for a variable set of not 100% known keys. If all you want is known key-value pairs, then you have some options that do the trick much more efficiently (examples --):
- structs (key is compiled out to be a quick pointer addition)
- classes (on top of the struct, you now have some abstraction capabilities that will help out future maintenance issues)
If you do not quite know what you want in the structure, but you have a small number of keys, an associative array with a decent accessor API is all you need. The few key comparisons will generally be faster than running them through a typical hashing algorithm.
When you provide a construct like this:
{ :foo 1 :bar 2 }
you are implying that you have a known set of keys and values. With a hash map, I would expect that someone would later add more key-value pairs, but in practice, code that starts with this initialization pattern treats it like a class rather than a hash, just modifying the values of the :foo and :bar attributes.
I have also seen instances where developers would -- because it was easy to do with these hash maps -- slap in another key-value pair in some subfunction and then suffer interface issues down the road when they assume its presence in the wrong places. Something a little stricter would have at least given them a warning.
So just make it a struct -- anonymous if you need -- at minimum. That is the most generic, all-purpose form that could benefit from this kind of {} syntax.
> which by the way manages efficiency concerns on your behalf
Can you elaborate on this? If this means that there is a magical conversion to a stricter struct underneath the hood, then I have no place to complain about {} in Clojure's case. I have not encountered this notion before (other than as faith).
Clojure is built on abstractions. Most operations in clojure source code convert to calls to java interfaces. For example, a simple function call (map foo bar) compiles to a call to .invoke() method on map, a java object that implements clojure.lang.IFn. Similarly, all of clojure's datastructure operations boil down to calls to objects implementing the java interfaces clojure.lang.IPersistentCollection, c.l.IPersistentMap, c.l.IPersistentSet, etc.
To add a new datastructure, simply create a new class that implements the appropriate interfaces, and you'll get literal syntax support.
To answer your question, Clojure records are named classes that implement IPersistentMap so they're a drop-in replacement for "normal" maps, that also implement an additional interface so the compiler can generate java field lookups to access items without the cost of a hash lookup.
Clojure records are named classes that implement IPersistentMap so they're a drop-in replacement for "normal" maps, that also implement an additional interface so the compiler can generate java field lookups to access items without the cost of a hash lookup.
Awesome! Thank you for that. I saw people referring to it as a hash when you point out it is not, so I was confused. I sit corrected as well as happier with the Clojure design.
>> which by the way manages efficiency concerns on your behalf
> Can you elaborate on this?
Sure thing. As a start the form `{:a 1, :b 2}` would start as an array-map because it's really fast to populate while the access efficiency is reasonable. At some point during the use of that map it will cross some critical threshold and become a hash-map providing (effectively) constant time lookup while still maintaining persistence. Additionally, behind the scenes Clojure uses mutable versions of the map for efficiency while again maintaining persistence.
Clojure provides different types of maps for use in different scenarios, but manages implementation efficiencies for you.
"Anyway, I would hesitate to call {} and [] an innovation."
What made it an "innovation" was the decision to close off access to reader macros from the general public. It sounds like a nice idea to allow people to define the behavior of {}, [], etc. however they like. But the effect is splintering the language even further with fewer reusable idioms.
The Clojure idiom of always using [] for any place you have let-like destructuring makes it easier to quickly visually parse code, for example. The abbreviation for anonymous functions wouldn't be part of the core language if the corresponding reader macro was not claimed by Benevolent Dictator Hickey.
This decision may or may appeal to you, personally, but I think it is one of the things that has quickly allowed Clojure to quickly gain popularity relative to other Lisps.
I'm not sure anyone programming common lisp would agree with you. There's been no "splintering" of the language due to reader macro usage, and the reader macros that CL already provides are not exactly few. Here's a good post describing the what why and how of CL reader macros:
I would also hesitate to name any particular language feature of Clojure as having an impact on its popularity. I think it's due more to the current zeitgeist of "Lisp is cool, but I program in something else" and "concurrency concurrency concurrency."
I think it's clear which Clojure feature was/is critical to its popularity: the JVM and interop with Java. This isn't a language feature, of course, but it is a marketing master stroke: it removes the principal objection to Lisp, gives people a cognitive-dissonance-free way to try it, positions the language nicely (no small deal) and soothes the savage enterprise. This dynamic is nearly entirely one of perception -- one could argue that technically it's a non-solution to a non-problem -- but perception is a life-and-death factor. It doesn't matter more than reality in the long run (if Clojure weren't a good language, people wouldn't keep using it) but it actually does in the short run.
The problem with that thesis is it has completely failed for Scheme (there are multiple Scheme implementations for the JVM, some are almost a decade old), and arguably for Common Lisp (ABCL came out at approximately the same time as Clojure, and although it's fairly popular as a Common Lisp implementation, it hasn't lead to major growth in adoption).
The reason I think the "it's Lisp but not really a Lisp" is a major factor is the example offered by newLISP. Its biggest thing seems to be "all this macro and lisp-2 and lexical scoping stuff is nonsense, this is not scheme or common lisp, this is a simple language," and despite this paradoxical assertion it has developed a somewhat large user community (a lot larger than Arc's, for example).
The JVM is much less of a factor than many people suggest. Another good example in support of this and the "concurrency" thesis is node.js - it's not the first JS web server (by far), and it's not the first NIO-based web server, but it does combine the two at the right time.
> Also, you are stuck with the decision some developer made for your hash's efficiency trade-offs.
PersistentHashMaps are not slow. If you think they are, you can provide a custom defrecord with the same fields to the functions of the API and get the all perf benefits without breakage.
> leaving me with )]}]]}})) noise
Guess you never heard of paredit.
> Anyway, I would hesitate to call {} and [] an innovation.
That is also an excellent choice. But I believe they are related, or at least complement each other, in the sense that all of the data structure literals implement seq. I do not think they would work as well as an extension for S-expressions, otherwise.