Introduction to Clojure
Clojure is a modern Lisp dialect that runs on the Java Virtual Machine (JVM), offering the expressiveness of a functional programming language with the robustness and performance of the JVM. Created by Rich Hickey in 2007, Clojure emphasizes immutability, simplicity, and practicality, making it excellent for concurrent programming, data processing, and building robust applications.
Core Concepts
Fundamental Principles
- Functional Programming: Emphasizes pure functions and immutable data
- Hosted Language: Runs on JVM, JavaScript runtimes (ClojureScript), or CLR (.NET)
- Dynamic Typing: Types are determined at runtime
- Immutability: Data structures are immutable by default
- REPL-Driven Development: Interactive development with immediate feedback
Syntax Basics
| Element | Description | Example |
|---|---|---|
| S-expressions | Code as data; expressions in parentheses | (+ 1 2 3) |
| Prefix notation | Function name comes first in expression | (str "Hello" " " "World") |
| Lists | Ordered collections of items | '(1 2 3) or (list 1 2 3) |
| Vectors | Indexed collections | [1 2 3] |
| Maps | Key-value pairs | {:a 1 :b 2} |
| Sets | Unique collections | #{1 2 3} |
| Keywords | Symbolic identifiers (often used as keys) | :name |
Data Types and Collections
Scalar Types
;; Numbers
42 ; integer
3.14 ; floating-point
22/7 ; ratio
1N ; arbitrary precision integer
1.0M ; arbitrary precision decimal
;; Strings and Characters
"hello" ; string
\a ; character
;; Other Scalars
true false ; booleans
nil ; null value
:keyword ; keyword
Collections
;; Lists - linked lists, efficient for sequential access
'(1 2 3) ; quoted list literal
(list 1 2 3) ; list function
;; Vectors - indexed access, efficient for random access
[1 2 3] ; vector literal
(vector 1 2 3) ; vector function
(get [1 2 3] 0) ; returns 1 (zero-indexed)
;; Maps - key-value associations
{:a 1 :b 2} ; map literal
(hash-map :a 1 :b 2) ; hash-map function
(get {:a 1 :b 2} :a) ; returns 1
({:a 1 :b 2} :a) ; also returns 1 (maps are functions of their keys)
;; Sets - unique values
#{1 2 3} ; set literal
(hash-set 1 2 3) ; hash-set function
(get #{1 2 3} 1) ; returns 1
(#{1 2 3} 1) ; also returns 1 (sets are functions of their members)
Functions and Control Flow
Function Definition
;; Named function
(defn add [a b]
(+ a b))
;; Anonymous function (lambda)
(fn [a b] (+ a b))
;; Shorthand anonymous function
#(+ %1 %2) ; % for single arg, %1, %2, etc. for multiple args
;; Multi-arity function
(defn greeting
([] (greeting "World"))
([name] (str "Hello, " name "!")))
;; Variadic function (variable number of arguments)
(defn sum [& numbers]
(apply + numbers))
Control Flow
;; Conditionals
(if condition
true-expr
false-expr)
(when condition ; when is like if without an else
expr1
expr2)
(cond
condition1 result1
condition2 result2
:else default-result)
(case value
value1 result1
value2 result2
default-result)
;; Iteration
(dotimes [i 3]
(println i)) ; prints 0, 1, 2
(doseq [item [1 2 3]]
(println item)) ; prints each item
(for [x [1 2 3]
y [4 5 6]
:when (> (* x y) 10)]
[x y]) ; list comprehension with filtering
Sequence Operations
Core Sequence Functions
;; Creating sequences
(range 5) ; returns (0 1 2 3 4)
(repeat 3 "hi") ; returns ("hi" "hi" "hi")
(iterate inc 1) ; returns infinite seq (1 2 3 ...)
;; Transforming sequences
(map inc [1 2 3]) ; returns (2 3 4)
(filter even? [1 2 3 4]) ; returns (2 4)
(remove odd? [1 2 3 4]) ; returns (2 4)
(reduce + [1 2 3 4]) ; returns 10
;; Other useful sequence functions
(count [1 2 3]) ; returns 3
(first [1 2 3]) ; returns 1
(rest [1 2 3]) ; returns (2 3)
(take 2 [1 2 3 4]) ; returns (1 2)
(drop 2 [1 2 3 4]) ; returns (3 4)
Common Sequence Patterns
;; Transforming a collection
(->> [1 2 3 4]
(filter even?)
(map #(* % %))
(reduce +)) ; returns 20 (sum of squares of even numbers)
;; Grouping and counting
(frequencies ["a" "b" "a" "c"]) ; returns {"a" 2, "b" 1, "c" 1}
(group-by count ["a" "aa" "b" "bb"]) ; returns {1 ["a" "b"], 2 ["aa" "bb"]}
State and Concurrency
Managing State
;; Atoms - synchronous, uncoordinated reference type
(def counter (atom 0))
(swap! counter inc) ; atomically increments counter
(reset! counter 0) ; resets counter to 0
@counter ; dereferences counter
;; Refs - coordinated, synchronous reference type (for transactions)
(def account (ref 1000))
(dosync
(alter account - 100) ; withdraw 100
(alter other-account + 100)) ; deposit 100
;; Agents - asynchronous reference type
(def counter (agent 0))
(send counter inc) ; asynchronously increments counter
Concurrency Patterns
| Pattern | Description | Best Used For |
|---|---|---|
| Atoms | Independent, synchronous changes | Simple shared state |
| Refs | Coordinated synchronous changes within transactions | Complex state changes requiring consistency |
| Agents | Asynchronous changes | Fire-and-forget operations |
| Futures | Compute a value in another thread | Parallelizing independent computation |
| Promises | Set a value once, possibly from another thread | One-time coordination point |
Namespaces and Project Structure
;; Namespace declaration
(ns myapp.core
(:require [clojure.string :as str]
[clojure.set :refer [union difference]]
[myapp.utils :refer :all])
(:import [java.util Date UUID]))
;; Using imported things
(str/join ", " ["a" "b" "c"])
(union #{1 2} #{2 3})
(Date.) ; constructing a Java class
Working with Java
;; Importing and using Java classes
(import '(java.util Date Calendar))
(def now (Date.)) ; constructor
(. now toString) ; method call
(.toString now) ; shorthand method call
;; Static method call
(Calendar/getInstance) ; static method
(Math/sqrt 16) ; static method with arg
;; Using Java collections
(doto (java.util.ArrayList.)
(.add "item1")
(.add "item2"))
Common Challenges and Solutions
Challenge: Debugging Complex Expressions
Solution: Use (println) or (prn) to inspect values. The thread-last macro (->>) helps break down complex pipelines.
(->> [1 2 3 4]
(map inc)
(do (println "After inc:" %) %) ; Debug output in a pipeline
(filter even?))
Challenge: Managing Side Effects
Solution: Isolate side effects and use functional core, imperative shell pattern.
;; Bad: Mixed side effects and logic
(defn process-data [data]
(println "Processing data...")
(let [result (transform-data data)]
(save-to-db result)
result))
;; Better: Separate logic from side effects
(defn transform-data [data]
;; Pure function, no side effects
(map inc data))
(defn process-data [data]
(println "Processing data...")
(let [result (transform-data data)]
(save-to-db result)
result))
Challenge: Handling Lazy Sequences
Solution: Be mindful of when sequences are realized and use doall when needed.
;; This won't print anything because map is lazy
(def seq1 (map #(do (println %) (inc %)) [1 2 3]))
;; Force evaluation
(def seq2 (doall (map #(do (println %) (inc %)) [1 2 3])))
Best Practices
Code Style
- Prefer functional style with immutable data structures
- Use meaningful naming for functions and variables
- Use threading macros (
->,->>) for readability - Keep functions small and focused on a single task
- Favor composing functions over complex logic
REPL-Driven Development
- Start with a running REPL
- Write small functions and test them immediately
- Develop incrementally, building up functionality
- Refactor and refine as understanding evolves
Performance Tips
- Use transients for batch operations on large collections
- Consider type hints for performance-critical code
- Use
reducersfor parallel processing of large collections - Profile before optimizing (use tools like YourKit, VisualVM)
Testing Best Practices
- Write small, focused tests
- Use
clojure.testor libraries likeexpectations - Test logic, not implementation details
- Consider property-based testing with
test.check
;; Example test with clojure.test
(ns myapp.core-test
(:require [clojure.test :refer :all]
[myapp.core :refer :all]))
(deftest test-add
(is (= 3 (add 1 2)))
(is (= 0 (add -1 1))))
Resources for Further Learning
Books
- “Clojure for the Brave and True” by Daniel Higginbotham
- “Programming Clojure” by Stuart Halloway and Aaron Bedra
- “The Joy of Clojure” by Michael Fogus and Chris Houser
Online Resources
- Official Clojure Documentation
- ClojureDocs (community examples)
- Clojure Cookbook
- 4Clojure (interactive problems)
Tools
- Leiningen or deps.edn for dependency management
- CIDER for Emacs or Cursive for IntelliJ for development
- Figwheel for ClojureScript development
- Gorilla REPL for literate programming and data visualization
