Recently, I've been taking advantage of the rich API for manipulating
clojure's builtin data structures such as Maps and Vectors. In a small
example, I have different user settings stored as maps and I can
easily merge them using the function merge. As I've developed the
application further, however, I need to store data using a tree/table
structure composed of maps and vectors. Querying and manipulating
arbitrary hybrid map/vector trees requires a sequence of calls to map,
assoc, filter, etc. depending on how nested the element is. This
results in long nested calls and redundant passing/packaging of data
that can be difficult to follow mentally.

In a simple example,

(def x {:a [{:A 1 :B 2}{:A 3 :B 4}]
          :b [1 2 3]
          :c {:X 10 :Y 9 :Z 8}})

(assoc x :a (map #(assoc % :B 30) (:a x)))

returns:
{:a ({:A 1, :B 30} {:A 3, :B 30}), :b [1 2 3], :c {:X 10, :Y 9, :Z 8}}

Clojure does a good job at being succinct, but you can imagine a more
complex structure, in addition, there is room for removing some
redundancy. Notice we already know we are editing :a, yet we need to
pass (:a x) to map.

To make this easier manipulate these data structures, I've wrote some
code that allows the programmer to write edits in a fashion similar to
zippers.

(defmacro doedit [init & forms]
  `((->> ~@(map (fn [form]
                  `(str-utils2/partial ~...@form))
                (reverse forms))) ~init))

(defn in-map [data key next-call]
  (assoc data key (next-call (key data))))

(defn all-seq [data next-call]
  (map #(next-call %) data))

(defn change-to [data value]
  value)

So the above can be rewritten as:

(doedit x
          (in-map :a)
          (all-seq)
          (in-map :B)
          (change-to 30))

or if you prefer

(doedit x
           (in-map :a)
           (all-seq)
           (assoc :B 30))

>From the perspective of the user, doedit takes the data structure, and
traverses the data according to the operations listed in the forms.
The last operation is required to be an edit operation, or more
technically, a call to function that accepts as its first argument the
data it can "edit" and returns the modified data. The passing and
packaging happen automatically from the macro so you don't see passing
data to assoc.

It's easy to add a select like function for sequences.

(defn select-seq [data classifier next-call]
  (map #(if (classifier %)
          (next-call %)
          %)
       data))

This way we can be specific about what we want to edit in a sequence

(doedit x
           (in-map :a)
           (select-seq #(= (:A %) 1))
           (assoc :B 30))

returns:
{:a ({:A 1, :B 30} {:A 3, :B 4}), :b [1 2 3], :c {:X 10, :Y 9, :Z 8}}

To do a simple query I find the thread last macro useful with the help
of simple function:

(defn find-seq [classifier data]
  (first (filter classifier data)))

(->> x
      (:a)
      (find-seq #(= (:A %) 1))
      (:B))
returns:
2

I'm interested in your thoughts, criticisms and improvements. I feel
like this seemed like a place for monads given all the packaging,
function passing, partial functions, and threading but trying to
understand monads makes my head turn to mush. For some reason I find
macro writing easier. I feel the code could be potentially more
general and make writing classifier functions for find-seq and select-
seq easier. Maybe there's a library that does this already? How do you
guys normally approach manipulating such data structures?

Best,
Brent

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