Why learn Clojure?

Clojure is expressive and powerful. It provides the concise expression capabilities of Lisp in a practical environment. It also leverages the JVM, anything you can do in Java you can do in Clojure with far less ceremony. There are loads of other reasons but lets dive into some syntax to get a feel for the language.

Clojure programs are written in forms. Forms enclosed in parenthesis indicate function calls:

(+ 1 2 3)

calls the '+' function with arguments 1 2 3 and returns the value 6, the sum of arguments.

New functions can be defined using defn:

(defn average [x y] (/ (+ x y) 2))

Here x and y are symbols representing the input arguments.
Function '/' is called to divide the sum of x and y by 2.
Note that forms are always in prefix notation, with function followed by subsequent arguments.
Now average can be invoked as

(average 3 5)

and will return 4.
In this example, 'average' is a symbol, whose value is a function.

Clojure provides easy access to the JVM:

(.show (javax.swing.JFrame.))

This calls the show method on the result of (javax.swing.JFrame.) which constructs a new Jframe. Note the full stop before the method call and the full stop after the construction. We can combine them without the usual boilerplate code:

(doto (javax.swing.JFrame.)
(.setLayout (java.awt.GridLayout. 2 2 3 3))
(.add (javax.swing.JTextField.))
(.add (javax.swing.JLabel. "Enter some text"))
(.setSize 300 80)
(.setVisible true))

Functions can be passed to other functions:

(map + [1 2 3] [4 5 6])

returns (5 7 9)
map is a function which takes another function and calls it with arguments taken from following collections. In our case we have provided the function '+' and two vectors of integers. The result is a list of the results of calling '+' with arguments taken from the vectors. Using functions as arguments to other functions is very powerful. We can use our previously defined average function with map like so:

(map average [1 2 3] [4 5 6])

returns (5/2 7/2 9/2)

Functions can also return other functions:

(defn addx [x] (fn [y] (+ x y)))

Here addx will return a new function that takes 1 argument and adds x to it.

(addx 5)

returns a function that can be called with 1 argument and will add 5 to it.

(map (addx 5) [1 2 3 4 5])

returns (6 7 8 9 10)
We called map with a the result of addx, which was a function that takes an argument and adds 5. That function was called on the list of numbers we supplied.

(reduce + (range 1 100 2))

(range 1 100 2) creates a lazy sequence of numbers 1 3 5 7 ... 99. 1 is the starting point, 100 is the end point, 2 is the step. reduce calls the + function. First it calls + with two arguments, the first two numbers supplied by range. Then it calls + again with the previous result and the next number, until all the numbers are exhausted. Clojure has lots of support for sequences, collections and high level operations. As you learn them, you will find very expressive ways to write tasks such as this.

(defn factorial [n]
(reduce * (range 2 (inc n))))

Clojure provides hash-maps which are very useful in many programming tasks. Maps are written between {} just like vectors are written between [] and lists are written between ().

{:name "Tim", :occupation "Programmer"}

Is a map which associates the keyword :name with "Tim" and :occupation with "Programmer". Note that the comma is treated as whitespace by Clojure but may be optionally supplied to help visually group things that go together. Keywords are preceeded by : and provide a convenient way to name fields, however keys don't have to be keywords.

assoc returns a new map with a value associated to a key:

user=> (assoc {:a 1, :b 2} :b 3)
{:a 1, :b 3}

(defn map-count [map key]
(assoc map key (inc (get map key 0))))

This function takes a map as input, looks up a key and increments how many times that key has been counted. (get map key 0) just looks up key in map and returns its value if found, else 0. inc adds one to that value. (assoc map key value) will return a new map with key associated to value. So as you can see the map is not modified in any way. A new map is returned with a new value associated to the key supplied.

(reduce map-count {} ["hi" "mum" "hi" "dad" "hi"])

Results in {"dad" 1, "mum" 1, "hi" 3} because reduce starts with an empty map which is used as the input to the first function call, then the resulting map is used for the next and so forth. The strings are used as keys. Now we can write a more useful function which takes a string and counts the words in it:

(defn count-words [s]
(reduce map-count {}
(re-seq #"\w+" (.toLowerCase s))))
(count-words "hi mum hi dad hi")

Gives the same result. Note that re-seq here splits the input string into words based upon the regular expression provided.

This should have given you a taste of the syntax and the power of
Try it out!

No comments:

Post a Comment