Maybe Not covers a lot of ground, and I want to explore the declaration of destructuring contexts. What does that mean? Read on to find out!

Let’s explore a specific part of the talk and see how it compared to this shape-based destructuring library of mine called Tracks. Here’s a link to that part of the talk:

Many times only a subset of a map is needed to perform an action. We will discuss two functions: get-movie-times and place-order which both accept the same shape of data, but depend on different pieces of it!

I recreated the example spec from the talk here:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
(ns scratch.select
  (:require [tracks.core :as t]
            [clojure.spec.alpha :as s]))

(s/def ::street string?)
(s/def ::city string?)
(s/def ::state string?)
(s/def ::zip int?)
(s/def ::addr (s/keys :req-un
                      [::street ::city ::state ::zip]))

(s/def ::id int?)
(s/def ::first string?)
(s/def ::last string?)
(s/def ::user (s/keys :req-un
                      [::id ::first ::last ::addr]))

Let’s define an example user, and make sure we got it right with s/valid?:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
(def example-user
  {:id 1,
   :first "George",
   :last "O' Jungle",
   :addr {:street "123 Lemon Ln.",
          :city "Chapel Hill",
          :state "NC",
          :zip 12345}})

(s/valid? ::user example-user)
;; => true

So, example-user checks out. Now let’s turn to the first function:

Get Movie Times

This function needs the user-id and the user’s zipcode, which is under the :addr key. We’ll make a toy function that returns a string. Assume this would be doing profitable and heroic work in your effective program.

Vanilla - the simplest way

1
2
3
4
5
6
7
8
9
(defn get-movie-times
  [user]
  (str "to get movie times we need: " (:id user)
       " and " (-> user
                   :addr
                   :zip)))

(get-movie-times example-user)
;; => "to get movie times we need: 1 and 12345"

But, it’s more idiomatic to add destructuring.

A little desctructuring

1
2
3
4
5
6
(defn get-movie-times
  [{:keys [addr id}]
  (str "to get movie times we need: " id " and " (:zip addr)))

(get-movie-times example-user)
;; => "to get movie times we need: 1 and 12345"

This is probably how I would write it - sort of half way between no destructuring and a lot of destructuring.

But why don’t more people (me included) follow through and destruct everything? Isn’t that a better way to describe a destructure context, compared to parsing our function and saying: ‘Wait a second, I only need the :zip of the addr.’?

Lots of desctructuring

1
2
3
4
5
6
(defn get-movie-times
  [{{zipcode :zip} :addr, user-id :id}]
  (str "to get movie times we need: " user-id " and " zipcode))

(get-movie-times example-user)
;; => "to get movie times we need: 1 and 12345"

So… on line 2 above, I had to actually google how to do this destructuring. Luckily for us we have an example-user laid out right infront of us, but often that’s not the case! So we need to study the destructuring form - thinking that could be used for better tasks - to figure out what sort of shape to pass in.

(This is the part of the infomercial where there are people struggling with a black and white filter and big red X’s.)

So let’s take a look at how this would be written using my library:

Using Tracks

1
2
3
4
5
6
(t/deftrack get-movie-times-tracks
  {:id user-id, ;; <--- the shape we need
  :addr {:zip zipcode}}

(str "to get movie times we need: " user-id " and " zipcode))
;; => "to get movie times we need: 1 and 12345"

Please notice the map that spans lines 2 and 3 above.

Here it is again:

{:id user-id :addr {:zip zipcode}}

That’s just a map with symbols that will be bound to the values found at those positions!

If you know how to write a Clojure map, then with Tracks, you also know how to destructure arbitrarily shaped bits of data.

Benefits of Tracks

Let me write another function from the talk here:

1
2
3
4
5
6
7
8
9
(t/deftrack place-order
  {:first fname,   ;; <-- the shape we need
   :last lname,
   :addr addr}     ;; <-- can (optionally) require all parts of address
  (str "order placed for: " fname " " lname " \nto: \n" (pr-str addr)))

;; => "order placed for: George O' Jungle
to:
{:street \"123 Lemon Ln.\", :city \"Chapel Hill\", :state \"NC\", :zip 12345}"

Lines 2-4 are the shape of data that we need from ::user, or the destructuring context. There can be any number of other keys and values and/or collections in the argument to place-order but they will be happily ignored.

Reminder: you can try out Tracks today!

Benefits of spec/select

Unlike tracks, spec/select can check that the pieces of data you declared are allowed in a clojure spec. I can see that being very useful!

Another interesting thing: In Tracks, the user is required to specify the to be bound, but with spec/select every key is unique – so maybe there can be a convention where:

keyword bound variable
:user/name user-name
:user.addr/zip user.addr-zip

Actually that seems kind of brittle - maybe there’s a better way?

Finally, Rich mentioned something about spec/selecting against nested collections of data. That sounds interesting, and is not something that Tracks can do.

Conclusion

I can’t wait to try out a new way to write software with the spec/select. We’ve all had the problem of having incomplete data that needs to fit a certain pattern once it comes into being, but as Rich mentioned there is no good way to do that today. It will be excellent when there is!

Appendix

  1. The clojure file is hosted here.