About this ‘A Language a Day’ Advent Calendar
This article became a part of my book ‘A Language a Day’, which you can get in both electronic and paper format. Languages covered in the book: C++, Clojure, Crystal, D, Dart, Elixir, Factor, Go, Hack, Hy, Io, Julia, Kotlin, Lua, Mercury, Nim, OCaml, Raku, Rust, Scala, and TypeScript.
Welcome to Day 14 of this year’s A Language a Day Advent Calendar. Today’s topic is introduction to the Clojure programming language.
Facts about the language
Some facts about Clojure:
- Based on Lisp
- Functional programming support
- Targets Java platform
- Appeared in 2007
- Website: clojure.org
Installing and running Clojure
The website of the language gives instructions for installing it on different operating system. For the Mac users, run:
$ brew install clojure
You will have a command-line tool that you can run a program with:
$ clj hello-world.clj
Hello, World!
Clojure has a specific syntax, and a simple call of a function is a list:
(println "Hello, World!")
Here, the list contains two items: the name of the println
function and its argument, the string containing the message.
Variables
You bind a value to a name using the def
keyword. To do that, construct a list like shown below:
(def who "John") (println (str "Hello, " who "!")) ; Hello, John!
The first line creates a variable who
, which is used in the second line where it is printed. You could pass all three parts of the message to the print
function: (print "Hello," who "!")
, but in this case Clojure will print them space-separated. So, it is better to construct a new string by calling str
with those three arguments how this is done in the above example.
Lists and vectors
To create a list, use the list
constructor:
(def data (list 10 20 30)) (doseq [n data] (println n))
This program also prints the elements of a list by calling doseq
, which is equivalent to the for
loop but allows side effects (remember that printing something is a side effect in functional languages).
Vectors allow random access and they are created with a pair of brackets. To access the nth element, use the nth
function:
(def data [11 22 33]) (doseq [n data] (println n)) (println (nth data 2)) ; prints 33
You can also use nth
with lists. Vectors are kept in memory in such a way that you access their elements in O(1) time, while for lists it is O(n).
Functions
You define functions with the defn
keyword. You can see the syntax in the following example:
(defn greet [name] (println (str "Hello, " name "!"))) (greet "John") (greet "Alla")
Function’s arguments follow the function name and are placed in brackets. If you don’t need any arguments, leave them empty:
(defn f [] (println "I am a function.")) (f)
Lambdas
You can define an anonymous function with the fn
keyword. Here is an example of using such a function inside a map
call that transforms a list of integers to a list of their squares:
(def data (list 1 3 5)) (def transformed_data ( map (fn [x] (* x x)) data)) (print transformed_data) ; (1 9 25)
Factorial, solution 1
Let us first solve the factorial task by using recursion. We can define a function that calls itself.
(defn factorial [n] ( if (< n 2) 1 (* n (factorial(- n 1))) )) (println (factorial 1)) ; 1 (println (factorial 5)) ; 120 (println (factorial 7)) ; 5040
Note that to compare the two values you have to write a list (< n 2)
and not an expression like n < 2
. The same for decrementing the value of n
: (- n 1)
.
Factorial, solution 2
Alternatively, use the apply
function that applies the operator to a list of values. To have the list, we need to create a range
from 2 to n, including n.
(defn factorial [n] ( apply * (range 2 (+ n 1)) ))
In the above code, you can alternatively use the dec
and inc
functions to get a decremented or incremented value: (dec n)
and (inc n)
. Notice that they do not change the value of the variable, but only return the result:
(def n 10) (println (dec n)) ; 9 (println n) ; did not change: 10 (println (inc n)); 11 (println n) ; still 10
Multimethods
There is no object-oriented support in Clojure, but if we need to talk about polymorphic behaviour, we could use multimethods. Let us examine a small program that implements another version of the factorial.
The program below contains three pre-defined solutions for the values of the factorial of 0, 1, and for the rest numbers.
(defmulti factorial identity) (defmethod factorial 0 [_] 1) (defmethod factorial 1 [_] 1) (defmethod factorial :default [n] (* n (factorial (- n 1))))
You announce a multimethod function with the defmulti
keyword, and then provide the implementations using defmethod
. In our example, the dispatch is based directly on values, but instead of identity
(that returns the passed value, n
in our case) we could create a separate dispatch function (see the next example).
The first two factorial variants respond to the exact values 0 and 1, and the third responds to :default
, thus it handles all other situations. The [_]
construct tells that we do not need any arguments for this variant.
A Polymorphic example
In this example, we are using the multimethods, but the dispatching is done via an anonymous function. The animals are the maps (this is a datatype similar to dictionaries or hashes in other languages) with the key kind
representing the animal type and nickname
for the name. The dispatch function converts such objects to the value corresponding to its kind
key.
Here is the full code:
(defmulti info (fn [x] (x "kind"))) (defmethod info :dog [x] (str (x "nickname") " is a dog.")) (defmethod info :cat [x] (str (x "nickname") " is a cat.")) (def zoo (list {"kind" :dog "nickname" "Charlie"} {"kind" :dog "nickname" "Milo"} {"kind" :cat "nickname" "Gracie"} {"kind" :cat "nickname" "Molly"} )) (doseq [x zoo] (println (info x)))
In the loop, the x
variable stands for one of the animals, for example: {"kind" :dog "nickname" "Charlie"}
. This value is passed to the info
function, the :dog
value is extracted for it, and based on this, the correct multi-function is triggered.
Concurrent computing
Clojure targets to work with JVM, and a number of its features rely on the libraries that you have in Java. If you want to explore concurrency deeper, you should probably start with the overview on Clojure’s website and examine the core.async library.
Sleep Sort
The Sleep sort implementation presented below uses futures, thus you don’t need to import any Java-specific libraries to your code.
(defn sort-me [n] (Thread/sleep (* n 1000)) (println n) ) (def data [10 4 2 6 2 7 1 3]) (doseq [n data] (future (sort-me n))) (shutdown-agents)
The main loop over data
creates a future
that executes the sort-me
function with the given value. That function, in its turn, performs a delay (using the Thread/sleep
function) and prints the number. To terminate the program, you need to call shutdown-agents
.
Notice that dashes are allowed characters in identifiers, and the slash is used to add the namespace to the name.
Get more
When learning Clojure, you may be confused how simple it looks in the beginning when you understand the main idea behind parentheses, but then you may be confused how difficult it is when you will have to count them 🙂
Examine the following resources to get more information about the language:
- ClojureDocs — a community resource with examples
- Clojure reference
- Clojure guides
- CLOJURE for the BRAVE and TRUE
The source codes for today’s article are available on GitHub.
Next: Day 15. Nim
One thought on “Clojure at a Glance — A Language a Day, Advent Calendar Day 14/24”