1
votes

I'm quite new to Clojure and, I think, programming in general but I ended up trying to write some kind of DSL for producing PDFs files (using PDFBox, a java library). I want the final syntax to look like something this:

(document {:name "test" :bleed "22mm" ...} 
  (page {:height "560mm" ...) (text {:color "red"} "blabla") (line-break))

So the thing is that every expression is a macro that will do some stuff at eval time + recursively read every expression after the first argument and expand it with a conj'ed options as its first argument. For now, for example it looks like this:

   (defmacro document
      [{:keys [name] :as options}
       & body]
      `(let [~'pdoc ~'(PDDocument.)]
         (do
           ~@(unfold (conj options {:pdoc 'pdoc}) ~body)
           (doto ~'pdoc
             (...)))

and unfold macro (the one expanding arguments):

(defmacro unfold
  [options & body]
  (loop [
         [f & fs] body
         opts options
         output []]
    (if (empty? f)
      output
      (recur fs
             (conj opts (second f))
             (conj output `(~(first f) ~(conj opts (second f)))))
      )))

The problem is that I'm unable to make them work together. After some debugging I still get java.lang.NullPointerException:.

I sense that the problem comes with the way unfold outputs to document but I totally unable to get how macros react when nested. When I use macroexpand, nested macros are never expanded I don't know if it's expected or a programming error on my part. Anyway, when

(macroexpand '(unfold {...} (document) (document)))

I get

[(document {...}) (document {...})]

which, in my current understanding, seems ok as I use it unquote-splice'd afterwards. But I'm clearly missing something.

--------- Edit -----------

For a very unuseful

(document {:name "hey"}} (document {:name "ho"}))

I would like to end up with ((doto ...) being an example) :

(let [pdoc (.PDDocument)]
  (do
    (let [pdoc (.PDDocument)]
      (do
        (doto pdoc .save (str "ho" ".pdf"))))
    (doto pdoc .save (str "hey" ".pdf"))))

Sorry for the very specific and long post but I didn't how to explain the problem without describing everything.

Thank you in advance, Bye

2
Can you please add in a sample call to the macro with the desired output? - Alan Thompson
You're calling unfold like a function, but it's a macro. Either emit an unfold (which would need to emit a do to work) or make unfold a function. Also, please do add the entire exception message. - ClojureMostly
@ClojureMostly Hum I think I get it. What would be the prefered way of doing it? making it a function seems cleaner. And that's actuelly the entire exception message. It's strangely empty... - lprndn
I've seen both, so it's your preference. If you need to use unfold in your code otherwise, then leave it a macro. If the unfold is only used for the library to generate code, then definitely make it a function. So just FYI: A macro has two implicit parameters, that you're not passing in when calling it like a function. Hence (probably) the error. Also, it seems you don't want unfold to be a vararg? - ClojureMostly

2 Answers

0
votes

I'm not sure that will solve the problem but just a few tips:

  • for let bindings, use foo# rather than ~'foo to avoid naming collisions. A sharp at the end generates a random machine-wise symbol so your scope is free of hidden bugs.
  • try to expand your macro definitions with (macroexpand '(document {...})) to see what's going in inside. You may re-evaluate the result list in REPL to check it for errors.
  • finally, try to avoid macros for a while. Try to implement you ideas with plain functions first. There could be enough.
0
votes

Thanks to all of you for your help and advises. For now, I endend up something like this :

(defn unfold
  [options body]
  (loop [
     [f & fs] body
     opts options
     output []]
    (if (empty? f)
      output
      (recur fs
          (conj opts (second f))
          (conj output (list (first f) (conj opts (second f)))))))))

and

(defmacro document
  [{:keys [name] :as options}
  & body]
  `(let [~'pdoc ~'(PDDocument.)]
    (do
      ~@(map macroexpand
          (unfold (conj options {:pdoc 'pdoc}) body))
      (doto ~'pdoc
            (...)))))

It feels a bit hacky but it works as expected. I will now try to investigate new approaches and strategies anyway.