Skip to content

Instantly share code, notes, and snippets.

@ah45
Created March 6, 2015 13:04
Show Gist options
  • Save ah45/96c50c9ee3cac5c3cbe4 to your computer and use it in GitHub Desktop.
Save ah45/96c50c9ee3cac5c3cbe4 to your computer and use it in GitHub Desktop.
Editing Nested Maps from the bottom up with Clojure Zippers
(ns nested-map-zipper
(:require [clojure.zip :as zip]
[clojure.data :refer [diff]]))
(defmacro recur-if
([expr]
`(recur-if ~expr nil))
([expr else]
`(let [temp# ~expr]
(if temp# (recur temp#) ~else))))
(def branching-keys
#{:publishers :authors :books})
(defn branch? [x]
(seq (some #(get x %) branching-keys)))
(defn map->seq-with-meta [meta-fn m]
(reduce
(fn [r [k v]]
(conj r (vary-meta v meta-fn k)))
[]
m))
(defn branches [x]
(if (branch? x)
(->> (select-keys x branching-keys)
(map (fn [[pk cs]]
(map->seq-with-meta
(fn [m k] (assoc m ::id k ::pk pk))
cs)))
(apply concat))))
(defn deep-merge
"Recursively merge maps. Sub-maps are recursively merged. Vectors
are merged by concat."
[& ms]
(letfn [(f [a b]
(if (and (map? a) (map? b))
(deep-merge a b)
(if (and (vector? a) (vector? b))
(vec (concat a b))
(or b a))))]
(apply merge-with f ms)))
(defn nested-map-zipper [nm]
(zip/zipper
(fn [x]
(if-let [m (if (map? x) x (nth x 1))]
(branch? m)))
(fn [x]
(if-let [m (if (map? x) x (nth x 1))]
(branches m)))
(fn [x cs]
(if (map? x)
(reduce
(fn [r c]
(let [id (-> c meta ::id)
pk (-> c meta ::pk)]
(update-in r [pk id] deep-merge c)))
x
cs)
(assoc x 1 (into {} cs))))
nm))
(defn edit-from-last-to-first
"Replaces each node in zipper `zp`--starting from the last and ending
with the first--with the value of `(f node args)` returning the root
node when finished."
[zp f & args]
(let [last (loop [loc zp]
(let [x (zip/next loc)]
(if (zip/end? x)
loc
(recur x))))]
(loop [n last]
(let [nn (apply zip/edit n f args)]
(recur-if (zip/prev nn) (zip/root nn))))))
(def example-nested-map
{:bookstore "The Shop Around The Corner"
:publishers
{"Cornerstone"
{:authors
{"Harper Lee"
{:books
{"Go Set A Watchman"
{:published nil
:rrp 18.99
:price 9.49}}}}}
"Faber & Faber"
{:authors
{"Kazuo Ishiguro"
{:books
{"The Buried Giant"
{:published "2015-03-03"
:formats #{:paperback :hardback}
:rrp 20.00
:price 16.00}
"Never Let Me Go"
{:published "2010-02-25"
:formats #{:paperback :hardback :ebook}
:rrp 16.00
:price 8.99}}}}}}})
(-> example-nested-map
nested-map-zipper
(edit-from-last-to-first
((fn []
(let [c (atom 0)]
(fn [n]
(let [i @c]
(swap! c inc)
(assoc n ::visitation-order i))))))))
;=>
"
{:bookstore \"The Shop Around The Corner\"
:nested-map-zipper/visitation-order 7
:publishers
{\"Cornerstone\"
{:nested-map-zipper/visitation-order 2
:authors
{\"Harper Lee\"
{:books
{\"Go Set A Watchman\"
:nested-map-zipper/visitation-order 1
{:nested-map-zipper/visitation-order 0
:rrp 18.99
:price 9.49
:published nil}}}}}
\"Faber & Faber\"
{:nested-map-zipper/visitation-order 6
:authors
{\"Kazuo Ishiguro\"
{:books
:nested-map-zipper/visitation-order 5
{\"Never Let Me Go\"
{:nested-map-zipper/visitation-order 4
:formats #{:ebook :hardback :paperback}
:rrp 16.0
:price 8.99
:published \"2010-02-25\"}
\"The Buried Giant\"
{:nested-map-zipper/visitation-order 3
:formats #{:hardback :paperback}
:rrp 20.0
:price 16.0
:published \"2015-03-03\"}}}}}}}
"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment