1
\$\begingroup\$

I need to save nested data to disk for a project. Right now, my state is a nested record, and has proven to be a pain to serialize. I tried compressing it to a map, but after testing it, I realized my code failed in a unfortunate corner case that proved it to impractical. I decided to take the suggestion to drop using records altogether since I don't need the speed, but found it annoying to write ->-ish pseudo-constructors for everything I needed to represent. I decided to write a macro that takes a vector of keyword fields, and emits a defn that creates an map with the field names mapped to the arguments of the function:

(defmap Hello [:one :two :three])
(->Hello 1 2 3)

{:one 1, :two 2, :three 3}

I decided for the macro to take keywords as arguments so they're introduced to IntelliJ. defrecord doesn't give any hints, so the first time you use a record's fields, they aren't suggested by IntelliJ.

Unfortunately, IntelliJ complains about Hello not existing, and doesn't recognize ->Hello as a declared function, so this isn't very nice to use, but it was a fun little sidetrack. I don't know if I'll actually use it, but it's the most complicated macro I think I've ever written, and I'd like it reviewed.

I'd like to know if I'm doing anything roundabout or overly complicated. I'd also like to know if there's a way to hint to IntelliJ that Hello doesn't need to be defined, and that ->Hello does.

(defmacro defmap
  "Generates a defn that creates a function with the name (str \"->\" map-name) that
    returns a map. The map will have keys of fields, with the values of the arguments passed to the function."
  [map-name fields]
  (let [adjusted-name# (str "->" map-name)
        args# (mapv (comp symbol name)  fields)
        body-map# (into {} (map #(do [% (symbol %2)]) fields args#))]

    `(defn ~(symbol adjusted-name#) ~args# ~body-map#))) 
\$\endgroup\$
  • \$\begingroup\$ Why do you want -> constructors? As opposed to literally defining the map (def a-hello {:one 1 :two 2 :three 3})? \$\endgroup\$ – BenC Sep 12 '17 at 7:19
  • \$\begingroup\$ @BenC I noticed the constructor functions I was writing basically just took the parameters of the function, and mapped each parameter to a keyword with the same name. I decided to try automating that. As noted in the question though, I didn't really plan in using the macro in the end. It was more of a quick side project. And I didn't just define a map literal because I may need to change what the map contains. It's handy to have a constructor. I also want to make sure that I don't accidentally introduce I typo into one of the keywords, so I don't want to manually writing them everywhere. \$\endgroup\$ – Carcigenicate Sep 12 '17 at 12:35
1
\$\begingroup\$

The code is nice, there are only some minor improvements here.

  1. I love that you have written a docstring.

  2. You do not need the # in the variable names. These names are used for generating symbols but you do not use this capability in the code.

  3. You could make adjusted-make a symbol when defined in the let block. This way the constructed form would be simpler and more readable.

  4. I prefer doing assert calls for error handling in macros. You could check that the first parameter is a symbol? and the second is a vector? of keys.

  5. Instead of (into {} (map ...)) you can also use zipmap to construct the map.

The code can be rewritten as the following:

  (defmacro defmap
"Generates a defn that creates a function with the name (str \"->\" map-name) that
returns a map. The map will have keys of fields, with the values of the arguments passed to the function."
[map-name fields]
(assert (symbol? map-name))
(assert (vector? fields))
(let [adjusted-name (symbol (str "->" map-name))
      args          (mapv (comp symbol name) fields)
      body-map      (zipmap fields args)]
  `(defn ~adjusted-name ~args ~body-map)))
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.