Showing posts with label destructuring. Show all posts
Showing posts with label destructuring. Show all posts

Tuesday, March 01, 2011

Clojure: if-let and when-let

I'm a fan of if-let and when-let. Both can be helpful in creating succinct, readable Clojure code. From the documentation:
If test is true, evaluates then with binding-form bound to the value of test, if not, yields else
A quick example should demonstrate the behavior.
user=> (if-let [a :anything] a "no")    
:anything
user=> (if-let [a nil] a "no")
"no"
user=> (if-let [a false] a "no")
"no"
The above example demonstrates a simple case where a truthy value causes the "then" form to be returned, and a falsey value (nil or false) causes the "else" form to be returned. The example also shows that "a" is bound to the value :anything and can be used within the "then".

The following example shows that you can put code in the "then" and "else" statement's as well.
user=> (if-let [a 4] (+ a 4) (+ 10 10)) 
8
user=> (if-let [a nil] (+ a 4) (+ 10 10))
20
Just like above, when "a" is truthy the value is bound and it can be used as desired in the "then".

The when-let function behaves basically the same way, except there is no "else".
user=> (when-let [a 9] (+ a 4))  
13
user=> (when-let [a nil] (+ a 4))
nil
user=> (when-let [a 9] (println a) (+ a 4))
9
13
The example demonstrates that then "then" case is very similar to if-let; however, if the test fails the when-let function simply returns nil. The last when-let example also shows how it slightly differs from an if-let: you can pass as many forms to the "then" as you'd like. If the test evaluates to true, all of the additional forms will be evaluated.

There are a few things worth knowing about if-let and when-let with respect to the bindings. Both if-let and when-let require a vector for their bindings, and there must be exactly two forms in the binding vector. The following example shows Clojure's response if you choose not to follow those rules.
user=> (when-let (a 9) (println a))
java.lang.IllegalArgumentException: when-let requires a vector for its binding (NO_SOURCE_FILE:0)
user=> (when-let nil (println "hi"))
java.lang.IllegalArgumentException: when-let requires a vector for its binding (NO_SOURCE_FILE:0)
user=> (when-let [a 9 b nil] (println a b))
java.lang.IllegalArgumentException: when-let requires exactly 2 forms in binding vector (NO_SOURCE_FILE:0)
It's nice to know if-let and when-let, but they aren't exactly hard concepts to follow. Once someone points them out to you, I expect you'll be able to easily use them in your code without much effort.

However, what is the impact of destructuring on if-let and when-let? I know what I expected, but I thought it was worth a quick trip to the REPL to make sure my assumptions were correct. The following code shows that as long as the value being bound is truthy the "then" will be executed - destructuring values have their normal behavior and do no effect execution flow.
user=> (when-let [{a :a} {:a 1}] [a])
[1]
user=> (when-let [{a :a} {}] [a])
[nil]
user=> (when-let [{a :a} {:a false}] [a])
[false]
user=> (when-let [{a :a} nil] [a])
nil
As you would expect the when-let is consistent and the destructuring behavior is also consistent.

if-let and when-let are good, give them a shot.

Tuesday, July 27, 2010

Clojure: Destructuring

In The Joy of Clojure (TJoC) destructuring is described as a mini-language within Clojure. It's not essential to learn this mini-language; however, as the authors of TJoC point out, destructuring facilitates concise, elegant code.

What is destructuring?
Clojure supports abstract structural binding, often called destructuring, in let binding lists, fn parameter lists, and any macro that expands into a let or fn. -- http://clojure.org/special_forms
The simplest example of destructuring is assigning the values of a vector.
user=> (def point [5 7])
#'user/point

user=> (let [[x y] point]
(println "x:" x "y:" y))
x: 5 y: 7
note: I'm using let for my examples of destructuring; however, in practice I tend to use destructuring in function parameter lists at least as often, if not more often.

I'll admit that I can't remember ever using destructuring like the first example, but it's a good starting point. A more realistic example is splitting a vector into a head and a tail. When defining a function with an arglist** you use an ampersand. The same is true in destructuring.
user=> (def indexes [1 2 3])
#'user/indexes

user=> (let [[x & more] indexes]
(println "x:" x "more:" more))
x: 1 more: (2 3)
It's also worth noting that you can bind the entire vector to a local using the :as directive.
user=> (def indexes [1 2 3])
#'user/indexes

user=> (let [[x & more :as full-list] indexes]
(println "x:" x "more:" more "full list:" full-list))
x: 1 more: (2 3) full list: [1 2 3]
Vector examples are the easiest; however, in practice I find myself using destructuring with maps far more often.

Simple destructuring on a map is as easy as choosing a local name and providing the key.
user=> (def point {:x 5 :y 7})
#'user/point

user=> (let [{the-x :x the-y :y} point]
(println "x:" the-x "y:" the-y))
x: 5 y: 7
As the example shows, the values of :x and :y are bound to locals with the names the-x and the-y. In practice we would never prepend "the-" to our local names; however, using different names provides a bit of clarity for our first example. In production code you would be much more likely to want locals with the same name as the key. This works perfectly well, as the next example shows.
user=> (def point {:x 5 :y 7})
#'user/point

user=> (let [{x :x y :y} point]
(println "x:" x "y:" y))
x: 5 y: 7
While this works perfectly well, creating locals with the same name as the keys becomes tedious and annoying (especially when your keys are longer than one letter). Clojure anticipates this frustration and provides :keys directive that allows you to specify keys that you would like as locals with the same name.
user=> (def point {:x 5 :y 7})
#'user/point

user=> (let [{:keys [x y]} point]
(println "x:" x "y:" y))
x: 5 y: 7
There are a few directives that work while destructuring maps. The above example shows the use of :keys. In practice I end up using :keys the most; however, I've also used the :as directive while working with maps.

The following example illustrates the use of an :as directive to bind a local with the entire map.
user=> (def point {:x 5 :y 7})
#'user/point

user=> (let [{:keys [x y] :as the-point} point]
(println "x:" x "y:" y "point:" the-point))
x: 5 y: 7 point: {:x 5, :y 7}
We've now seen the :as directive used for both vectors and maps. In both cases the local is always assigned to the entire expression that is being destructured.

For completeness I'll document the :or directive; however, I must admit that I've never used it in practice. The :or directive is used to assign default values when the map being destructured doesn't contain a specified key.
user=> (def point {:y 7})
#'user/point

user=> (let [{:keys [x y] :or {x 0 y 0}} point]
(println "x:" x "y:" y))
x: 0 y: 7
Lastly, it's also worth noting that you can destructure nested maps, vectors and a combination of both.

The following example destructures a nested map
user=> (def book {:name "SICP" :details {:pages 657 :isbn-10 "0262011530"}})
#'user/book

user=> (let [{name :name {pages :pages isbn-10 :isbn-10} :details} book]
(println "name:" name "pages:" pages "isbn-10:" isbn-10))
name: SICP pages: 657 isbn-10: 0262011530
As you would expect, you can also use directives while destructuring nested maps.
user=> (def book {:name "SICP" :details {:pages 657 :isbn-10 "0262011530"}})
#'user/book
user=>
user=> (let [{name :name {:keys [pages isbn-10]} :details} book]
(println "name:" name "pages:" pages "isbn-10:" isbn-10))
name: SICP pages: 657 isbn-10: 0262011530
Destructuring nested vectors is also very straight-forward, as the following example illustrates
user=> (def numbers [[1 2][3 4]])
#'user/numbers

user=> (let [[[a b][c d]] numbers]
(println "a:" a "b:" b "c:" c "d:" d))
a: 1 b: 2 c: 3 d: 4
Since binding forms can be nested within one another arbitrarily, you can pull apart just about anything -- http://clojure.org/special_forms
The following example destructures a map and a vector at the same time.
user=> (def golfer {:name "Jim" :scores [3 5 4 5]})
#'user/golfer

user=> (let [{name :name [hole1 hole2] :scores} golfer]
(println "name:" name "hole1:" hole1 "hole2:" hole2))
name: Jim hole1: 3 hole2: 5
The same example can be rewritten using a function definition to show the simplicity of using destructuring in parameter lists.
user=> (defn print-status [{name :name [hole1 hole2] :scores}] 
(println "name:" name "hole1:" hole1 "hole2:" hole2))
#'user/print-status

user=> (print-status {:name "Jim" :scores [3 5 4 5]})
name: Jim hole1: 3 hole2: 5
There are other (less used) directives and deeper explanations available on http://clojure.org/special_forms and in The Joy of Clojure. I recommend both.

**(defn do-something [x y & more] ... )