1 Introduction
1.1 Overview
std.lib.collection provides extended collection utilities beyond clojure.core, including:
- Advanced map operations (map-keys, map-vals, filter-keys, filter-vals)
- Nested data structure manipulation (merge-nested, flatten-nested)
- Sequence utilities (queue, seqify, unlazy)
- Tree operations (tree-flatten, tree-nestify)
- Data diffing and patching (diff, diff:patch, diff:unpatch)
2 Walkthrough
2.1 Working with maps
Most day-to-day use starts with map transformations. map-keys and map-vals apply a function to every key or value, while filter-keys and filter-vals keep entries that satisfy a predicate.
transform map keys and values
(map-keys inc {0 :a 1 :b 2 :c})
=> {1 :a 2 :b 3 :c}
(map-vals inc {:a 1 :b 2 :c 3})
=> {:a 2 :b 3 :c 4}
filter entries by key or value
(filter-keys even? {0 :a 1 :b 2 :c})
=> {0 :a 2 :c}
(filter-vals even? {:a 1 :b 2 :c 3})
=> {:b 2}
rename and qualify keys
(rename-keys {:a 1 :b 2} {:a :name :b :id})
=> {:name 1 :id 2}
(qualify-keys {:a 1 :b 2} :user)
=> #:user{:a 1 :b 2}
(unqualify-keys {:user/a 1 :user/b 2})
=> {:a 1 :b 2}
2.2 Nested maps
Configuration data is often deeply nested. merge-nested recurses into maps, assoc-new only adds missing keys, and dissoc-nested removes a path and collapses empty parents.
merge nested configurations
(merge-nested {:server {:host "localhost" :port 8080}}
{:server {:port 3000}})
=> {:server {:host "localhost" :port 3000}}
(merge-nested-new {:server {:host "localhost"}}
{:server {:port 3000}})
=> {:server {:host "localhost" :port 3000}}
update a nested path safely
(dissoc-nested {:a {:b {:c 1 :d 2}}}
[:a :b :c])
=> {:a {:b {:d 2}}}
2.3 Tree-shaped data
Flat maps with slash-separated keys can be nested into trees with tree-nestify, and deep trees can be flattened back with tree-flatten.
flatten and nest tree keys
(tree-flatten {:a {:b {:c 1 :d 2}
:e {:f 3}}})
=> {:a/b/c 1 :a/b/d 2 :a/e/f 3}
(tree-nestify {:a/b/c 1 :a/b/d 2})
=> {:a {:b {:c 1 :d 2}}}
2.4 Diffing and patching
diff compares two maps and returns additions (:+), removals (:-), and changes (:> and :<). The resulting patch can be applied or reversed.
compute and apply a diff
(diff {:a 2} {:a 1})
=> {:+ {} :- {} :> {[:a] 2}}
(let [old {:a {:b 1 :d 3}}
new {:a {:c 2 :d 4}}
d (diff new old true)]
(diff:patch old d))
=> {:a {:c 2 :d 4}}
(let [old {:a {:b 1 :d 3}}
new {:a {:c 2 :d 4}}
d (diff new old true)]
(diff:unpatch new d))
=> {:a {:b 1 :d 3}}
2.5 Sequences and queues
queue builds a persistent queue, seqify normalises values to sequences, and unlazy forces lazy sequences.
use a persistent queue
(-> (queue 1 2 3 4)
pop
vec)
=> [2 3 4]
normalise values to sequences
(seqify 1)
=> [1]
(seqify [1 2])
=> [1 2]
(unlazy (map inc [1 2 3]))
=> [2 3 4]
2.6 End-to-end: reshaping config data
Combining the utilities above makes it easy to reshape data. Here a flat keystore is transformed into a nested login record using find-templates and transform.
transform a keystore into a nested record
(transform {:keystore {:hash "{{hash}}"
:salt "{{salt}}"
:email "{{email}}"}
:db {:login {:user {:hash "{{hash}}"
:salt "{{salt}}"}
:value "{{email}}"}}}
[:keystore :db]
{:hash "1234"
:salt "ABCD"
:email "[email protected]"})
=> {:login {:user {:hash "1234"
:salt "ABCD"}
:value "[email protected]"}}
3 Type Predicates
v 3.0
(defn cons?
([x] (or (instance? clojure.lang.Cons x)
(instance? clojure.lang.ChunkedCons x))))
link
(cons? (cons 1 [1 2 3])) => true
v 3.0
(defn form?
([form]
(or (cons? form)
(list? form)
(lazy-seq? form)
(or (instance? clojure.lang.PersistentVector$ChunkedSeq form)))))
link
(form? '(+ 1 2)) => true (form? [1 2]) => false (form? {:a 1}) => false (form? "a") => false
4 Sequence Utilities
v 3.0
(defn queue
([] (clojure.lang.PersistentQueue/EMPTY))
([x] (conj (queue) x))
([x & xs] (apply conj (queue) x xs)))
link
(pop (queue 1 2 3 4)) => [2 3 4]
v 3.0
(defn seqify
([x]
(if (or (coll? x)
(instance? java.util.List x))
x
[x])))
link
(seqify 1) => [1] (seqify [1]) => [1]
5 Map Operations
v 3.0
(defn map-entries
([f m]
(->> (map f m)
(into {}))))
link
(map-entries (fn [[k v]] [(keyword (str v)) (name k)]) {:a 1 :b 2 :c 3}) => {:1 "a", :2 "b", :3 "c"}
v 3.0
(defn map-juxt
([[kf vf] arr]
(->> (map (juxt kf vf) arr)
(into {}))))
link
(map-juxt [str inc] [1 2 3 4 5]) => {"1" 2, "2" 3, "3" 4, "4" 5, "5" 6}
v 3.0
(defn map-keys
([f m]
(reduce (fn [out [k v]]
(assoc out (f k) v))
{}
m)))
link
(map-keys inc {0 :a 1 :b 2 :c}) => {1 :a, 2 :b, 3 :c}
v 3.0
(defn map-vals
([f m]
(reduce (fn [out [k v]]
(assoc out k (f v)))
{}
m)))
link
(map-vals inc {:a 1 :b 2 :c 3}) => {:a 2, :b 3, :c 4}
6 Map Filtering
v 3.0
(defn filter-keys
([pred m]
(reduce (fn [out [k v]]
(if (pred k)
(assoc out k v)
out))
{}
m)))
link
(filter-keys even? {0 :a 1 :b 2 :c}) => {0 :a, 2 :c}
v 3.0
(defn filter-vals
([pred m]
(reduce (fn [out [k v]]
(if (pred v)
(assoc out k v)
out))
{}
m)))
link
(filter-vals even? {:a 1 :b 2 :c 3}) => {:b 2}
7 Key Operations
v 3.0
(defn qualified-keys
([m]
(filter-keys (fn [k] (and (keyword? k)
(namespace k)))
m))
([m ns]
(let [pred (set (map h/strn (seqify ns)))]
(filter-keys (fn [k] (and (keyword? k)
(pred (namespace k))))
m))))
link
(qualified-keys {:a 1 :ns/b 1}) => #:ns{:b 1} (qualified-keys {:a 1 :ns.1/b 1 :ns.2/c 1} :ns.1) => #:ns.1{:b 1}
v 3.0
(defn qualify-keys
([m ns]
(let [ns (h/strn ns)]
(map-keys (fn [k]
(if (and (keyword? k)
(not (namespace k)))
(keyword ns (name k))
k))
m))))
link
(qualify-keys {:a 1} :ns.1) => #:ns.1{:a 1} (qualify-keys {:a 1 :ns.2/c 1} :ns.1) => {:ns.1/a 1, :ns.2/c 1}
v 4.0
(defn rename-keys
([m trs]
(reduce (fn [m [from to]]
(if (contains? m from)
(assoc (dissoc m from)
to (get m from))
m))
m
trs)))
link
(rename-keys {:a 1} {:a :b}) => {:b 1}
v 3.0
(defn unqualified-keys
([m]
(filter-keys (fn [k] (and (keyword? k)
(nil? (namespace k))))
m)))
link
(unqualified-keys {:a 1 :ns/b 1}) => {:a 1}
v 3.0
(defn unqualify-keys
([m]
(map-keys (fn [k] (keyword (name k))) m))
([m ns]
(let [ns (h/strn ns)]
(map-keys (fn [k]
(if (and (keyword? k)
(= (namespace k) ns))
(keyword (name k))
k))
m))))
link
(unqualify-keys {:a 1 :ns.1/b 1 :ns.2/c 1}) => {:a 1, :b 1, :c 1} (unqualify-keys {:a 1 :ns.1/b 1 :ns.2/c 1} :ns.1) => {:a 1, :b 1, :ns.2/c 1}
8 Nested Operations
v 3.0
(defn assoc-new
([m k v]
(if (contains? m k) m (assoc m k v)))
([m k v & more]
(apply assoc-new (assoc-new m k v) more)))
link
(assoc-new {:a 1} :b 2) => {:a 1 :b 2} (assoc-new {:a 1} :a 2 :b 2) => {:a 1 :b 2}
v 3.0
(defn dissoc-nested
([m [k & ks]]
(if-not ks
(dissoc m k)
(let [nm (dissoc-nested ((or m {}) k) ks)]
(cond (empty? nm) (dissoc m k)
:else (assoc m k nm))))))
link
(dissoc-nested {:a {:b {:c 1}}} [:a :b :c]) => {}
v 3.0
(defn flatten-nested
([x]
(filter (complement coll?)
(rest (tree-seq coll? seq x)))))
link
(flatten-nested [1 2 #{3 {4 5}}]) => [1 2 3 4 5]
v 3.0
(defn merge-nested
([& maps]
(apply merge-with (fn [& args]
(if (every? #(or (map? %) (nil? %)) args)
(apply merge-nested args)
(last args)))
maps)))
link
(merge-nested {:a {:b {:c 3}}} {:a {:b 3}}) => {:a {:b 3}} (merge-nested {:a {:b {:c 1 :d 2}}} {:a {:b {:c 3}}}) => {:a {:b {:c 3 :d 2}}}
merge-nested-new ^
[& maps]
merges nested values from left to right, provided the merged value does not exist
v 3.0
(defn merge-nested-new
([& maps]
(apply merge-nested (reverse maps))))
link
(merge-nested-new {:a {:b 2}} {:a {:c 2}}) => {:a {:b 2 :c 2}} (merge-nested-new {:b {:c :old}} {:b {:c :new}}) => {:b {:c :old}}
9 Tree Operations
v 3.0
(defn find-templates
([m]
(find-templates m [] {}))
([m path saved]
(let [template? (fn [^String s]
(and (string? s)
(.startsWith s "{{")
(.endsWith s "}}")))]
(reduce-kv (fn [out k v]
(cond (template? v)
(assoc out v (conj path k))
(map? v)
(find-templates v (conj path k) out)
:else
out))
saved
m))))
link
(find-templates {:hash "{{hash}}" :salt "{{salt}}" :email "{{email}}" :user {:firstname "{{firstname}}" :lastname "{{lastname}}"}}) => {"{{hash}}" [:hash] "{{salt}}" [:salt] "{{email}}" [:email] "{{firstname}}" [:user :firstname] "{{lastname}}" [:user :lastname]}
v 3.0
(defn reshape
([m rels]
(reduce (fn [out [to from]]
(if (= to from)
out
(let [v (get-in m from)]
(-> out
(dissoc-nested from)
(assoc-in to v)))))
m
rels)))
link
(reshape {:a 1 :b 2} {[:c :d] [:a]}) => {:b 2, :c {:d 1}}
v 3.0
(defn transform
([schema [from to] data]
(let [f (transform-fn* schema [from to])]
(f data))))
link
(transform {:keystore {:hash "{{hash}}" :salt "{{salt}}" :email "{{email}}"} :db {:login {:user {:hash "{{hash}}" :salt "{{salt}}"} :value "{{email}}"}}} [:keystore :db] {:hash "1234" :salt "ABCD" :email "[email protected]"}) => {:login {:user {:hash "1234", :salt "ABCD"}, :value "[email protected]"}}
v 3.0
(defn tree-flatten
([m]
(tree-flatten m h/*sep*))
([m sep]
(tree-flatten m sep []))
([m sep path]
(->> (keep (fn [[k v]]
(if (and (map? v) (not-empty v))
(tree-flatten v sep (conj path k))
(if (or (not (coll? v))
(not-empty v))
[(->> (conj path k)
(map h/strn)
(clojure.string/join sep)
keyword) v])))
m)
(into {}))))
link
(->> (tree-flatten {"a" {"b" {"c" 3 "d" 4} "e" {"f" 5 "g" 6}} "h" {"i" {}}}) (map-keys h/strn)) => {"a/b/c" 3, "a/b/d" 4, "a/e/f" 5, "a/e/g" 6}
v 3.0
(defn tree-nestify
([m]
(tree-nestify m h/*sep*))
([m sep]
(tree-nestify m sep identity))
([m sep f]
(let [pattern (re-create sep)]
(reduce-kv (fn [m k v]
(let [path (->> (clojure.string/split (h/strn k) pattern)
(map keyword))
v (f v)]
(update-in m path
(fnil (fn [p]
(cond (and (map? p) (map? v))
(merge p v)
:else v))
v))))
{}
m))))
link
(tree-nestify {:a/b 2 :a/c 3}) => {:a {:b 2 :c 3}} (tree-nestify {:a/b {:e/f 1} :a/c {:g/h 1}}) => {:a {:b {:e/f 1} :c {:g/h 1}}}
10 Diff Operations
v 3.0
(defn diff
([m1 m2] (diff m1 m2 false))
([m1 m2 reversible]
(let [diff (hash-map :+ (diff:new m1 m2)
:- (diff:new m2 m1)
:> (diff:changes m1 m2))]
(if reversible
(assoc diff :< (diff:changes m2 m1))
diff))))
link
(diff {:a 2} {:a 1}) => {:+ {} :- {} :> {[:a] 2}} (diff {:a {:b 1 :d 3}} {:a {:c 2 :d 4}} true) => {:+ {[:a :b] 1} :- {[:a :c] 2} :> {[:a :d] 3} :< {[:a :d] 4}}
v 3.0
(defn diff:changed
([new old]
(->> (diff new old)
((juxt :> :+))
(apply merge)
(reduce-kv (fn [out ks v]
(assoc-in out ks v))
{}))))
link
(diff:changed {:a {:b {:c 3 :d 4}}} {:a {:b {:c 3}}}) => {:a {:b {:d 4}}}
diff:changes ^
[m1 m2] [m1 m2 arr equal-fn]
finds changes in nested maps, does not consider new elements
v 3.0
(defn diff:changes
([m1 m2]
(diff:changes m1 m2 [] =))
([m1 m2 arr equal-fn]
(reduce-kv (fn [out k1 v1]
(if (contains? m2 k1)
(let [v2 (get m2 k1)]
(cond (and (hash-map? v1) (hash-map? v2))
(merge out (diff:changes v1 v2 (conj arr k1) equal-fn))
(equal-fn v1 v2)
out
:else
(assoc out (conj arr k1) v1)))
out))
{}
m1)))
link
(diff:changes {:a 2} {:a 1}) => {[:a] 2} (diff:changes {:a {:b 1 :c 2}} {:a {:b 1 :c 3}}) => {[:a :c] 2} (diff:changes {:a 1 :b 2 :c 3} {:a 1 :b 2}) => {} (diff:changes {:a 1 :b 2} {:a 1 :b 2 :c 3}) => {} (diff:changes {:a 1} {:a nil}) => {[:a] 1} (diff:changes {:a nil} {:a 1}) => {[:a] nil} (diff:changes {:a true} {:a false}) => {[:a] true} (diff:changes {:a false} {:a true}) => {[:a] false}
diff:new ^
[m1 m2] [m1 m2 arr]
finds new elements in nested maps, does not consider changes
v 3.0
(defn diff:new
([m1 m2]
(diff:new m1 m2 []))
([m1 m2 arr]
(reduce-kv (fn [out k1 v1]
(let [v2 (get m2 k1)]
(cond (and (hash-map? v1) (hash-map? v2))
(merge out (diff:new v1 v2 (conj arr k1)))
(not (contains? m2 k1))
(assoc out (conj arr k1) v1)
:else out)))
{}
m1)))
link
(diff:new {:a 2} {:a 1}) => {} (diff:new {:a {:b 1}} {:a {:c 2}}) => {[:a :b] 1} (diff:new {:a {:b 1 :c 2}} {:a {:b 1 :c 3}}) => {} (diff:new {:a 1 :b 2 :c 3} {:a 1 :b 2}) => {[:c] 3} (diff:new {:a 1 :b 2} {:a 1 :b 2 :c 3}) => {}
v 3.0
(defn diff:patch
([m diff]
(->> m
(#(reduce-kv (fn [m arr v]
(update-in m arr merge-or-replace v))
%
(merge (:+ diff) (:> diff))))
(#(reduce (fn [m arr]
(dissoc-nested m arr))
%
(keys (:- diff)))))))
link
(let [m1 {:a {:b 1 :d 3}} m2 {:a {:c 2 :d 4}} df (diff m2 m1)] (diff:patch m1 df)) => {:a {:c 2 :d 4}}
v 3.0
(defn diff:unpatch
([m diff]
(->> m
(#(reduce-kv (fn [m arr v]
(update-in m arr merge-or-replace v))
%
(merge (:- diff) (:< diff))))
(#(reduce (fn [m arr]
(dissoc-nested m arr))
%
(keys (:+ diff)))))))
link
(let [m1 {:a {:b 1 :d 3}} m2 {:a {:c 2 :d 4}} df (diff m2 m1 true)] (diff:unpatch m2 df)) => {:a {:b 1 :d 3}}
11 Collection Manipulation
v 3.0
(defn deduped?
[coll]
(= (count (set coll))
(count coll)))
link
(deduped? [1 2 3 4]) => true (deduped? [1 2 1 4]) => false
v 3.0
(defn element-at
([pred coll]
(loop [[x & more :as coll] coll]
(cond (empty? coll) nil
(pred x) x
:else
(recur more)))))
link
(element-at keyword? [1 2 :hello 4]) => :hello
v 3.0
(defn index-at
([pred coll]
(loop [[x & more :as coll] coll
i 0]
(cond (empty? coll) -1
(pred x) i
:else
(recur more (inc i))))))
link
(index-at even? [1 2 3 4]) => 1 (index-at keyword? [1 2 :hello 4]) => 2
insert-at ^
[coll i new] [coll i new & more]
insert one or more elements at the given index
v 3.0
(defn insert-at
([coll i new]
(apply conj (subvec coll 0 i) new (subvec coll i)))
([coll i new & more]
(apply conj (subvec coll 0 i) new (concat more (subvec coll i)))))
link
(insert-at [:a :b] 1 :b :c) => [:a :b :c :b]
v 3.0
(defn remove-at
([coll i]
(remove-at coll i 1))
([coll i n]
(cond (vector? coll)
(reduce conj
(subvec coll 0 i)
(subvec coll (+ i n) (count coll)))
:else
(keep-indexed #(if (not (and (>= %1 i)
(< %1 (+ i n)))) %2) coll))))
link
(remove-at [:a :b :c :d] 2) => [:a :b :d]
v 4.0
(defn split-by
[pred coll]
(loop [remaining coll
current-chunk []
result []]
(if (empty? remaining)
(if (empty? current-chunk)
result
(conj result current-chunk))
(let [item (first remaining)]
(if (pred item)
(recur (rest remaining)
[]
(conj result current-chunk))
(recur (rest remaining)
(conj current-chunk item)
result))))))
link
(split-by #(= % :split) [:a :b :split :c :d :split :e]) => [[:a :b] [:c :d] [:e]] (split-by even? [1 3 5 2 7 9 4 11]) => [[1 3 5] [7 9] [11]] (split-by #(= % :split) [:split :a :b :split :c]) => [[] [:a :b] [:c]] (split-by #(= % :split) [:a :b :c]) => [[:a :b :c]]
12 std.lib.collection: A Comprehensive Summary
The std.lib.collection namespace provides a rich set of utility functions for working with Clojure's core data structures (maps, sequences, vectors, sets). It extends the functionality of clojure.core with specialized operations for nested data manipulation, key/value transformations, sequence processing, and advanced map differencing and patching. This module aims to simplify common collection-related tasks and provide more powerful tools for data transformation.
12.1 Core Concepts:
- Collection Predicates: Functions to check the type of various collections (e.g.,
hash-map?,lazy-seq?,cons?,form?).n Sequence Utilities: Functions for manipulating sequences, includingseqify,unseqify,unlazy,insert-at,remove-at,deduped?, andunfold.n Map Transformations: Extensive functions for transforming map keys, values, and entries, includingmap-keys,map-vals,map-juxt,map-entries,pmap-vals,pmap-entries,rename-keys,filter-keys,filter-vals,keep-vals,transpose.n Namespaced Key Handling: Utilities for working with namespaced keywords in maps, such asqualified-keys,unqualified-keys,qualify-keys,unqualify-keys.n Nested Map Operations: Powerful functions for merging, associating, and disassociating values in nested maps, includingmerge-nested,merge-nested-new,dissoc-nested,flatten-nested,tree-flatten,tree-nestify,tree-nestify:all.n Map Differencing and Patching: A sophisticated set of functions (diff,diff:changes,diff:new,diff:changed,diff:patch,diff:unpatch) for comparing two maps, identifying differences, and applying/reverting patches.n Data Transformation Pipelines: Functions likereshapeandtransform(withfind-templates) for defining and applying complex data transformations based on schemas or templates.
12.2 Key Functions:
hash-map?,lazy-seq?,cons?,form?: Predicates for checking collection types.nqueue: Creates aclojure.lang.PersistentQueue.nseqify,unseqify: Convert non-sequences to sequences and vice-versa.nunlazy: Forces evaluation of a lazy sequence.nmap-keys,map-vals,map-juxt,map-entries: Apply functions to map keys, values, or entries.pmap-valsandpmap-entriesprovide parallel versions.nrename-keys: Renames keys in a map based on a mapping.nfilter-keys,filter-vals,keep-vals: Filter map entries based on keys or values.nqualified-keys,unqualified-keys,qualify-keys,unqualify-keys: Manipulate namespaced keywords in maps.nassoc-new: Associates a key-value pair only if the key is not already present or its value isnil.nmerge-nested,merge-nested-new: Recursively merge nested maps.merge-nested-newonly merges if the target key doesn't exist.ndissoc-nested: Recursively disassociates keys from nested maps, removing empty intermediate maps.nflatten-nested: Flattens all elements of a collection into a single sequence.ntree-flatten,tree-nestify,tree-nestify:all: Convert between flat (path-based keys) and nested map representations.nreshape: Moves values within a map according to a specified transformation table.nfind-templates,transform-fn,transform: Tools for defining and applying data transformations using template strings.nempty-record: Creates an empty instance of adefrecord.ntranspose: Swaps keys and values in a map.nindex-at,element-at: Find the index or element matching a predicate in a collection.ninsert-at,remove-at: Insert or remove elements at a specific index in a vector.ndeduped?: Checks if all elements in a collection are unique.nunfold: Generates a sequence by repeatedly applying a function to a seed value.n*diff,diff:changes,diff:new,diff:changed,diff:patch,diff:unpatch: Functions for computing differences between nested maps and applying/reverting those differences.
12.3 Usage Pattern:
This namespace is a utility belt for any Clojure project that deals heavily with data manipulation. It's particularly valuable for:
- Configuration Management: Merging and transforming configuration maps.n State Management: Efficiently updating and tracking changes in complex application state.n Data Processing Pipelines: Building transformations for data flowing through an application.n API Development: Reshaping data structures for different API endpoints or external systems.n Metaprogramming: Manipulating data structures that represent code or schemas.
By providing these powerful and often recursive collection utilities, std.lib.collection significantly enhances Clojure's already strong data-oriented programming capabilities.