On Mon, May 2, 2011 at 6:37 AM, David Jagoe <davidja...@gmail.com> 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 clojure@googlegroups.com
Note that posts from new members are moderated - please be patient with your 
first post.
To unsubscribe from this group, send email to
clojure+unsubscr...@googlegroups.com
For more options, visit this group at
http://groups.google.com/group/clojure?hl=en

Reply via email to