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