Defining Functions with defn
The defn macro is the primary way to define named functions in Clojure. It combines def (which binds a symbol) with fn (which creates a function value), taking a name, an optional docstring, a vector of parameters, and a body of expressions. Because Clojure is expression-oriented, the last expression evaluated in the body is automatically returned — there is no explicit return keyword. For example, (defn square [x] (* x x)) creates a function named square that takes one argument and returns its square.
Cricket analogy: Defining a function with defn is like the Indian team management scripting a fixed net-session drill for Virat Kohli — you name the drill, specify its input (ball speed), and whatever happens on the final delivery of the set is recorded as the outcome, with no separate 'return' step.
Anonymous Functions
Not every function needs a name. The fn special form creates an anonymous function directly: (fn [x y] (+ x y)). This is common when passing a short function as an argument to another function, such as map or filter. Clojure also provides the compact #() reader macro syntax, where % refers to the single argument and %1, %2, etc. refer to multiple positional arguments — for example, #(+ % 1) increments its argument, and #(+ %1 %2) adds two arguments.
Cricket analogy: Using #(+ % 1) is like a substitute fielder brought on for a single over during an IPL match with no fixed squad number — they do one job, saving a boundary, and leave, just as an anonymous function performs one task inline without ever being named.
Variadic Functions and Multiple Arities
Clojure functions can accept a variable number of arguments using the & symbol in the parameter vector, which collects the remaining arguments into a sequence: (defn greet-all [greeting & names] (map #(str greeting ", " %) names)). Functions can also define multiple arities — different parameter lists, each with its own body — inside a single defn, letting the same function name behave differently depending on how many arguments are passed, such as supplying a default value when an optional argument is omitted.
Cricket analogy: A variadic function is like Rohit Sharma's batting order naming an opener '& the rest' of the middle order as a flexible group, while multi-arity defn mirrors India having separate tactics for a T20 versus a 50-over World Cup final under one team name.
Higher-Order Functions
Because functions in Clojure are first-class values, they can be passed as arguments, returned from other functions, and stored in data structures. This enables higher-order functions like map, filter, and reduce, which take a function as an argument and apply it across a collection: (map inc [1 2 3]) returns (2 3 4). Functions can also return other functions — for instance, (defn make-adder [n] (fn [x] (+ x n))) returns a new function each time it's called, closing over the value of n.
Cricket analogy: Passing inc to map is like a bowling coach assigning the same throw-down routine to every batter in the nets lineup, applying one instruction across the whole squad, from Rohit Sharma to the newest net bowler, in a single pass.
(defn greet
"Returns a greeting for the given name, or a default greeting."
([] (greet "World"))
([name] (str "Hello, " name "!")))
(greet) ;=> "Hello, World!"
(greet "Clojure") ;=> "Hello, Clojure!"
;; Anonymous functions
(map (fn [x] (* x x)) [1 2 3]) ;=> (1 4 9)
(map #(* % %) [1 2 3]) ;=> (1 4 9)
;; Variadic function
(defn sum-all [& nums] (reduce + 0 nums))
(sum-all 1 2 3 4) ;=> 10
;; Function returning a function (closure)
(defn make-adder [n] (fn [x] (+ x n)))
(def add5 (make-adder 5))
(add5 10) ;=> 15
Clojure functions are values just like numbers or strings — they can be stored in vars, passed as arguments, returned from other functions, and stored inside data structures such as maps. This is what makes higher-order functions like map, filter, and reduce so natural in idiomatic Clojure code.
Calling a multi-arity function with a number of arguments that doesn't match any defined arity throws an ArityException at runtime, not compile time — always check that every arity you call is actually defined in the defn form.
- defn defines a named function with an optional docstring, a parameter vector, and a body whose last expression is automatically returned.
- fn creates an anonymous function; the #() reader macro is a shorthand using % for a single argument or %1, %2 for multiple.
- The & symbol in a parameter vector collects any remaining arguments into a sequence, enabling variadic functions.
- A single defn can define multiple arities, each with its own parameter list and body, selected based on the number of arguments passed.
- Functions are first-class values in Clojure — they can be passed to other functions, returned from functions, and stored in data structures.
- Higher-order functions like map, filter, and reduce take a function as an argument and apply it across a collection.
- A function that returns another function, like make-adder, creates a closure that captures the enclosing scope's values.
Practice what you learned
1. What does (defn square [x] (* x x)) return when called as (square 4)?
2. Which reader macro syntax creates an anonymous function using % for its argument?
3. What does the & symbol do in a parameter vector like [greeting & names]?
4. Given (defn make-adder [n] (fn [x] (+ x n))), what does ((make-adder 5) 10) evaluate to?
5. What exception is thrown when a function is called with an unsupported number of arguments?
Was this page helpful?
You May Also Like
Immutable Data Structures
Explore Clojure's core persistent data structures — vectors, lists, maps, and sets — and understand how immutability and structural sharing make them both safe and efficient.
Sequences in Clojure
Understand Clojure's sequence abstraction — the uniform interface of first, rest, and cons that lets the same functions work across lists, vectors, maps, and lazy, potentially infinite streams.
Destructuring in Clojure
Learn how Clojure's destructuring syntax lets you pull values out of vectors and maps directly in let bindings and function parameters, making data-heavy code concise and readable.
Recursion and loop/recur
Learn how Clojure handles recursion without mutable loop variables, and how the recur special form enables safe, stack-efficient tail recursion via loop and function self-calls.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics