On Thu, May 19, 2011 at 11:26 PM, Lachlan <lachlan.kana...@gmail.com> wrote:
> Just working through this, if we take your example above, what if I
> wanted to override the 'put' method rather than define a new one,
> since we don't have access to 'proxy-super'.  For example making a map
> that enforces storage of dates in a string format - something like:
>
> (defprotocol DMap (put [this o1 o2]) (get-as-date [this o1]))
>
> (extend-type clojure.lang.PersistentArrayMap
>   NPut
>   (put [this o1 o2] (if (::datestore (meta this)) (super put o1
>     (format-date-to-string o2)) (super put o1 o2)))
>   (get-as-date [this o1] (if (::datestore (meta this)) (parse-date
>     (super get o1)) (super get o1)))
>
> and get will return the string formatted version
>
> or another example could be forcing all keys to be keywords, even if
> they are provided as a string or symbol, without having to write
> (assoc {} (keyword k) v) all the time.
>
> the other motivation would be to be able to throw exceptions on
> invalid input into a map or vector, rather than debugging involving
> 'why the hell has that map got an xxx in it!' when after much
> searching it turns out i've got some function arguments around the
> wrong way :)

Several suggestions:

First, you may want to solve some of these issues by providing your
own functions (and possibly using protocols and extend-type to make
them polymorphic) that do what you want. In Clojure, behavior (and
decisions about what works with which objects) tends to be in
functions, not the objects themselves. So you can make this for
instance:

(defn assoc-k [m k v]
  (let [k (if (keyword? k) k (keyword (str k)))]
    (assoc m k v)))

For validation you could make an assoc-v that checks the map's
metadata for a validation function and calls it on the key and value,
then calls assoc. The validation function throws an exception if it
doesn't like the kv pair. Or, if the maps will be in refs, atoms, or
similarly, you can attach validators to those that check the whole map
(and can enforce multi-key constraints).

For the big guns, you can also use deftype on clojure.lang.Associative
or clojure.lang.IPersistentMap to make map-like objects with
overridden assoc and other methods. You tried that above, but hoping
to extend PersistentArrayMap and call super. Instead, implement
IPersistentMap and delegate by having an internal map object, e.g.

(defn format-date-to-string [obj]
  (if (instance? java.util.Date obj)
    (str obj)
    obj))

(defn parse-date [obj]
  (if (string? obj)
    (try
      (java.util.Date. (java.util.Date/parse obj))
      (catch IllegalArgumentException _ obj))
    obj))

(defprotocol DMap
  (put [this k v])
  (get-as-date [this k]))

(deftype DateMap [m]
  clojure.lang.IPersistentMap
  ; implement methods to delegate to m
  ; and wrap return in (DateMap. ...)
  ; but maybe punt assoc to put and get to
  ; get-as-date instead.
  (assoc [this k v]
    (DateMap.
      ; (assoc m k (format-date-to-string v))))
        (assoc m k v)))
  (without [this k]
    (DateMap. (dissoc m k)))
  (assocEx [this k v]
    (DateMap.
      ; (.assocEx m k (format-date-to-string v))))
        (.assocEx m k v)))
  (iterator [this] (.iterator m))
  (containsKey [this k] (.containsKey m k))
  (entryAt [this k] (first {k (get this k)}))
  (count [this] (count m))
  (cons [this [k v]] (assoc this k v))
  (empty [this] (DateMap. (with-meta {} (meta m))))
  (equiv [this obj] (= m obj))
  (seq [this]
    (seq (zipmap (keys m) (map this (keys m)))))
  (valAt [this k] (.valAt this k nil))
  (valAt [this k not-found]
    (if-let [[_ v] (find m k)]
      ; (parse-date (m k not-found))
      v
      not-found))
  clojure.lang.IObj ; Let DateMap have metadata
  (meta [this] (meta m))
  (withMeta [this md] (DateMap. (with-meta m md)))
  clojure.lang.IFn
  (invoke [this k] ; Make DateMap a function
    (get this k))  ; of its keys like normal maps
  (invoke [this k not-found]
    (get this k not-found))
  DMap
  (put [this k v]
    (DateMap. (assoc m k (format-date-to-string v))))
  (get-as-date [this k]
    (parse-date (m k))))

(extend-type clojure.lang.APersistentMap
  ; Should catch PersistentArrayMap, PersistentHashMap, etc.
  DMap
  (put [this k v]
    (assoc this k v))
  (get-as-date [this k]
    (this k)))

which works:

=> (def q (put {} :foo (java.util.Date.)))
#'user/q
=> q
{:foo #<Date Thu May 19 22:44:10 PDT 2011>}
=> (get-as-date q :foo)
#<Date Thu May 19 22:44:10 PDT 2011>
=> (def r (put (DateMap. {}) :foo (java.util.Date.)))
#'user/r
=> r
{:foo "Thu May 19 22:44:37 PDT 2011"}
=> (get-as-date r :foo)
#<Date Thu May 19 22:44:10 PDT 2011>

(Try also storing other types of value in r, even with put, and
retrieving them, even with get-as-date. Try even strings that aren't
dates. Try calling the map as a function, or using get or a keyword,
and using the optional not-found with each. Try attaching and reading
metadata, or doing pretty much anything else commonly done with
Clojure maps. I tested all of these things and they worked properly.
In particular, using put and get-as-date interconverts Dates and
string-formatted dates, without bothering anything else.)

The protocol dispatches on type. On normal Clojure map types it does
nothing; on DateMaps it converts Dates internally to strings.

If you want to make assoc and get do the date-converting behavior, you
can even dispense with the protocol entirely and implement the put and
get-as-date methods' behaviors directly in assoc and get; just
uncomment the commented lines in assoc, assocEx, and valAt and delete
the single line following each of the three uncommented lines, and
delete the DMap protocol, the DMap section of DateMap, and the
extend-type. DateMaps will then act exactly like normal maps except
for converting Dates to strings internally and back again on retrieval
(and, for better or worse, any strings that parse as dates to Dates,
even if they were added as strings). This includes that vals, seq,
find, etc. will give you Dates and MapEntries containing Dates with
conversion being performed. (This has also been tested and works.
Optional not-found objects will be returned by get/etc. unaffected by
conversions, even if they are strings that would parse as Dates.)

Of course, the main reason for wanting to turn Dates into strings
internally is probably to support externalization, which datatypes and
records do poorly. You'll need to add a method for print-dup to write
Dates as #=(java.util.Date. whatever) instead for that, or else use
DateMap and add a method to print-dup those as `#=(DateMap. ~(into {}
the-date-map)) -- that is, the date map is poured into a normal map so
it will print normally, and printed inside #=(DateMap. ...). If for
security reasons (untrusted data being read in) you disable #=(...)
via (binding [*read-eval* false] ...) for your reads then you'll need
to just print DateMaps as normal maps and know which objects to pass
to DateMap's constructor on reconstitution.

Or again you could put data or metadata in the map to indicate it's a
date map and make ordinary functions put and get-as-date that check
for this and wrap assoc and get. This won't let you make assoc and get
(or even calling as a function) directly do the conversions with date
maps, though.

Clojure is very flexible; you have many options, and different ones
may be better or worse, on a case-by-case basis, depending on your
needs of the moment.

All that being said, if you really want access to proxy-super, you can
use proxy on non-final classes and interfaces like IPersistentMap, but
it's really not recommended. For one thing, proxy-super isn't thread
safe.

-- 
Protege: What is this seething mass of parentheses?!
Master: Your father's Lisp REPL. This is the language of a true
hacker. Not as clumsy or random as C++; a language for a more
civilized age.

-- 
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