1
votes

I'm currently working on a pdf generating library built around pdfbox, a java library. I don't have a problem per se, I'm just uncertain of what would be the clever way in clojure to do something. I try to stick to a Hiccup style syntax for generating pdf.

With something like that (a very impractical example):

[:page {:title "hey"}
    [:frame {:name "frame1" :top 130}]]

I would like to retrieve later in the document the values passed to page and frame (which are functions after parsing). For example, the next frame:

[:frame {:bottom (+ 10 (:top "frame1"))} (str "Titre:" (:title page))]

Every function passes its options map to the other so the first frame's options actually look like this:

{:title "hey", :name "frame1", :top 130}

But obviously the user can't access that map when the executing this kind of code. For the page I think using a global Var that is updated with binding seems to be an okay solution (open to any suggestions). But as there might be any number of frames they can't be declared earlier. Therefore, my question is:

What kind of function, concept or way of doing things would be best to deal with that kind of problem? How could I give the user the ability to retrieve these data? (avoiding a global var for all options and a get-in if possible)

1

1 Answers

1
votes

i've got an idea about that: why don't you use dynamically scoped value for context, that would contain all the data for your struct's call stack. And then you can analyze your struct, evaluating in this context.

I would go with something like this:

(def ^:dynamic *context* ())

(defn lookup-context [& kv-pairs]
  (some #(when (every? (fn [[k v]] (= (k %) v)) kv-pairs) %)
        *context*))

(defmacro with-context [data]
  (let [items (tree-seq #(and (vector? %) (#{:frame :page} (first %)))
                        #(nthnext % 2)
                        data)
        ctx-items (reverse (map second items))
        let-bindings (zipmap ctx-items (repeatedly gensym))
        data (clojure.walk/postwalk-replace let-bindings data)]
    (reduce (fn [acc [itm sym]]
              `(let [~sym ~itm]
                 (binding [*context* (cons ~sym *context*)] ~acc)))
                 data ;; here goes your data parsing
                 let-bindings)))

so this macro establishes cascading dynamic bindings, and all the calls to lookup-context inside it (even in the nested functions called from ";;here goes your data parsing" part)

for example with this structure:

(with-context [:page
               {:name "page0" :val 1000}
               [:frame
                {:name "frame0" :val 10}
                [:frame {:name "frame1" :val (+ (:val (lookup-context [:name "page0"]))
                                                (:val (lookup-context [:name "frame0"])))}]]])

it is going to be expanded to this:

(let [G__8644 {:name "page0", :val 1000}]
  (binding [*context* (cons G__8644 *context*)]
    (let [G__8643 {:name "frame0", :val 10}]
      (binding [*context* (cons G__8643 *context*)]
        (let [G__8642 {:name "frame1",
                       :val
                       (+
                         (:val (lookup-context [:name "page0"]))
                         (:val (lookup-context [:name "frame0"])))}]
          (binding [*context* (cons G__8642 *context*)]
            [:page G__8644 [:frame G__8643 [:frame G__8642]]]))))))

giving you the result you need, i guess

UPDATE as an answer to @amalloy's question about the reason for dynamically scoped var usage:

user> (defn item-factory []
        [:frame {:name "frame2" :val (+ (:val (lookup-context [:name "frame1"]))
                                        (:val (lookup-context [:name "page0"])))}])
#'user/item-factory

user> 
(with-context [:page
               {:name "page0" :val 1000}
               [:frame
                {:name "frame0" :val 10}
                [:frame {:name "frame1" :val (+ (:val (lookup-context [:name "page0"]))
                                                (:val (lookup-context [:name "frame0"])))}]
                (item-factory)]])
;;=> [:page {:name "page0", :val 1000} 
;;          [:frame {:name "frame0", :val 10} 
;;                  [:frame {:name "frame1", :val 1010}] 
;;                  [:frame {:name "frame2", :val 2010}]]]

as you can see, the item-factory function, being called inside the data processing, is also context aware, meaning that the lib user can simply decompose the data generation, keeping the implicit dependency on the items defined upper on the definitions stack.