Intermediate

Functions in Clojure

Learn how to define and use functions in Clojure - parameters, multi-arity, variadic arguments, recursion, closures, and higher-order functions

Functions are the heart of Clojure. As a functional Lisp, Clojure treats functions as first-class values: you can pass them as arguments, return them from other functions, store them in data structures, and build new functions by composing existing ones. There are no “methods” attached to objects here—just functions that take values and return values.

Because Clojure favors immutability, functions are typically pure: given the same inputs, they always return the same output and produce no side effects. This makes code easier to reason about, test, and parallelize. In this tutorial you’ll learn how to define functions with defn, how Clojure handles multiple arities and variadic arguments, how recursion replaces traditional loops, and how closures and higher-order functions let you treat behavior as data.

If you’ve only seen Hello World so far, this is where Clojure’s functional character really begins to show. Let’s build up from a simple named function to closures and function composition.

Defining and Calling Functions

The defn macro defines a named function. It takes a name, an optional docstring, a vector of parameters in square brackets, and a body. The value of the last expression in the body is automatically returned—there is no return keyword.

Create a file named functions.clj:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
;; A named function with a docstring and one parameter
(defn greet
  "Returns a friendly greeting for the given name."
  [name]
  (str "Hello, " name "!"))

;; A function with two parameters; the last expression is the return value
(defn add
  [a b]
  (+ a b))

(println (greet "Ada"))
(println (add 3 4))
  • defn combines def (bind a name) and fn (create a function) in one step.
  • [name] is the parameter vector—parameters are listed inside square brackets.
  • The body (str "Hello, " name "!") is the return value; Clojure functions return their last expression implicitly.

Multi-Arity and Variadic Functions

A single Clojure function can accept different numbers of arguments. Each arity is written as its own (params body) clause. You can also accept an arbitrary number of arguments using &, which gathers the rest into a sequence.

Create a file named functions_arity.clj:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
;; Multi-arity: one definition handles zero or one argument
(defn welcome
  ([] (welcome "stranger"))          ; zero-arg version calls the one-arg version
  ([name] (str "Welcome, " name "!")))

;; Variadic: & gathers any extra arguments into a sequence called numbers
(defn sum-all
  [& numbers]
  (apply + numbers))                  ; apply spreads the sequence into +

(println (welcome))
(println (welcome "Rich"))
(println (sum-all 1 2 3 4 5))
  • Multi-arity lets one function provide default behavior—(welcome) delegates to (welcome "stranger").
  • & numbers captures every remaining argument as a sequence.
  • apply calls a function with the elements of a sequence as its arguments, so (apply + [1 2 3]) is (+ 1 2 3).

Recursion Instead of Loops

Functional languages favor recursion over mutable loop counters. A function calls itself with a smaller problem until it reaches a base case. Here is the classic factorial.

Create a file named functions_recursion.clj:

1
2
3
4
5
6
7
8
9
;; Recursive factorial: multiply n by the factorial of n-1
(defn factorial
  [n]
  (if (<= n 1)
    1                        ; base case
    (* n (factorial (dec n))))) ; recursive case

(println (factorial 5))
(println (factorial 10))
  • (dec n) subtracts one from n; (<= n 1) is the base case that stops recursion.
  • Each call waits for the next, so (factorial 5) expands to (* 5 (* 4 (* 3 (* 2 1)))).
  • For very deep recursion, Clojure offers loop/recur for constant-stack iteration, but plain recursion is clear and correct for small inputs like these.

Closures and Higher-Order Functions

Functions are values in Clojure. A function can return another function that “closes over” the variables in scope—this is a closure. Functions that take or return other functions are called higher-order functions, and they power Clojure’s data-processing style.

Create a file named functions_higher_order.clj:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
;; make-adder returns a new function that remembers n (a closure)
(defn make-adder
  [n]
  (fn [x] (+ x n)))

(def add-10 (make-adder 10))

;; Higher-order functions take other functions as arguments
;; #(* % %) is shorthand for (fn [x] (* x x))
(println (add-10 5))
(println (map #(* % %) [1 2 3 4]))   ; transform each element
(println (filter even? (range 10)))  ; keep elements matching a predicate
(println (reduce + [1 2 3 4 5]))     ; combine elements into one value

;; The ->> threading macro pipes a value through a series of functions
(println (->> (range 1 6)
              (map #(* % %))         ; square each number
              (filter even?)         ; keep the even squares
              (reduce +)))           ; add them up
  • (fn [x] (+ x n)) captures n from the enclosing scope—add-10 always adds 10.
  • map, filter, and reduce are higher-order functions; they take a function as their first argument.
  • #(* % %) is the anonymous-function reader shorthand, where % is the first argument.
  • The ->> threading macro reads top-to-bottom, passing each result as the last argument of the next call—a clean alternative to deeply nested parentheses.

Running with Docker

Run any of the example files with the official Clojure image. Replace the filename as needed.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# Pull the official image
docker pull clojure:latest

# Run the basic functions example
docker run --rm -v $(pwd):/app -w /app clojure:latest clojure functions.clj

# Run the other examples
docker run --rm -v $(pwd):/app -w /app clojure:latest clojure functions_arity.clj
docker run --rm -v $(pwd):/app -w /app clojure:latest clojure functions_recursion.clj
docker run --rm -v $(pwd):/app -w /app clojure:latest clojure functions_higher_order.clj

Expected Output

Running functions.clj:

Hello, Ada!
7

Running functions_arity.clj:

Welcome, stranger!
Welcome, Rich!
15

Running functions_recursion.clj:

120
3628800

Running functions_higher_order.clj:

15
(1 4 9 16)
(0 2 4 6 8)
15
20

Key Concepts

  • defn defines named functions with an optional docstring, a parameter vector in square brackets, and an implicit return of the last expression.
  • No return keyword—a function evaluates to the value of its final form.
  • Multi-arity lets one function name handle different argument counts, often to provide defaults by delegating between arities.
  • Variadic arguments use & rest to collect extra arguments into a sequence, frequently combined with apply.
  • Recursion replaces loops in idiomatic functional code; always provide a base case to terminate.
  • Closures capture surrounding bindings, letting you generate specialized functions like make-adder.
  • Higher-order functions (map, filter, reduce) take functions as arguments and are the backbone of Clojure’s data transformation pipelines.
  • The ->> threading macro makes nested function calls readable by piping a value through each step.

Running Today

All examples can be run using Docker:

docker pull clojure:latest
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining