On Mon, May 2, 2011 at 6:37 AM, David Jagoe <[email protected]> wrote:
> Hi Everyone,
>
> Background to my problem:
>
> I am developing a compojure application, and there is lots of
> duplication in listing field names in my current data model:
>
> (i) in the defstruct
> (ii) in the public constructor's argument list
> (iii) in the hiccup form fields
> (iv) in the compojure argument destructuring
> (v) in the handler's argument list
>
> Ideally I would like to declare my data model in one place like this:
>
> (def person-entity
> {:name {:type String :validator name-validator}
> :id-number {:type String :validator id-number-validator}
> :height {:type Float :default 0.0}
> :weight {:type Float :default 0.0}
> :bmi {:type Float :internal true}})
>
> And generate everything from there so that it is trivial to add fields
> etc. (:internal true just means that this is a calculated field or for
> some other reason is not supplied by the user - it is therefore not
> needed by the constructor, and will not show up on the edit-person
> form).
>
> I have tried to generate the defrecord etc from such a definition and
> have battled a bit, so in the interest of making some progress with my
> experiment I have tried this instead:
>
> (defn name-validator [val] val)
> (defn id-number-validator [val] val)
> (defn nil-validator [val] val)
>
> (defrecord Person
> [#^String name
> #^String id-number
> #^Float height
> #^Float weight
> #^Float bmi])
>
> (def person-traits
> {:name {:validator name-validator}
> :id-number {:validator id-number-validator}
> :height {:default 100.0}
> :weight {:default 100.0}
> :bmi {:internal true}})
>
> (def person-constructor (make-constructor Person person-traits))
> (def person-editor (make-editor Person person-traits))
> (def bob (person-constructor {:name "Bob" :id-number "123"}))
>
> ;;; I don't really know how bmi is calculated!! Let's pretend it is
> weight (kg) / height (cm)
> (bob :bmi)
> 1.0
> (bob :id-number)
> "123"
>
> And the person-editor is a snippet of hiccup which has all of the
> relevant fields (e.g. text fields by default).
>
> I plan to use flutter validators, and not to use compojure
> destructuring (I will have to pull the fields out of the request in
> the generated form handlers).
>
> I am looking for advice in the following areas:
>
> (i) Is it possible to generate the (defrecord Person ...) from the
> person-entity hash-map that I have shown?
Shoundn't be too hard. Something like
(defn to-rec-field [kword entity-map]
(let [rf (symbol (name kword))]
(if-let [t (:type (entity-map kword))]
(with-meta rf {:tag t})
rf)))
(defmacro defentity [name entity-map]
(let [fields (fn [ks]
(map #(to-rec-field % entity-map)
ks))
optionals (filter #(:default (entity-map %))
(keys entity-map))
optional-subsets (map #(set (drop % (reverse optionals)))
(range (count optionals)))]
`(do
(defrecord ~(symbol (str "R" name))
~@(fields (keys entity-map)))
(defn ~(symbol (str "construct-" name))
~@(map
(fn [ops]
(list
(vec (fields (remove ops (keys entity-map))))
(cons
(symbol (str "R" name "."))
(map
#(if (ops %)
(:default (entity-map %))
(to-rec-field % entity-map))
(keys entity-map)))))
optional-subsets))
(defn ~(symbol (str "edit-" name))
~@(left as an exercise for the reader)))))
This should (untested) produce a defrecord and arity-overloaded
constructors for the whole argument list and with successively more of
the fields with defaults omitted, starting at the right -- so, for
your person, you'd get constructors for [name id-number height weight
bmi], [name id-number height bmi], and [name id-number bmi].
You probably want it to omit bmi from the argument lists and compute
it -- that will complicate things, something like:
(defmacro defentity [name entity-map]
(let [fields (fn [ks]
(map #(to-rec-field % entity-map)
ks))
default #(:default (entity-map %))
optionals (filter default (keys entity-map))
optional-subsets (map #(set (drop % (reverse optionals)))
(range (count optionals)))
computer #(:computer (entity-map %))
computed (filter computer (keys entity-map))]
`(do
(defrecord ~(symbol (str "R" name))
~@(fields (keys entity-map)))
(defn ~(symbol (str "construct-" name))
~@(map
(fn [ops]
(list
(vec
(fields
(remove (concat ops computed)
(keys entity-map))))
`(let ~(vec
(interleave
(fields (keys entity-map))
(map #(if (ops %)
(default %)
(to-rec-field % entity-map))
(keys entity-map))))
~(symbol (str "R" name "."))
(map
#(if (computer %)
(computer %)
(to-rec-field % entity-map))
(keys entity-map)))))
optional-subsets))
(defn ~(symbol (str "edit-" name))
~@(left as an exercise for the reader)))))
used with something like
(defentity person
{:name {:type String}
:id-number {:type Integer}
:height {:type Double :default 100.0}
:weight {:type Double :default 100.0}
:bmi {:type Double :computer (/ weight height)}})
which should create a record Rperson and
(defn construct-person
([#^String name #^Integer id-number #^Double height #^Double weight]
(let [#^String name name
#^Integer id-number id-number
#^Double height height
#^Double weight weight])
(Rperson. name id-number height weight (/ weight height))))
([#^String name #^Integer id-number #^Double height]
(let [#^String name name
#^Integer id-number id-number
#^Double height height
#^Double weight 100.0])
(Rperson. name id-number height weight (/ weight height))))
([#^String name #^Integer id-number]
(let [#^String name name
#^Integer id-number id-number
#^Double height 100.0
#^Double weight 100.0])
(Rperson. name id-number height weight (/ weight height)))))
Note that both the :default and the :computer can be s-expressions
that just get inserted verbatim as code. The two differences are:
1. A field with :computer will not ever be a constructor parameter.
2. The :computer s-expression can refer to any of the fields; the
:default can only refer to earlier ones and non-optional ones.
I hope this shows how one might go about constructing a macro to take
a definition similar to your original person-entity map and turn it
into a record, constructor function, and possibly other structures.
For example it could put code in the constructor function to run a
:validator, if present, on that field, where the :validator throws an
exception; or to turn a :validator field into a test and throw
exception clause where the :validator is a boolean expression; etc.
--
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to [email protected]
Note that posts from new members are moderated - please be patient with your
first post.
To unsubscribe from this group, send email to
[email protected]
For more options, visit this group at
http://groups.google.com/group/clojure?hl=en