Clojure Destructuring aka "abstract structural binding"
These are my notes for a talk I prepared for a clojure user group meeting.
What is it?
Sometimes said to be clojures way to do named parameters but it's much more than that. It is a way to take apart a structure into multiple substructures typically when assigning variables.
Let's dive in with examples! It works on lists:
> (let [[x y z] [1 2 3]])
y)
2
> (let [[x y & z] [1 2 3 4]]
z)
(3 4)
and on maps:
> (let [{z :z} {:x 1 :y 2 :z 3}]
z)
3
It goes deeper into the structure:
> (let [{{x :x} :y} {:y {:x 1}}] ; symmetry!
x)
1
> (let [[_ _ [_ _ [x]]] [1 [2] [3 4 [5]]]]
x)
5
> (let [[_ _ [_ _ [{z :z}]]] [1 [2] [3 4 [{:x 1 :y 2 :z 3}]]]]
z)
3
and there's the fancy stuff like "keys", "strs" and "syms":
> (let [{:keys [x y]} {:x 1 :y 2}]
y)
2
> (let [{:strs [x y]} {"x" 1 "y" 2}]
y)
2
> (let [{:syms [x y]} {'x 1 'y 2}]
y)
2
Also "as" and "or":
> (let [[x y :as all] [1 2]]
[x all])
[1 [1 2]]
> (let [{:keys [x y z] :as all} {:x 1 :y 2}]
[x all])
[1 {:x 1, :y 2}]
> (let [{:keys [x y z] :or {z 3}} {:x 1 :y 2}]
z)
3
Can it be used outside of "let"?
Yes, many expressions support it, like "defn" or rather "fn":
> ((fn [{x :x}] x) {:x 5})
5
And many more, like "loop", "doseq":
> (loop [[val & coll] [1 2 3]]
(if (even? val)
val
(if (seq coll)
(recur coll))))
2
> (doseq [{x :x} [{:a 1} {:x 2}]]
(prn x))
nil
2
Anything in core having some kind of binding, params, seq-exprs supports this.
How does it work?
Let's investigate! What is "let"?
> (source let)
(defmacro let
"binding => binding-form init-expr
Evaluates the exprs in a lexical context in which the symbols in
the binding-forms are bound to their respective init-exprs or parts
therein."
{:added "1.0", :special-form true, :forms '[(let [bindings*] exprs*)]}
[bindings & body]
(assert-args let
(vector? bindings) "a vector for its binding"
(even? (count bindings)) "an even number of forms in binding vector")
`(let* ~(destructure bindings) ~@body))
So what happens when we use it?
> (macroexpand '(let [{x :x y :y} val]))
(let* [map__2604 val
map__2604 (if (clojure.core/seq? map__2604)
(clojure.core/apply clojure.core/hash-map map__2604)
map__2604)
x (clojure.core/get map__2604 :x)
y (clojure.core/get map__2604 :y)])
The destructure function?
> (destructure [{'x :x 'y :y} 'val])
[map__2173 val
map__2173 (if (clojure.core/seq? map__2173)
(clojure.core/apply clojure.core/hash-map map__2173)
map__2173)
x (clojure.core/get map__2173 :x)
y (clojure.core/get map__2173 :y)]
> (destructure [['a 'b '& 'c] [1 2 3 4]])
[vec__2020 [1 2 3 4]
a (clojure.core/nth vec__2020 0 nil)
b (clojure.core/nth vec__2020 1 nil)
c (clojure.core/nthnext vec__2020 2)]
Any seq will do?
> (let [{x :x} '(:x 1 :y 2)] x)
1
Really seq?
> ((fn [& x] x) 1)
(1)
> (seq? ((fn [& x] x) 1))
true
Aha! Finally! Named parameters!
> ((fn [& {x :x}] x) :x 1 :y 2)
1
Instead of the unspliced:
> ((fn [{x :x}] x) {:x 1 :y 2})
1
What about the doc string of my new function?
> (defn ^{:doc "bla bla bla" :arglist ["(:x SOME-X)? (:y SOME-Y)?"]}..
What about all the named parameters?
> ((fn [& {:as options}] options) :x 1 :y 2)
{:x 1 :y 2}
Only lists and maps?
> (source get)
(defn get
"Returns the value mapped to key, not-found or nil if key not present."
{:inline (fn [m k & nf] `(. clojure.lang.RT (get ~m ~k ~@nf)))
:inline-arities #{2 3}
:added "1.0"}
([map key]
(. clojure.lang.RT (get map key)))
([map key not-found]
(. clojure.lang.RT (get map key not-found))))
Hmm, let's look at RT.java:
static public Object get(Object coll, Object key){
if(coll instanceof ILookup)
return ((ILookup) coll).valAt(key);
return getFrom(coll, key);
}
static Object getFrom(Object coll, Object key){
if(coll == null)
return null;
else if(coll instanceof Map) {
Map m = (Map) coll;
return m.get(key);
}
else if(coll instanceof IPersistentSet) {
IPersistentSet set = (IPersistentSet) coll;
return set.get(key);
}
else if(key instanceof Number && (coll instanceof String || coll.getClass().isArray())) {
int n = ((Number) key).intValue();
if(n >= 0 && n < count(coll))
return nth(coll, n);
return null;
}
return null;
}
https://github.com/clojure/clojure/blob/clojure-1.7.0/src/jvm/clojure/lang/RT.java#L719
Anything we can "get" or "nth"; strings!
> (let [[_ x] "yelp"]
x)
\e
> (let [{x 2} "yelp"]
x)
\l
> (let [[_ & x] "yelp"]
(apply str x))
"elp"
> (let [[c & r] "yelp"]
(apply str (Character/toUpperCase c) r))
"Yelp"
Use your own types!
> (deftype Stuff [n] clojure.lang.ILookup
(valAt [this k]
({:wings n
:coolness (* n 3.14)
:speed (/ n 1.33)} k)))
> (let [{speed :speed} (Stuff. 5)]
speed)
3.7593984962406015
Pretty cool, right? Thanks for listening.
--
📅 2014-04-15
📧 hello@rwv.io
CC BY-NC-SA 4.0