💾 Archived View for rwv.io › articles › clojure-destructuring.gmi captured on 2024-05-10 at 10:45:47. Gemini links have been rewritten to link to archived content

View Raw

More Information

⬅️ Previous capture (2023-01-29)

-=-=-=-=-=-=-

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