Saturday, August 20, 2011

Clojure learning assignment: destructuring

I've decided to do a once weekly at least foray into topics in clojure to both help myself and others learn this fascinating language.  I do not really have a lisp background (I do not count the few hundred lines of code I wrote in an AI class a long time ago), and thus clojure has been a bit of a mind-warping stretch for me.  Here are some of the topics I will eventually cover:

1.  Destructuring
2.  Coverage of the "cheat sheet" functions with examples
3.  Lazy sequences vs. recursion
4.  Macros (once I've figured them out myself)
5.  A multithreaded merge sort using refs
6.  A multithreaded tree traversal
7.  Examples of gen-class and proxy
8.  Examples of using defrecords


And anything else I can think of.  Mind you, I'm still learning this language in many ways as I go.  What I hope that I can provide that the clojurian master's may not be able to, is the perspective of a newbie to the lisp and functional programming world.  I personally learn through real-world examples, and while the books The Joy of Clojure, Programming Clojure, and Clojure in Action have all been very helpful, sometimes I wish there had been a little bit more attention paid to the small things.  Just try and do a (doc ->>) and you will know what I mean.  But anyhow....off to my first lesson, destructuring.


I often hear that Clojure (and lisps) have very little syntax, and they tout this as a defining feature of the language.  While to a degree that's true compared to many other languages, there ARE more syntax rules than may appear at first glance.  Take for example code like this


   1 (defn get-parts 
   2   [ [x y z & others ] ]
   3     (do 
   4       (println "First three are: " x y z)
   5       (println "Rest is: " others))
   6     others)

Normally, the first thing you'll see after the function name is the argument list (or possibly a docstring or metadata see here).  But that's a kind of strange looking argument list.  What am I supposed to pass in there?  It kind of looks like I pass in an array of symbols...but what's that ampersand doing there?

We can run it like this:

user=> (get-parts [ 1 2 3 4 ] )
First three are:  1 2 3
Rest is:  (4)
(4)


This is one of clojure's destructuring forms which is loosely akin to pattern matching found in other languages.  The above code takes in a sequence of some form, splits out the first three values into x, y, and z respectively, and then stuffs the remainder of the sequence into others.  It would be code equivalent to this:

 1 (defn get-parts-no-dest
 2   [ s ]
 3   (let [ x (nth s 1)
 4          y (nth s 2)
 5          z (nth s 3)
 6          others (drop 3 s) ]
 7     (do
 8       (println "First three are: " x y z)
 9       (println "Rest is: " others))
10     others))

As you can see, the destructuring above did cut down on some lines of code...if at the price of some readability in my opinion.  Unfortunately, using destructuring seems to be the preferred idiomatic clojure style.

So the above example works well for a vector as well as a list or sequence.  It will not however work on a map of any sort.  If we try, we will get this:


user=> (get-parts { :1 1 :2 2 :3 3 :4 4} )
java.lang.UnsupportedOperationException: nth not supported on this type: PersistentArrayMap (NO_SOURCE_FILE:0)


So are there destructuring forms for maps?  Of course.  Here's an example where we take a map containing the keys fname, address and city, print them and return a vector of the values of those keys:

1 (defn get-parts-map
2   "Takes a map with keys fname, address and city and prints them"
3   [ {:keys  [fname address city]  } ]
4   (do
5     (println "Name: " fname)
6     (println "Address: " address)
7     (println "City: " city))
8   [ fname address city ])


If we called it with a map like { :fname "John Doe" :address "1234 Cherry Lane" :city "Timbuktu" }, we would see this:


user=> (def john_doe { :fname "John Doe" :address "1234 Cherry Lane" :city "Timbuktu" } )
#'user/john_doe
user=> (get-parts-map john_doe)
Name:  John Doe
Address:  1234 Cherry Lane
City:  Timbuktu
["John Doe" "1234 Cherry Lane" "Timbuktu"]


Notice how we used the :keys keyword and followed it with a vector of symbols, and not keywords.  Keep that in mind when destructuring using maps.  Also, you can use these destructuring features in let forms as well.  For example, I could have written the code above like this:

 1 (defn get-parts-map-w-let
 2   "Takes a map with keys fname, address and city and prints them"
 3   [ m ]
 4   (let [ {:keys  [fname address city]  }  m ]
 5     (do
 6       (println "Name: " fname)
 7       (println "Address: " address)
 8       (println "City: " city))
 9     [ fname address city ]))
10 

And the output would be exactly the same as above.  As well as the :keys directive, you may use :syms, if the keys are symbols (instead of keywords) or :strs (if the keys are strings).


The other useful destructuring form is to associate a map with the elements of a sequence.  For example, you could do something like this:

1 (let [ { dog 0 cat 1} [ "husky" "persian" "pug" "siamese" ] ]
2   (println "Dog is a " dog " and cat is a " cat))


This would print out "Dog is a husky and cat is a persian

No comments:

Post a Comment