I am new to clojure and am trying to refactor some code that I've written that looks like this
(defn transform
[entity]
(let [new-obj (doto (SomeObj.)
(.setField1 (:field-1 entity)))
new-obj))
I have many objects that will need this implemented but would like to create a macro that accepts an entity, prototype Ex: (SomeObj.), and a map where the keys are the field names of the prototype and the values are vectors of keywords to get the correct field from the entity. For each key I would need to call .set + keyName using the argument from (get-in map [value as a vector]).
My hope is for each new entity I can create a config of the mappings and only write code for special cases. Is this possible using a macro?
Or is there a more idiomatic way to do this in clojure.
yes, you could easily do it with a macro like this:
(defmacro map-to [type mappings entity]
`(doto (new ~type)
~#(map (fn [[field entity-field]]
`(~(symbol (str ".set" (clojure.string/capitalize field)))
(~entity-field ~entity)))
mappings)))
this would generate exactly the code you need:
(map-to java.util.Date {date :dt minutes :mm hours :h}
{:dt 10 :mm 22 :h 12})
would be expanded into the following:
(doto
(new java.util.Date)
(.setDate (:dt {:dt 10, :mm 22, :h 12}))
(.setMinutes (:mm {:dt 10, :mm 22, :h 12}))
(.setHours (:h {:dt 10, :mm 22, :h 12})))
a few things to notice here:
1) you don't need to introduce a new variable new-obj, since doto returns the object being operated on.
2) your mappings should be passed as a literal map, because otherwise you cannot get the keys to pass to . special form.
3) you can see that the entity map is being repeated. You can fix this by introducing another binding inside the macro:
(defmacro map-to [type mappings entity]
(let [ent (gensym "entity")]
`(let [~ent ~entity]
(doto (new ~type)
~#(map (fn [[field entity-field]]
`(~(symbol (str ".set" (clojure.string/capitalize field)))
(~entity-field ~ent)))
mappings)))))
so now it expands like this:
(let [entity20047 {:dt 10, :mm 22, :h 12}]
(doto
(new java.util.Date)
(.setDate (:dt entity20047))
(.setMinutes (:mm entity20047))
(.setHours (:h entity20047))))
in repl:
user> (map-to java.util.Date {date :dt minutes :mm hours :h}
{:dt 10 :mm 22 :h 12})
;;=> #inst "2016-09-10T09:22:48.867-00:00"
user> (let [ent {:dt 10 :mm 22 :h 12}]
(map-to java.util.Date {date :dt minutes :mm hours :h} ent))
;;=> #inst "2016-09-10T09:22:48.899-00:00"
(the value is three hours earlier due to my time zone (gmt+3))
update
to get your desired behaviour (with get-in) you can just slightly modify this macro:
(defmacro map-to [type mappings entity]
(let [ent (gensym "entity")]
`(let [~ent ~entity]
(doto (new ~type)
~#(map (fn [[field entity-field]]
`(~(symbol (str ".set" (clojure.string/capitalize field)))
(get-in ~ent ~entity-field)))
mappings)))))
in repl:
user> (map-to java.util.Date {date [:date :dt]
minutes [:time :mm]
hours [:time :h]}
{:date {:dt 10} :time {:mm 22 :h 12}})
;;=> #inst "2016-09-10T09:22:41.935-00:00"
expands to:
(let [entity20094 {:date {:dt 10}, :time {:mm 22, :h 12}}]
(doto
(new java.util.Date)
(.setDate (get-in entity20094 [:date :dt]))
(.setMinutes (get-in entity20094 [:time :mm]))
(.setHours (get-in entity20094 [:time :h]))))
now you can make one more macro to automate the creation of mapping functions:
first of all you need a function to produce maker name from class object:
(defn make-name [c]
(->> c
.getName
(#(clojure.string/split % #"\."))
(clojure.string/join "-")
(str "create-")
symbol))
user> (make-name java.util.Date)
;;=> create-java-util-Date
now the macro to define functions to create instances from entities:
(defmacro defmapper [type mappings]
`(defn ~(make-name type) [entity#]
(map-to ~type ~mappings entity#)))
this one would create functions, that are, given an entity, convert it to the class instance. It's just an ordinary function:
(defmapper java.util.Date {date [:date :dt]
minutes [:time :mm]
hours [:time :h]})
expands to:
(defn create-java-util-Date [entity__20122__auto__]
(map-to
java.util.Date
{date [:date :dt], minutes [:time :mm], hours [:time :h]}
entity__20122__auto__))
in repl:
user> (map create-java-util-Date
[{:date {:dt 10} :time {:mm 22 :h 12}}
{:date {:dt 11} :time {:mm 22 :h 12}}
{:date {:dt 12} :time {:mm 22 :h 12}}])
;;(#inst "2016-09-10T09:22:18.974-00:00"
;; #inst "2016-09-11T09:22:18.974-00:00"
;; #inst "2016-09-12T09:22:18.974-00:00")
Related
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)
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.
Is there a way to retrieve the metadata of the arguments inside a clojure macro without using eval? The only thing I could come up with so far is this:
(def ^{:a :b} my-var)
(defmacro my-macro [s] (prn (eval `(meta (var ~s)))))
(my-macro my-var)
;; Prints {:a :b, :name my-var, ...}
I ended up finding a solution:
(def ^{:a :b} my-var)
(defmacro my-macro [s] (prn (meta (resolve s))))
(my-macro my-var)
;; Prints {:a :b, :name my-var, ...}
So the key part here is to use resolve function to get the var associated to the symbol.
I'm not quite understanding what's wrong about
(defrecord Person [name age])
(defn make-person [& opts]
(let [defaults {:age 18}]
(map->Person (merge defaults opts))))
(make-person {:name "Jim"})
=> ClassCastException clojure.lang.PersistentArrayMap cannot be cast to java.util.Map$Entry clojure.lang.APersistentMap.cons (APersistentMap.java:42)
If I do:
(map->Person (merge {:age 18} {:name "Jim"}))
I can also get the make-person function working with a non optional argument.
(defn make-person [opts]
(let [defaults {:age 18}]
(map->Person (merge defaults opts))))
The solution I've settled on for what I want to do works like:
(defn make-person
([opts] (map->Person (merge {:age 18} opts)))
([] (map->Person {:age 18})))
So I guess I'm asking, what does & really do when destructuring function arguments?
The & opts returns a sequence, which is problematic if you pass a map in.
You probably want to destructure the sequence something like:
(defn make-person [& [opts]]
(map->Person (merge {:age 18} opts))
Which lets you do (make-person) or (make-person {:opt1 "foo" :opt2 "bar"})
you could take advantage of the special syntax to allow keyword args:
(defn make-person [ & {:as opts} ]
(map->Person (merge {:age 18} opts))
Allowing you to do (make-person :opt1 "foo" :opt2 "bar") but in my experience that makes calling make-person difficult if you want to do the merging outside the call (which you will one day) (i.e if you want (make-person (merge some-opts some-other-opts)
Here is a code example from Clojure Programming
(defn character
[name & {:as opts}]
(ref (merge {:name name :itmes #{} :health 500}
opts)))
(def smaug (character "Smaug" :health 500 :strength 400 :items (set (range 50))))
(def bilbo (character "Bilbo" :health 100 :strength 100))
(def gandalf (character "Gandalf" :health 75 :mana 750))
(defn loot
[from to]
(dosync
(when-let [item (first (:items #from))]
(alter to update-in [:items] conj item)
(alter from update-in [:items] disj item))))
(wait-futures 1
(while (loot smaug bilbo))
(while (loot smaug gandalf)))
(map (comp count :items deref) [bilbo gandalf])
(filter (:items #bilbo) (:items #gandalf))
Everything works fine until the last line, which brings up an error:
ClassCastException clojure.lang.PersistentList cannot be cast to clojure.lang.IFn clojure.core/filter/fn--4264 (core.clj:2605)
The constructor function "character" typos the key for :items as :itmes, making the conj on alter get nil as the initial value. Conjing nil and a value gives you a list -- thus the ClassCastException.
Looks like you've got a typo in your definition of character - :itmes instead of :items.
This means that when you loot, you're calling
(conj nil 0), which turns the entry under :items into a list of (0). A list doesn't implement IFn, hence the error.
If I've got a sequence of records defined by (defrecord Person [name age]) and I want to get the record of the person with the maximum age, is there an easier way to do this than
(reduce #(if (> (:age %1) (:age %2)) %1 %2) people)
That's the only way I've figured out to do it so far, but it seems like this has to be a common enough scenario that there must be some built-in library functions that make this easier and/or more generic.
clojure.core/max-key is a suitable tool for the job.
(apply max-key :age [{:age 12} {:age 20} {:age 30}]) ;; -> {:age 30}
(last (sort-by :age [{:age 12} {:age 20} {:age 30}]))
sort-by uses compare