Beginner

Operators in Clojure

Learn arithmetic, comparison, logical, and threading operators in Clojure - functions in prefix notation with practical Docker-ready examples

In most languages, operators are special syntactic forms distinct from regular function calls. In Clojure — as in all Lisps — operators are just ordinary functions called using prefix notation. +, -, =, and, and or are all functions (or macros) that you invoke the same way you invoke any other function: with the name first, inside parentheses.

This uniform model has surprising consequences. Operators are variadic by default: (+ 1 2 3 4) works just like (+ 1 2). There is no operator precedence to memorize because parentheses make evaluation order explicit. And because operators are first-class values, you can pass + or < to higher-order functions like reduce and map.

This tutorial covers arithmetic, comparison, logical, and equality operators, plus Clojure’s idiomatic threading macros (-> and ->>) which serve as functional “pipe” operators for composing transformations.

Arithmetic Operators

Clojure’s arithmetic operators are variadic functions. They accept any number of arguments and return a single result.

Create a file named arithmetic.clj:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
;; Basic arithmetic - all functions are variadic
(println "Addition:        " (+ 2 3 4))
(println "Subtraction:     " (- 20 5 3))
(println "Multiplication:  " (* 2 3 4))
(println "Division:        " (/ 20 4))

;; Integer vs ratio division
(println "Ratio result:    " (/ 10 3))      ; => 10/3 (exact ratio)
(println "Float division:  " (/ 10.0 3))    ; => 3.333...
(println "Integer quotient:" (quot 10 3))   ; => 3
(println "Remainder:       " (rem 10 3))    ; => 1
(println "Modulo:          " (mod -7 3))    ; => 2 (mod differs from rem for negatives)

;; Exponentiation and absolute value
(println "Power 2^10:      " (Math/pow 2 10))
(println "Absolute value:  " (abs -42))

;; Increment and decrement helpers
(println "Increment:       " (inc 5))
(println "Decrement:       " (dec 5))

;; Min and max are also variadic
(println "Min of values:   " (min 3 1 4 1 5 9 2 6))
(println "Max of values:   " (max 3 1 4 1 5 9 2 6))

Note that (/ 10 3) returns the exact ratio 10/3, not a truncated integer or an inexact float. Clojure preserves precision unless you opt into floating-point by including a decimal.

Comparison and Equality

Comparison operators in Clojure are also variadic, and they check that the relationship holds across all arguments in sequence. This means (< 1 2 3 4) is a single call that tests strictly ascending order — there is no need to chain comparisons with and.

Create a file named comparison.clj:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
;; Numeric comparison - variadic and chainable
(println "1 < 2:           " (< 1 2))
(println "1 < 2 < 3 < 4:   " (< 1 2 3 4))   ; => true (all ascending)
(println "1 < 3 < 2:       " (< 1 3 2))     ; => false
(println "5 >= 5 >= 4:     " (>= 5 5 4))    ; => true

;; Equality
(println "= integers:      " (= 1 1 1))           ; => true
(println "= mixed numeric: " (= 1 1.0))           ; => false (different types)
(println "== numeric only: " (== 1 1.0))          ; => true (numeric equivalence)
(println "= collections:   " (= [1 2 3] [1 2 3])) ; => true (value equality)

;; Not equal
(println "not= example:    " (not= 1 2))

;; Type-aware predicates
(println "zero?:           " (zero? 0))
(println "pos?:            " (pos? 5))
(println "neg?:            " (neg? -3))
(println "even?:           " (even? 4))
(println "odd?:            " (odd? 7))

The distinction between = and == matters: = is value equality and is strict about types, while == is numeric equivalence that treats 1, 1.0, and 1/1 as equal.

Logical Operators

Clojure’s and, or, and not are short-circuiting. Importantly, and and or return values, not booleans — they return the actual operand that determined the result. This makes them useful for providing defaults and guarding expressions.

In Clojure’s truthiness model, only false and nil are falsy. Everything else — including 0, empty strings, and empty collections — is truthy.

Create a file named logical.clj:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
;; and returns the first falsy value, or the last value if all are truthy
(println "and all true:    " (and 1 2 3))         ; => 3
(println "and with false:  " (and 1 false 3))     ; => false
(println "and with nil:    " (and 1 nil 3))       ; => nil

;; or returns the first truthy value, or the last value if all are falsy
(println "or first truthy: " (or nil false 42))   ; => 42
(println "or all falsy:    " (or nil false))      ; => false

;; Idiomatic default values
(defn greet [name]
  (str "Hello, " (or name "stranger")))
(println (greet "Ada"))
(println (greet nil))

;; not always returns a boolean
(println "not true:        " (not true))
(println "not nil:         " (not nil))
(println "not 0:           " (not 0))      ; => false! 0 is truthy

;; Short-circuit evaluation prevents division by zero
(defn safe-divide [a b]
  (and (not (zero? b)) (/ a b)))
(println "safe 10/2:       " (safe-divide 10 2))
(println "safe 10/0:       " (safe-divide 10 0))

Threading Macros: Clojure’s Pipe Operators

Threading macros are one of Clojure’s most beloved features. They let you write data transformations as a top-to-bottom pipeline instead of deeply nested function calls. -> (thread-first) inserts each result as the first argument of the next form; ->> (thread-last) inserts it as the last argument.

Create a file named threading.clj:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
;; Without threading - read inside-out, right-to-left
(def result-nested
  (str (clojure.string/upper-case (clojure.string/trim "  hello world  ")) "!"))
(println "Nested:    " result-nested)

;; With -> (thread-first) - read top-to-bottom
(def result-threaded
  (-> "  hello world  "
      clojure.string/trim
      clojure.string/upper-case
      (str "!")))
(println "Threaded:  " result-threaded)

;; Thread-first works well for object-like transformations on the first arg
(def person {:name "Ada" :age 36})
(-> person
    (assoc :role "Engineer")
    (update :age inc)
    println)

;; ->> (thread-last) suits sequence operations where the collection is the last arg
(->> (range 1 11)             ; (1 2 3 4 5 6 7 8 9 10)
     (filter even?)           ; (2 4 6 8 10)
     (map #(* % %))           ; (4 16 36 64 100)
     (reduce +)               ; 220
     (println "Sum of squares of evens 1-10:"))

;; Operators as first-class values - pass + and < to higher-order functions
(println "Reduce with +:   " (reduce + [1 2 3 4 5]))
(println "Sort with <:     " (sort < [3 1 4 1 5 9 2 6]))

The fact that + and < can be passed to reduce and sort is not a special case — it works because operators are functions like any other.

Running with Docker

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

# Run each example
docker run --rm -v $(pwd):/app -w /app clojure:latest clojure arithmetic.clj
docker run --rm -v $(pwd):/app -w /app clojure:latest clojure comparison.clj
docker run --rm -v $(pwd):/app -w /app clojure:latest clojure logical.clj
docker run --rm -v $(pwd):/app -w /app clojure:latest clojure threading.clj

Expected Output

Running arithmetic.clj:

Addition:         9
Subtraction:      12
Multiplication:   24
Division:         5
Ratio result:     10/3
Float division:   3.3333333333333335
Integer quotient: 3
Remainder:        1
Modulo:           2
Power 2^10:       1024.0
Absolute value:   42
Increment:        6
Decrement:        4
Min of values:    1
Max of values:    9

Running comparison.clj:

1 < 2:            true
1 < 2 < 3 < 4:    true
1 < 3 < 2:        false
5 >= 5 >= 4:      true
= integers:       true
= mixed numeric:  false
== numeric only:  true
= collections:    true
not= example:     true
zero?:            true
pos?:             true
neg?:             true
even?:            true
odd?:             true

Running logical.clj:

and all true:     3
and with false:   false
and with nil:     nil
or first truthy:  42
or all falsy:     false
Hello, Ada
Hello, stranger
not true:         false
not nil:          true
not 0:            false
safe 10/2:        5
safe 10/0:        false

Running threading.clj:

Nested:     HELLO WORLD!
Threaded:   HELLO WORLD!
{:name Ada, :age 37, :role Engineer}
Sum of squares of evens 1-10: 220
Reduce with +:    15
Sort with <:      (1 1 2 3 4 5 6 9)

Key Concepts

  • Operators are functions+, -, <, =, and friends are ordinary functions called with prefix notation, not special syntax.
  • Variadic by default — Arithmetic and comparison operators accept any number of arguments: (< 1 2 3 4) tests strict ascending order in one call, and parentheses make evaluation order explicit so there are no precedence rules to memorize.
  • Exact arithmetic(/ 10 3) returns the ratio 10/3, preserving precision until you opt into floating-point.
  • = vs === is strict value equality (types matter); == is numeric equivalence that compares across numeric types.
  • Truthiness is narrow — Only false and nil are falsy. 0, "", and [] are all truthy.
  • and/or return values — They short-circuit and return the operand that decided the result, making them useful for defaults and guards.
  • Threading macros (->, ->>) — Transform nested calls into top-to-bottom pipelines; -> threads as first argument (object-style), ->> threads as last argument (sequence-style).
  • Operators as first-class values — Because they are functions, you can pass + to reduce or < to sort without any special wrapping.

Running Today

All examples can be run using Docker:

docker pull clojure:latest
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining