5

tl;dr

How can I derive a keyword from a number in ClojureScript:

(keyword 22)
;;=> :22 but in fact returns nil.

Background

In my ClojureScript/Hoplon application I make HTTP requests via cljs-http. Parts of the response I receive look like this:

{:companies
  {:22 {:description ... } ; A company.
   :64 {:description ... }
   ...                    }
{:offers
  [{:description ... } ; An offer.
   {:description ... }
   ...                ]

Each offer within the vector behind :offers has a :companyId which represents a key in :companies. As soon as I receive the response, I reset! a cell (similar to an atom) query.

Now, I'd like to iterate over each offer and call a function offer-tpl that creates the corresponding HTML. In order to do so, offer-tpl needs the offer itself as well as the related company:

(for [offer (:offers @query)]
  (offer-tpl offer (get-in @query [:companies (keyword (:companyId offer))]))))))

Despite the fact that this surely can be done more elegant (suggestions very appreciated), the get-in doesn't work. (:companyId offer) returns a number (e.g. 22) but (keyword (:companyId offer)) returns nil. Calling (keyword (str (:companyId offer))) does the trick, but aren't there any other ways to do this?

beatngu13
  • 7,201
  • 6
  • 37
  • 66
  • Is there the special reason for indexing companies with keywords, not numbers? `{:companies {22 {:description ...} 23 {:description ...}}}` ? Where do you get this response from? can you change the format there? – leetwinski Aug 28 '16 at 14:37
  • @leetwinski That's just how the API responses, the JSON looks like this: `"companies": { "22": { ... } }`. AFAIK cljs-http uses [transit-cljs](https://github.com/cognitect/transit-cljs) to transform the response into native ClojureScript data structures. – beatngu13 Aug 28 '16 at 14:47

1 Answers1

13

(keyword "22") or (keyword (str 22)) returns :22

The reason you are getting :22 is likely because of the keywordize-keys option of a JSON translation. For example:

cljs-http defaults to keywordize-keys for jsonp: https://github.com/r0man/cljs-http/blob/1fb899d3f9c5728521786432b5f6c36d1d7a1452/src/cljs_http/core.cljs#L115 But you can (and should) in this case pass in a flag to disable keywordization.

Not all keys in JSON are appropriate for Clojure keywordization. For example spaces in a JSON key are valid, but not in Clojure.

Please be aware that numeric keywords are probably incorrect. https://clojuredocs.org/clojure.core/keyword#example-542692cec026201cdc326d70 It seems like that caveat has been removed from the current Clojure website, so perhaps that means something but I'm not sure what.

http://clojure.org/reference/reader Currently states that

Keywords - Keywords are like symbols, except: They can and must begin with a colon, e.g. :fred. They cannot contain '.' or name classes. Like symbols, they can contain a namespace, :person/name A keyword that begins with two colons is resolved in the current namespace: In the user namespace, ::rect is read as :user/rect

and that

Symbols begin with a non-numeric character and can contain alphanumeric.

This definition of a keyword excludes :22 and :with spaces

The keyword function returns a result for invalid input, but this is not an endorsement, it is simply because checking for incorrect input would be a performance overhead in a core part of Clojure.

In short, not all JSON keys translate to keywords, so you should avoid keywordize-keys unless you know the keyspace and/or doing so provides some conveniences.

Timothy Pratley
  • 10,586
  • 3
  • 34
  • 63
  • Spaces are valid for Clojure keywords, they're just not common. `(keyword "with spaces")` returns `:with spaces`. – Hoagy Carmichael Aug 28 '16 at 21:41
  • 2
    @HoagyCarmichael you can not "test for correctness" with `(keyword "...")`, since `keyword` does not behave correctly according to the specs. `keyword` will work with (close to) everything and that's the reason OP is in this mess. – cfrick Aug 29 '16 at 08:06