tracks | clojure spec - blog post

parent adf4954d
Pipeline #42924985 passed with stages
in 34 seconds
......@@ -5,9 +5,12 @@
'(
backward-sexp
crux-move-beginning-of-line
electric-newline-and-maybe-indent
forward-sexp
markdown-electric-backquote
markdown-outdent-or-delete
sp-backward-delete-char
sp-forward-sexp
))
(setq mc/cmds-to-run-once
......
---
title: Spec Select and Tracks
subtitle: ""
tags: []
date: "2019-01-10"
---
`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](https://github.com/escherize/tracks). Here's a link to that part of the talk:
<div style="text-align: center">
<iframe width="600" height="372" src="http://www.youtube.com/embed/YR5WdGrpoug??t=2405color=white&amp;theme=light"></iframe>
</div>
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:
{{<highlight clojure "linenos=table,linenostart=1">}}
(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]))
{{< / highlight >}}
Let's define an example user, and make sure we got it right with `s/valid?`:
{{<highlight clojure "linenos=table,linenostart=1">}}
(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
{{< / highlight >}}
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](https://www.youtube.com/watch?v=2V1FtfBDsLU).
#### Vanilla - the simplest way
{{<highlight clojure "linenos=table,linenostart=1">}}
(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"
{{</highlight>}}
But, it's more idiomatic to add destructuring.
#### A little desctructuring
{{<highlight clojure "linenos=table,linenostart=1">}}
(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"
{{</highlight>}}
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
{{<highlight clojure "linenos=table,linenostart=1">}}
(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"
{{</highlight>}}
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](https://github.com/escherize/tracks)
{{<highlight clojure "linenos=table,linenostart=1">}}
(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"
{{</highlight>}}
Please notice the map that spans **lines 2 and 3** above.
Here it is again:
{{<highlight clojure>}}
{:id user-id :addr {:zip zipcode}}
{{</highlight>}}
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:
{{<highlight clojure "linenos=table,linenostart=1">}}
(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}"
{{</highlight>}}
**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](https://github.com/escherize/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/select**ing 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](/select.clj).
......@@ -29,3 +29,7 @@
### [Hiccup space](../hiccup.space)
- Simple example of html as data.
<hr>
... and a whole lot more that didn't make the cut!
(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]))
(def example-user
{:id 1,
:first "George",
:last "O' Jungle",
:addr
{:street "123 Lemon Ln.", :city "Chapel Hill", :state "NC", :zip 12345}})
;; as it is now:
(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"
(defn get-movie-times-destructuring
[{{zipcode :zip} :addr, user-id :id}]
(str "to get movie times we need: " user-id " and " zipcode))
(get-movie-times-destructuring example-user)
;; => "to get movie times we need: 1 and 12345"
;;## now, with tracks:
(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))
(get-movie-times-tracks example-user)
;; => "to get movie times we need: 1 and 12345"
(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 " to: " (pr-str addr)))
(place-order example-user)
;; => "order placed for: George O' Jungle to: {:street \"123 Lemon Ln.\", :city
;; \"Chapel Hill\", :state \"NC\", :zip 12345}"
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment