Shortand clojure syntax for properties on hashmaps


#1

Hello,

One of the things I like the most about ECS6 is the short syntax for creating object with variables as values on properties with the same name. It is better explained with code:

const x = 5, y = 55, z = 78
thing = {x,y,z}
// { x: 5, y: 55, z: 78 }

Does the same thing exist for clojure ?


#2

There is defrecord - see e.g. https://clojuredocs.org/clojure.core/defrecord
But I’m not sure it creates objects :slight_smile:


#3

Continuing the discussion from Shortand clojure syntax for properties on hashmaps:

hmm, as we are using a Lisp, you could use a macro like for example:

(defmacro nmap [& xs] 
    (cons 'hash-map 
              (interleave (map (comp keyword name) xs) xs)))

and then use it like

(let [a 1, b 2, c 3] 
  (nmap a b c))

yielding

{:c 3, :b 2, :a 1}

#4

Cool, exactly the functionality that I was looking for.
Are all those macros? I guess that at least interleave is not. Now that you make me think about macros I would prefer a macro that just spits about the final construction instead of a function that builds it


#5

I just tried it on the CLJS repl, and It gives me the following error:
Doesn't support name: 3
Is it supposed to work on clojurescript ? Maybe I should have specified the CLJS target before


#6

no, all of them (cons, interleave and map) are normal clojure functions. IMHO one should stay away from Macros unless they are really required (like here). I needed to write a macro here to transform the syntax.

Macros get the written expression at compile time (here: (nmap a b c) a list with four symbols in it) and spits out a different list instead (here: (hash-map :a 1 :b 2 :c 3)). This is what then is seen by the compiler. This is why you perceive to have as a result “the final construction instead of a function that builds it”.


#7

Hmmm, I have never touched macros in clojurescript, my experience is quite limited here, maybe someone else knows?!


#8

Hello @jochenriekhof
After executing macroexpand on your macro then I saw that it actually produces the expected compiled output. Now I understand that the function at the body of the macro is executed at compile time and it produces the final code that will be executed at runtime. My misunderstanding comes from the fact that I though the body of the macro will be just “pasted” on every appearance of the macro, but what it actually does is execute that function to produce the the final code.

Regarding the error I was reporting I checked the implementation of name and it is very laky. It only supports strings, and it does not return the name of the variable but just the string, which is absolutely useless in my opinion. Here is the implementation of name on cljs

(defn name
  [x]
  (if (implements? INamed x)
    (-name ^not-native x)
    (if (string? x)
      x
      (throw (js/Error. (str "Doesn't support name: " x))))))

#9

It support more then strings, it supports all objects that implement INamed, which a symbol would, as symbols are used as variable names.

Your problem is that in ClojureScript you can not define macros at the REPL, or within the same namespace as your other code. If you do, the macro will actually behave as a function, which is why you are getting this error, because instead of name getting the symbol, it gets the value of the variable, which is 3, and name does not work on numbers.

Try creating a foo.cljc file (you can not declare macros in .cljs files, only .clj and .cljc files can have ClojureScript macros), and in that file define the macro like:

(ns foo)

(defmacro nmap [& xs] 
  (cons 'hash-map 
        (interleave (map (comp keyword name) xs) xs)))

Now in your .cljs files, or at the REPL, you need to use require-macros such as:

(require-macros '[foo :refer [nmap]])

And now if you try it it will work:

cljs.user=> (let [a 1 b 2 c 3] (nmap a b c))
{:c 3, :b 2, :a 1}

This article has a bit of a hacky workaround that you can use to declare macros in a self hosted Clojurescript REPL, which can be useful when iterating on writing a macro: http://blog.fikesfarm.com/posts/2015-09-07-messing-with-macros-at-the-repl.html


#10

great info, thanx for sharing!


#11

Thank you very much ! I would like to give you more likes !

That information is very valuable. Right now I’m using shadow-cljs to compile my code, so I don’t care about not being able to declare macros on cljs.

Thanks!

PS: If I want to propose something like this getting into the core of cljs, where should I go ? I really think that cljs needs to have all the functionalities ES6 has to be appealing.


#12

You could try bringing things up on the ClojureScript mailing test, but just to warn you, I wouldn’t get my hopes up too much.

It is very unlikely that ClojureScript will add this unless Clojure adds it first, and it is very unlikely that Clojure will add this because Clojure is deliberately conservative in what it adds at this point.

You are of course welcome to bring it up anyway, maybe some very interesting discussion will result, but my guess is that you will most likely be told to provide these conveniences as a library instead, which is what I would also recommend.


#13

I think the fact that you can provide this functionality in 3 lines of code means that it will never be added in Clojure…


#14

And just because ES6 has a feature, doesn’t mean that it’s a good feature – nor that it would be a desirable feature for Clojure(Script). Languages are different for a reason. A feature in Language X might make no sense at in Language Y.


#15

You’re just saying that it may not be desirable. Do you have any concrete opinion about it?


#16

Unlike js, clojure hash-maps map any value to any value and all the clojure.core map constructors allow the creation of maps mapping any value to any value. It would be pretty inconsistent to have an nmap that requires not only symbol keys but only vars that point to a value. This seems to conflate symbols and vars, things which clojure.core tries to keep a distinction between.

I could go on but I think this introduces a ton of inconsistencies for almost no gain (it’s not really obvious where this syntactic sugar will ever even save any keystrokes!).

I don’t mean to be harsh but I think this functionality not only doesn’t belong in clojure.core, but I think this nmap is non-idiomatic clojure code and I’m having a hard time imagining where it would make sense to ever have in any clojure code.

I think it may be that this sugar is justifiable in JS due to the ways objects are commonly used there, but I think you’ll find that once you get feel for the idioms of clojure you’ll never want this.


#17

Ok,

I’ll trust you guys about this one and I’ll try to advance in Clojure without this functionality.
However if I find myself writing tons of :username username lines I’ll report it back :smile:


#18

My concrete opinion is: that ES6 feature is terrible, as it overloads the literal syntax for two completely different things. I agree with @jjttjj and his response is exactly the sort of thing I’d expect from most seasoned Clojure devs and, esp. from the Clojure/core team, about this sort of feature.

As you say, you’re new to Clojure so right now you’re seeing a lot of differences from your “home” language without having a history of why things are different, or why your favorite feature X seems to be not only missing from Clojure but also discouraged even from being added. I’ve changed languages a lot over my 35 or so year career, and pretty much every time I pick up a new language, I’m left wondering why feature Y from the previous language is “missing”… but after a while, I get used to the fact that it uses feature Z instead (and that’s more idiomatic or easier to use), or just simply doesn’t need feature X at all.

The more languages you learn, the more you’ll see this and, in my opinion, you more you’ll come to appreciate the differences in idioms (and the differences in features) between languages.

I’m reminded of an experience, many years ago… I worked for a company that specialized in static source code analysis tools (to perform QA based on coding guidelines, bug detection through source code analysis, and software metrics calculations). The company started with a FORTRAN analysis tool, then they introduced a C analyzer (and, later, a C++ analyzer). We would go into companies, run our tooling on a large sample of their source code, and then present our findings – often uncovering bugs that their teams had been trying to track down for months. Quite a few C shops that we went into had unusual code bases (according to the metrics) and we quickly realized these were former FORTRAN shops. Once we’d spotted the pattern, it became obvious as we’d go into new C shops whether they too had migrated from FORTRAN. Their C code was particularly unidiomatic in certain ways: we dubbed it C-TRAN, because their engineers were trying to write C in the style of FORTRAN, rather than adapting to the new idioms and features.


#19

Thank you for your detailed response and for sharing your experience. Your words reminded me to a history mentioned on eloquent javascript (which I’m unable to find) about someone blaming one language until he understood the differences.

Regards


#20

The truth is, Clojure and ClojureScript are benevolent dictator designed languages. A feature almost never gets added unless Rich Hickey decides its a good idea.

But since Rich Hickey has pretty good intuition and insight into programming languages and Clojure’s design, it’s often for the best.

To counter act the fact that he might say no to adding a feature people want, he’s given us macros and tagged literals.

So I think its totally fine if you start creating yourself a little convenience library of things that you would have added to clojure.core, but is missing. A lot of people do that. Though I’d recommend being careful not to add too many things at first, be sure it really adds value.

Personally, I think if you have an nmap macro as above, its fine. Though I might give it a better name. That said, I’m not sure it saves a lot of keystrokes, since declaring the let binding is longer to type then just {:a "a" :b "b"}.

I can’t think of a situation where you’d call nmap more then once within that same binding, so I don’t know. But if it’s something you come accross a lot, and you’ve got good reasons for your specific use cases, then there’s nothing wrong with using that macro.

As for adding it to clojure.core, I think the truth at this point is that Clojure is already a pretty big language, with unfamiliar syntax and lots of DSLs to learn. That’s why I think new syntax is something only added if it’s absolutly necessary at this point.