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

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
Remco van 't Veer <2014-04-15 Tue>