DEV Community

vinayak
vinayak

Posted on Originally published at itsvinayak.github.io on

Getting Started with Clojure

Introduction

Clojure is a functional programming language and dialect of the Lisp programming language, which means it use prefix notation for all its expressions, which runs on the Java Virtual Machine (JVM).

From Clojure's website: https://clojure.org

Clojure is a dialect of Lisp, and shares with Lisp the code-as-data philosophy and a powerful macro system. Clojure is predominantly a functional programming language, and features a rich set of immutable, persistent data structures. When mutable state is needed, Clojure offers a software transactional memory system and reactive Agent system that ensure clean, correct, multithreaded designs.

Clojure is a compiled and JVM-hosted language. It's compiler translates Clojure forms into JVM bytecode, which is then executed by the JVM. It also provides REPL (Read-Eval-Print Loop) support, this allows us to interactively evaluate Clojure expressions.

How Clojure code executes

Clojure code is executed in the following steps:

  • Clojure source code
  • Clojure Reader (Reads source into Clojure forms)
  • Clojure Compiler (Compiles Clojure forms into JVM bytecode)
  • JVM Execution (Executes the compiled bytecode)

Environment Setup

For compiling Clojure code, there are two popular ways to do this:

  1. Using Leiningen (Build and project management)
  2. Using Clojure CLI (Official command-line tool)

Using Clojure CLI

In the case of Clojure CLI, the project is configured manually, unlike Leiningen.

  • Creating a new project using Clojure CLI

In the case of Clojure CLI, new projects are created manually by setting up the folder structure and deps.edn files.

Folder structure for a Clojure CLI project typically looks like this:

Clojure CLI Project Structure

where the project is configured using Clojure CLI deps.edn

  • running the project using Clojure CLI
clojure -M -m hello-world.core

Enter fullscreen mode Exit fullscreen mode
  • running the REPL using Clojure CLI
clojure -M

Enter fullscreen mode Exit fullscreen mode

Using Leiningen

  • creating a new project using Leiningen
lein new app hello-world

Enter fullscreen mode Exit fullscreen mode

Folder structure for a Leiningen project typically looks like this:

Leiningen Project Structure

where the project is configured using Leiningen (project.clj)

  • Running the project using Leiningen
lein run

Enter fullscreen mode Exit fullscreen mode
  • Running the REPL using Leiningen
lein repl

Enter fullscreen mode Exit fullscreen mode

Language Basics

  • Data Types Clojure supports several fundamental data types.
Type Example Description
Integer 42 Whole number without a decimal point
Float/Double 3.14 Numeric value with a decimal point
String "Hello" Text enclosed in double quotes
Boolean true, false Logical values representing true or false
Nil nil Represents the absence of a value
Keyword :name Named identifier commonly used as a key in maps
Symbol user-name Name used to refer to a value, function, or other entity
Character \A Represents a single character
(type 42)
;; => java.lang.Long
(type 12.09)
;; => java.lang.Double
(type "Hello")
;; => java.lang.String
(type :name)
;; => clojure.lang.Keyword
(def name "Vinayak")
(type name)
;; => java.lang.String

Enter fullscreen mode Exit fullscreen mode

Difference between Keyword vs String vs Symbol:

;; Keyword
;; Define a map 
(def user {:name "Vinayak" :age 28})
;; Use
(println (:name user))
;; => "Vinayak"
(println (:age user))
;; => 28
(type user)
;; => clojure.lang.PersistentArrayMap
(type :name)
;; => clojure.lang.Keyword

;; String
(def name "Vinayak")
(println name)
;; => "Vinayak"
(type name)
;; => java.lang.String

;; Symbol -> represent a name literally
(type 'name)
;; => clojure.lang.Symbol

Enter fullscreen mode Exit fullscreen mode
  • Variables

In Clojure, variables are immutable by default. Which means once a value is assigned, it can not be changed.

(def name "vinayak")
;; Try to change name directly
(set! name "vinayak sharma")
;; Execution error (IllegalStateException) at user/eval1577 (REPL:1).
;; Can't change/establish root binding of: name with set

Enter fullscreen mode Exit fullscreen mode

But we can Re-definition using def again:

(def name "vinayak")
(def name "vinayak sharma")
(println name)
;; => vinayak sharma

Enter fullscreen mode Exit fullscreen mode

Defining array

(def numbers [1 2 3 4 5])
(println numbers)
;; => [1 2 3 4 5]
(type numbers)
;; => clojure.lang.PersistentVector

Enter fullscreen mode Exit fullscreen mode

Defining the map

(def user {:name "Vinayak" :age 28})
(println user)
;; => {:name "Vinayak", :age 28}
(type user)
;; => clojure.lang.PersistentArrayMap

Enter fullscreen mode Exit fullscreen mode
  • Operators
(+ 10 5) ;; => 15
(- 10 5) ;; => 5
(* 10 5) ;; => 50
(/ 10 5) ;; => 2

(= 10 10) ;; => true, this is different from assignment
(not= 10 5) ;; => true
(< 5 10) ;; => true
(>= 10 10) ;; => true

(and true true) ;; => true
(or false true) ;; => true
(not true) ;; => false

Enter fullscreen mode Exit fullscreen mode

In Clojure, arithmetic operation support multiple arguments. For example:

(+ 1 2 3 4) ;; => 10
(* 2 3 4) ;; => 24
(- 10 1 2) ;; => 7
(/ 100 2 5) ;; => 10

Enter fullscreen mode Exit fullscreen mode
  • Compound Data Types

    • Vector: Ordered collection, efficient indexed access.
    • List: Ordered collection, efficient sequential access.
    • Map: Stores key-value pairs.
    • Set: Stores unique values.
  • Functions

In Clojure, functions are defined using the defn macro. Here is an example:

;; Define a function
(defn add [a b] 
    (+ a b))

;; Call the function
(add 10 20)
;; => 30

Enter fullscreen mode Exit fullscreen mode

Quick Reference

Syntax Meaning
fn Creates a function
[name] Defines a function parameter
(str ...) Combines values into a string
#() Shorthand syntax for an anonymous function
% Represents the first argument in #()
%1 Represents the first argument in #()
%2 Represents the second argument in #()
%& Represents the rest of the arguments in #()

Fundamentals

  • Conditionals

  • Loops

When to use what?

Construct Purpose
doseq Perform side effects for each item in a collection.
for Generate a sequence of results.
map Transform every item in a collection.
reduce Combine items into a single result.
loop/recur Implement custom iterative logic using recursion without growing the call stack
  • Atoms in Clojure: values are immutable by default. Atoms allow storing a value that can change over time or creates a mutable reference.
(def age (atom 25))
(println @age)
;; => 25
(println age)
;; #object[clojure.lang.Atom 0x3a14182a {:status :ready, :val 25}]
(reset! age 30)
(println @age)
;; => 30

Enter fullscreen mode Exit fullscreen mode

Increase the value using a swap!

(def age (atom 25))
(swap! age inc)
(println @age)
;; Output: 26

Enter fullscreen mode Exit fullscreen mode
  • Sequences
Function Purpose Example
first Get the first item (first '(10 20 30)) → 10
rest Get remaining items (rest '(10 20 30)) → (20 30)
seq Get a sequence view (seq [1 2 3]) → (1 2 3)
map Transform every item (map inc [1 2 3]) → (2 3 4)
filter Select matching items (filter even? [1 2 3 4]) → (2 4)
count Count items (count '(10 20 30)) → 3
  • Map & Records
  • Map: Maps are flexible key-value structures.
(def user
    {:id 101
    :name "Vinayak"
    :role "Engineer"})

(:name user)
;; => "Vinayak"

(assoc user :age 28)
;; => {:id 101, :name "Vinayak",
;; :role "Engineer", :age 28}

Enter fullscreen mode Exit fullscreen mode
  • Records: Records are fixed-schema key-value structures.
(defrecord Employee [id name department])

(def emp
    (->Employee 101 "Vinayak" "Engineering"))

(:name emp)
;; => "Vinayak"

(assoc emp :location "Bangalore")
;; Returns a new record-like value with the extra key

Enter fullscreen mode Exit fullscreen mode
  • Destructuring: Destructuring extracts values from collections and binds them to local names.
;; Vector destructuring
(let [[a b c] [1 2 3]]
    (println a b c))
;; => 1 2 3

(let [[a b c] [10 20 30]]
    (+ a b c))
;; => 60

;; Map destructuring
(let [{:keys [id name]} {:id 101 :name "Vinayak"}]
    (println id name))
;; => 101 "Vinayak"

;; Destructuring function arguments
(defn greet [{:keys [name]}]
    (str "Hello, " name))

(greet {:name "Vinayak"})
;; => "Hello, Vinayak"

Enter fullscreen mode Exit fullscreen mode
  • Exception Handling

Clojure uses try, catch, and finally for exception handling.

(try
    (/ 1 0)
    (catch ArithmeticException e
    (println "Caught exception:" (.getMessage e)))
    (finally
    (println "Finally block executed")))

Enter fullscreen mode Exit fullscreen mode

Using throw

(defn withdraw [balance amount]
    (if (> amount balance)
    (throw (ex-info "Insufficient funds"
                    {:balance balance
                        :amount amount}))
    (- balance amount)))

Enter fullscreen mode Exit fullscreen mode

More on Clojure

  • Namespaces

Namespaces organize code into logical modules and prevent naming conflicts.

(ns my-app.core)


(ns myapp.payments)

(defn process-payment [amount]
    (str "Processing payment: " amount))
(process-payment 100)
;; => "Processing payment: 100"

Enter fullscreen mode Exit fullscreen mode

Requiring namespaces in Clojure is done using the require function within the ns macro.

(ns my-app.core
    (:require [myapp.payments :as payments]))

Enter fullscreen mode Exit fullscreen mode
  • Macros

Macros allow to extend the language by transforming code before it executes. Functions operate on values at runtime, whereas macros operate on code at compile-time.

defmacro: Used to define a macro.

(defmacro macro_name [condition & body]
    `(if (not ~condition)
        (do ~@body)))

(macro_name false
    (println "This will be printed"))
;; => "This will be printed"

Enter fullscreen mode Exit fullscreen mode

Understand the Syntax

Syntax Meaning
defmacro Defines a macro
Backtick


``| Syntax-quotes a code form |
|
~| Unquotes and inserts a form |
|
~@` | Splices a sequence of forms into another form |

  • Watchers: A watcher observes changes to a reference type and runs a callback when its state changes. Watchers can be attached to atoms, refs, and agents.

clojure
(def balance (atom 100))

(add-watch balance :audit
    (fn [key reference old-value new-value]
    (println "Balance changed:"
        old-value "->" new-value)))

(reset! balance 200)
;; Prints a message about 100 -> 200



Enter fullscreen mode Exit fullscreen mode

Note: fn is used to create an anonymous function

  • Agents: An agent manages state through asynchronous actions.

clojure
(def counter (agent 0))
(send counter inc)
(await counter)

@counter
;; => 1



Enter fullscreen mode Exit fullscreen mode

send schedules an action on an agent's thread pool. await waits for actions dispatched before the call to finish.

Atom vs Agent in Clojure

Feature Atom Agent
State update Synchronous Asynchronous
Update operation swap!, reset! send, send-off
Read state @atom @agent
Main use Independent shared state Asynchronous state updates

Note: Atoms are suitable for managing independent shared state, while agents are ideal for handling asynchronous state updates.

  • Reference Values

Clojure provides several reference types for managing state, especially when concurrency is involved.

  • Atoms

clojure
(def x (atom 10))
(swap! x inc)
@x
;; => 11



Enter fullscreen mode Exit fullscreen mode
  • Agent

clojure
(def counter (agent 0))
(send counter inc)
(await counter)
@counter
;; => 1



Enter fullscreen mode Exit fullscreen mode
  • Ref

clojure
(def r (ref 0))
(dosync
    (alter r inc))
@r
;; => 1



Enter fullscreen mode Exit fullscreen mode
  • Var

clojure
(def ^:dynamic *my-var* 42)
*my-var*
;; => 42



Enter fullscreen mode Exit fullscreen mode

Top comments (0)