Mastering Clojure Fundamentals for Beginners

Category Data Engineering

1. Getting Started

1.1. Running the Clojure Interpreter

Clojure is powerful, easy to use, general purpose programming language that runs on the JVM. Clojure like other Lisp dialects is dynamic, which means that it is not overly typed based system and gives you some room to breathe. In addition to that Clojure is compiled and can easily call upon the vast amount of Java libraries and the goodness of the JVM. I have created this series keeping in mind the average programmer who is more experienced in C-based programming languages like Java, Python, JavaScript. If you come from such programming languages and are looking into exploring the world of a full-fledged functional programming language, then Clojure is the one for you.
This chapter is the starting of a series on Clojure. We will try to see the internals of Clojure and at the end of it seek to build a real world app.
You can run Clojure both as a jar file or as an interpreter. We will look at how to distribute the Clojure code as a jar file later on. Right now lets first start Clojure as an interpreter. To do that install leiningen which is considered to be the easiest way to use Clojure.
First, you need to install Java if you don’t have that installed already. The below instructions are for an Ubuntu machine. In case you have a different machine and are not able to install the dependencies, write to me and we will try to find what the issue might be.

 sudo apt-get update
 sudo add-apt-repository ppa:webupd8team/java
 sudo apt-get update
 sudo apt install oracle-java8-installer

You can then follow the installation steps as shown below. This is the same as the official installation steps which I followed in my Ubuntu machine.

 ~ wget https://raw.githubusercontent.com/technomancy/leiningen/stable/bin/lein --2017-06-14 19:57:44-- https://raw.githubusercontent.com/technomancy/leiningen/stable/bin/lein
 Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 151.101.8.133
 Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|151.101.8.133|:443... connected.
 HTTP request sent, awaiting response... 200 OK
 Length: 12871 (13K) [text/plain]
 Saving to: ‘lein’
  
 lein 100%[==========================================================>] 12.57K --.-KB/s in 0.007s
  
 2017-06-14 19:57:45 (1.72 MB/s) - ‘lein’ saved [12871/12871]
  
 ~ sudo mv lein /usr/local/bin/lein
 [sudo] password for joy:
 ~ sudo chmod a+x /usr/local/bin/lein

Once done you can start the lein repl command and this will download the necessary libraries and start a repl for you.

 ~ lein repl
 nREPL server started on port 37057 on host 127.0.0.1 - nrepl://127.0.0.1:37057
 REPL-y 0.3.7, nREPL 0.2.12
 Clojure 1.8.0
 Java HotSpot(TM) 64-Bit Server VM 1.8.0_131-b11
 Docs: (doc function-name-here)
 (find-doc "part-of-name-here")
 Source: (source function-name-here)
 Javadoc: (javadoc java-object-or-class-here)
 Exit: Control+D or (exit) or (quit)
 Results: Stored in vars *1, *2, *3, an exception in *e
  
 user=>
view rawlein_repl.bash hosted with ❤ by GitHub

Now check if you can run the code and if it gives the correct result.

user=> (+ 1 1)
2

Voila! You have successfully started your Clojure interpreter and have run a Clojure program.

1.2 Assignments

The use of global variables is recommended to avoid in Clojure and to keep the functions as clean and pure as possible. But you can create a persistent reference to a changing object. This is done through the use of Vars.

 user=> (def x 1)
 #'user/x
 user=> x
 1
 user=> (def y 2)
 #'user/y
 user=> (+ x y)
 3
 user=>
view rawasignment.clj hosted with ❤ by GitHub

Vars are generally static. So, if you need to make it dynamic and have some calculations based on them, you will need to use the macro binding. Bindings are thread-local and do not propagate outside the thread.

 user=> (def ^:dynamic x 1)
 #'user/x
 user=> (def ^:dynamic y 1)
 #'user/y
 user=> (binding [x 2 y 3] (+ x y))
 5
 user=> (+ x y)
 2
 user=>

1.3 Numbers

Clojure is a dialect of Lisp and that means that means that operators are considered as functions and so come before the arguments that should go into the operation.

 user=> (+ 1 1)
 2
 user=> (* 2 3)
 6
 user=> (/ 17 5)
 17/5
 user=> ( float (/ 17 5))
 3.4
view rawnumbers.clj hosted with ❤ by GitHub

1.4 Strings

Strings are believed to be one of the basic building blocks of programming. Almost everything that you do will involve manipulation of strings.In Clojure, a string literal is created by enclosing the string in double quotes.
To concatenate some string variables you can use the `str` function:

 user=> (def a1 "My favorite lang is ")
 #'user/a1
 user=> (def a2 "Clojure")
 #'user/a2
 user=> (str a1 a2)
 "My favorite lang is Clojure"
 user=>
view rawstrings.clj hosted with ❤ by GitHub

You can also use string formatting using the Format function.

 user=> (format "Any man whose errors take %s years to correct is quite the man." "10")
 "Any man whose errors take 10 years to correct is quite the man."
 user=>

1.5 Functions

Clojure is a functional programming language. So you will be creating a lot of functions. Functions that do something, functions that pass functions, functions that manipulate other functions and so on. You can consider a function to be unrealized functionality. Generally, functions are defined in the following manner.

(defn name doc-string? attr-map? [params*] body)

For example, a function that returns a square of a number can be created.

 (defn my_square
 "This is my my_square function"
 [x]
 (* x x))
  
 (my_square 2)
view rawsquare.clj hosted with ❤ by GitHub

The above code should return 4. Now the functions can be used in other expressions.

(+ (my_square 2) (my_square 3))

which should give the output 13.
If you just need to create a small function that you can consider writing anonymous functions usingfn .

user=> ((fn [x] (* x x)) 2)
4

1.6 Conditions

One way of implementing branching in Clojure is through the if statement which is basically a test. The format is basically:
(if condition-statment <(optional)if condition is false return this>)

The above construct will be clear in the below examples.

user=> (if nil “truth value” “false value”)

“false value”

In Clojure, only nil and false are considered logical false.

user=> (if “true” “truth value” “false value”)
“truth value”

But if cannot take multiple tests in the format of if..elif..else. For that you will need to use thecond macro. Then you can pass multiple test parameters.

 user=> (defn odd-even
 #_=> "Determines whether or not n is odd or even."
 #_=> [n]
 #_=> (cond
 #_=> (= (rem n 2) 0) "even"
 #_=> (= (rem n 2) 1) "odd"))
 #'user/odd-even
 user=>
  
 user=> (odd-even 2)
 "even"
 user=> (odd-even 3)
 "odd"

The above constructs are more in line with the imperative languages.Of course, you can convert any if constructs to filter, but more on that later.

1.7 Data Containers and Data Structures.

Clojure has four primary data structure or collections of data.

1.Lists
2.Vectors
3.Maps
4.Sets

Lists in Clojure are singly linked lists. So modifications of lists are efficient while random access is not. Lists are one of the most important data structures in Clojure. A code is data in Clojure and they are represented in lists.

You can either use the list function or the quote literal to creating a list.

user=> (list “clojure” “F#” “python” “java”)
(“clojure” “F#” “python” “java”)
user=> ‘(“clojure” “F#” “python” “java”)
(“clojure” “F#” “python” “java”)

Get the first or nth element of the list.

 user=> (def langs (list "clojure" "F#" "python" "java"))
 #'user/langs
 user=> (first langs)
 "clojure"
 user=> (nth langs 2)
 "python"

You can treat the list like a stack with peek and pop.

 user=> (peek langs)
 "clojure"
 user=> langs
 ("clojure" "F#" "python" "java")
 user=> (pop langs)
 ("F#" "python" "java")
 user=>
view rawas_stack.clj hosted with ❤ by GitHub

You can also extend a list to a new list with conj and concat. Notice that the conjoining in lists happen at the beginning of the list.

 user=> (conj langs "APL")
 ("APL" "clojure" "F#" "python" "java")
 user=> (conj langs '("OCAML" "erlang"))
 (("OCAML" "erlang") "clojure" "F#" "python" "java")
 user=> (concat langs '("OCAML" "erlang"))
 ("clojure" "F#" "python" "java" "OCAML" "erlang")

B. Vectors
Vectors are similar to arrays in C and Java. The elements in a vector are indexed by contiguous integers. Vectors are optimized for random lookups.
To create a vector you can use the vector function or just define them.

 user=> (vector "Amherstia Nobilis" "Adansonia digitata" "Ficus bengalensis var - krishnae" "Araucaria Cookie" "Bombax Ceiba")
 ["Amherstia Nobilis" "Adansonia digitata" "Ficus bengalensis var - krishnae" "Araucaria Cookie" "Bombax Ceiba"]
  
 user=> ["Amherstia Nobilis" "Adansonia digitata" "Ficus bengalensis var - krishnae" "Araucaria Cookie" "Bombax Ceiba"]
 ["Amherstia Nobilis" "Adansonia digitata" "Ficus bengalensis var - krishnae" "Araucaria Cookie" "Bombax Ceiba"]

Retrieving the nth element of the vector is optimised for a vector.

 user=> (def plants-in-lalbagh ["Amherstia Nobilis" "Adansonia digitata" "Ficus bengalensis var - krishnae" "Araucaria Cookie" "Bombax Ceiba"])
 #'user/plants-in-lalbagh
 user=>
  
 user=> (nth plants-in-lalbagh 2)
 "Ficus bengalensis var - krishnae"
 user=> (get plants-in-lalbagh 2)
 "Ficus bengalensis var - krishnae"
 user=>

Concatenation works the same way to lists but conjoining happens at the end of the vector.

 user=> (def plants-in-lalbagh ["Amherstia Nobilis" "Adansonia digitata" "Ficus bengalensis var - krishnae" "Araucaria Cookie" "Bombax Ceiba"])
 #'user/plants-in-lalbagh
 user=>
  
 user=> (nth plants-in-lalbagh 2)
 "Ficus bengalensis var - krishnae"
 user=> (get plants-in-lalbagh 2)
 "Ficus bengalensis var - krishnae"
 user=> (conj plants-in-lalbagh "eucalyptus")
 ["Amherstia Nobilis" "Adansonia digitata" "Ficus bengalensis var - krishnae" "Araucaria Cookie" "Bombax Ceiba" "eucalyptus"]
 user=> (def plants-in-lalbagh1 ["Amherstia Nobilis" "Adansonia digitata"])
 #'user/plants-in-lalbagh1
 user=> (def plants-in-lalbagh2 ["Araucaria Cookie" "Bombax Ceiba"])
 #'user/plants-in-lalbagh2
 user=> (concat plants-in-lalbagh)
 plants-in-lalbagh plants-in-lalbagh1 plants-in-lalbagh2
 user=> (concat plants-in-lalbagh1 plants-in-lalbagh)
 plants-in-lalbagh plants-in-lalbagh1 plants-in-lalbagh2
 user=> (concat plants-in-lalbagh1 plants-in-lalbagh2)
 ("Amherstia Nobilis" "Adansonia digitata" "Araucaria Cookie" "Bombax Ceiba")
 user=>

C. Maps or HashMaps.
Maps are equivalent to dictionaries in Python and HashMaps in Java. Maps associate keys to values and the keys must be comparable. There are numerous implementations of maps with various guarantees of ordering and different performance on the lookups.
You can create a map using literals.

user=> (def fav-filmmakers {:stanley-kubrik “26 July 1928”
#_=> :satyajit-ray “2 May 1921”})
#’user/fav-filmmakers
user=>

Note that keywords are used as keys here. This is more of a convention and not really required. To access the value corresponding to a particular key, you can use the get function or use the key specified as a function.

 user=> ;; keys as a function.
  
 user=> (:satyajit-ray fav-filmmakers)
 "2 May 1921"
 user=> ;; using the get function.
  
 user=> (get fav-filmmakers :satyajit-ray)
 "2 May 1921"
view rawmap_lookup.clj hosted with ❤ by GitHub

D. Sets
Sets are similar to mathematical sets and are a collection of unique values. They are hashed and unordered.
To create a set you can use a literal or the set function.

user=> #{1 2 3 4 5}
#{1 4 3 2 5}
user=> (set [1 2 3 4 5])
#{1 4 3 2 5}

Sets are collections so you can count, conjugate them with other elements or break them or see if they contain some element.

 user=> (def banned-books #{"the hindus", "The God of Small Things"})
 #'user/banned-books
 user=> (count banned-books)
 2
 user=> (seq banned-books)
 ("The God of Small Things" "the hindus")
 user=> (conj banned-books "Five Past Midnight in Bhopal")
 #{"The God of Small Things" "Five Past Midnight in Bhopal" "the hindus"}
 user=> (disj banned-books "The God of Small Things")
 #{"the hindus"}
 user=> (contains? banned-books "the hindus")
 true

As you can expect from the name, sets support set-operations like union, difference, intersection, etc. Set operations require the set namespace to be loaded.

 user=> (require '[clojure.set :as set])
 niL
 user=> ; set operations
 user=> (set/union #{1 2} #{2 3}
 #{1 3 2}
 user=> (set/intersection #{1 2} #{2 3})
 #{2}
 user=> (set/difference #{1 2} #{2 3})
 #{1}

Note:
DataStructures are functions:
Maps are functions of their keys
Keywords are functions of maps
Sets are functions of their elements
Vectors are functions of their indices
Hope you liked this!
To get regular updates on more content, learning material and general rants you can follow me on Twitter.

Ready to embark on a transformative journey? Connect with our experts and fuel your growth today!