https://github.com/escherize/tracks.git
git clone 'https://github.com/escherize/tracks.git'
(ql:quickload :escherize.tracks)
We become what we behold. We shape our tools, and thereafter our tools shape us.
― Marshall McLuhan
Add the following line to your leiningen dependencies:
Require tracks in your namespace header:
(:require [tracks.core :as t :refer [track]])
This is a library dedicated to the concept of shape.
shape n.
- the external form, contours, or outline of something.
- the correct or original form or contours of something.
- an example of something that has a particular form.
shape v.
- to give definite form, organization, or character to.
- fashion or form.
It's common to grapple with large maps whose shapes are uncomfortable to reason about.
tracks
simplifies transformations and destructuring of Clojure datastructures. Instead of describing how to do a transformation, tracks allows the user to create those transformations by example. This makes writing complex code that takes one shape and transforms them to another dead simple.
Destructuring complex nested data structures can be a real pain. Tracks makes this easy. Much like clojure.core/let
, symbols in the track pattern will be bound to the value and available the body. Unlike clojure.core/let
we supply a binding form of the same shape as the data we are interested in.
(t/let [{:a {:b [greeting person]}} ;;<- binding form
{:a {:b ["Hello" "World"]}} ;;<- data we want to get at
]
(str greeting " " person "!"))
;;=> "Hello World!"
(t/let [{:a {:b x} :c {:d y}}
{:a {:b 1} :c {:d 2}}]
(+ x y))
;;=> 3
track
returns a function which takes data of the shape of its first argument.
Below, the function returned by track
will take a map with keys :a
and :b
and move the value at :a
to :b
, and the value at :b
to :a
:
(track {:a one :b two}
{:a two :b one})
;;=> anonymous fn
(def swap-a-b (track {:a one :b two}
{:a two :b one}))
(swap-a-b {:a 100 :b 3000})
;;=> {:a 3000 :b 100}
deftrack
does the same thing, but binds it too:
(deftrack swap-a-b {:a one :b two} {:a two :b one})
(swap-a-b {:a 100 :b 3000})
;;=> {:a 3000 :b 100}
We can move positions in vectors and deeply nested maps in exactly the same way:
((track {:a [zero one]}
{:b [one zero]})
{:a [:zero :one]})
;; => {:b [:one :zero]}
Deep thinking about deeply nested shapes is a bygone era:
(deftrack deeptx
{0 zero, 1 one, 2 two, 3 three} ;; <- deeptx takes a map with this shape
{:a zero :b {:c one :d {:e two :f {:g three}}}} ;; <- deeptx then returns one with this shape
)
(deeptx {0 "first" 1 "second" 2 "third" 3 "fourth"})
;;=> {:a "first", :b {:c "second", :d {:e "third", :f {:g "fourth"}}}}
track
greatly simplifies rotating values, too:
Let's simulate a game where there's an active player, and all other players wait in line to become the active one. Once a player has played their turn, they go to the back of the line.
;;; Setup the function that moves around players,
;;; no matter what datastructure the players are
;;; represented as:
(deftrack move-players
{:active-player p1 :players [p2 p3 p4]}
{:active-player p2 :players [p3 p4 p1]})
;;; Here's the datastructure that represents the state of the game.
;;; Notice that the players are more than scalar values!
(defonce game (atom {:active-player {:name "A"}
:players [{:name "B"}
{:name "C"}
{:name "D"}]}))
(swap! game move-players)
;;=> {:active-player {:name "B"}
;; :players [{:name "C"}
;; {:name "D"}
;; {:name "A"}]}
(swap! game move-players)
;;=> {:active-player {:name "C"}
;; :players [{:name "D"}
;; {:name "A"}
;; {:name "B"}]}
(swap! game move-players)
;;=> {:active-player {:name "D"}
;; :players [{:name "A"}
;; {:name "B"}
;; {:name "C"}]}
Like a train track, sometimes one track can split into many. With track
the values can be duplicated.
(deftrack one-to-many x {:a x :b {:c [x x]}})
(one-to-many "?")
;;=> {:a "?", :b {:c ["?" "?"]}}
track
is implemented in terms of let
(def move-a-key (track {:x one} {:y one}))
(move-a-key {:x "MoveMe"})
;;=> {:y "MoveMe"}
(move-a-key {:x [:a :b :c]})
;;=> {:y [:a :b :c]}
We see it moves any value from keypath [:x] to keypath [:y].
The way it does it is by moving {:x one}
into a let
like so:
;; vvvvvvvv---- this is the first arg to track
(tracks/let [{:x one} input]
;; so now one is bound to (get input :x)
;; vvvvvv---- this is the 2nd arg to track
{:y one})
Check the test namespace!
Copyright © 2016 Bryan Maass
Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.