Equality and identity
BasicClojure has three "equality" operators, each answering a different question:
| Operator | Question | Example |
|---|---|---|
= | Are these values structurally equal? | (= [1 2 3] (list 1 2 3)) → true |
== | Are these numbers numerically equal? | (== 1 1.0) → true |
| identical?clojure.core/identical?True if x and y are the same reference (Java identity).view on clojuredocs → | Are these the same object in memory? | (identical? :a :a) → true |
You'll reach for = almost every time. == shows up in numeric code.
identical?clojure.core/identical?True if x and y are the same reference (Java identity).view on clojuredocs → is for pointer-equality optimization (caching, memoization
keys) and rarely matters at the application level.
= is value equality, even across types
(= [1 2 3] [1 2 3]) ;; => true
(= [1 2 3] '(1 2 3)) ;; => true — sequential collections compare element-wise
(= {:a 1 :b 2} {:b 2 :a 1}) ;; => true — maps compare by key/value, order doesn't matter
(= #{1 2 3} #{3 2 1}) ;; => true
(= "x" :x "x") ;; => false — different typesThe rule: two values are = if they have the same information content,
where "same" respects the type category (sequential vs map vs set) but
not the concrete class.
== is numeric equality (with coercion)
(= 1 1.0) ;; => false — different numeric types
(== 1 1.0) ;; => true — numerically equal
(== 1 1.0 1N 1M) ;; => true — long, double, BigInt, BigDecimal
(== 1/2 0.5) ;; => true= is strict about numeric type because Clojure's hash code must be
consistent with equality — and 1 (long) and 1.0 (double) hash
differently. If you want them to compare equal, you want ==.
This is also why using 1.0 as a map key and looking up by 1 won't
find anything:
(get {1 :a} 1.0) ;; => nil
(get {1 :a} 1) ;; => :aidentical?clojure.core/identical?True if x and y are the same reference (Java identity).view on clojuredocs → is pointer equality
(identical? :foo :foo) ;; => true — keywords are interned
(identical? "x" "x") ;; => true — string literals are interned by the JVM
(identical? (str "x") (str "x")) ;; => false — two newly constructed strings
(identical? [1 2] [1 2]) ;; => false — two distinct vector objectsidentical?clojure.core/identical?True if x and y are the same reference (Java identity).view on clojuredocs → is fast (a single reference comparison) and useful inside hot-path caches or as an early bailout: "if it's the same object, I already know the answer."
Hash and equality contract
Clojure enforces the JVM contract: (= a b) ⇒ (= (hash a) (hash b)).
That is, if two values are equal, they must hash to the same number — this
is what lets maps and sets find things in O(1).
For everyday code (vectors, maps, records), Clojure handles this for you. The
only time it matters is if you later define a custom type with deftypeclojure.core/deftypeDefine a low-level type; like defrecord but without map semantics.view on clojuredocs → and
override equality: you must also override hashCode to match, or your value
will behave erratically as a map key.
Records vs maps
A record with the same fields as a map is not equal to that map:
(defrecord User [name])
(= (->User "alice") {:name "alice"}) ;; => false
(= (->User "alice") (->User "alice")) ;; => true
(= (->User "alice") (map->User {:name "alice"})) ;; => trueTwo records compare equal only if they have the same record type and the same field values. This is intentional: records carry behavior (protocol methods) and identity that maps don't.
Try it
The contains? gotcha
This one trips up almost every newcomer. contains? asks "is this key
present?" — not "is this value in the collection?" For a vector, the keys
are indices:
(contains? [10 20 30] 10) ;; => false — there is no index 10
(contains? [10 20 30] 1) ;; => true — index 1 exists (and holds 20)
(contains? {:a 1 :b 2} :a) ;; => true — keys are :a and :b
(contains? #{10 20 30} 10) ;; => true — for sets, keys *are* the valuesFor "is this value in the collection?" use some with a set, or just call
the set as a function:
(some #{10} [10 20 30]) ;; => 10 (truthy)
((set [10 20 30]) 10) ;; => 10Other places the distinction bites
- Numeric map keys. If your data has mixed integers and decimals,
normalize before keying — or compare with
==and search manually. - JSON round-trips. JSON numbers become
Doubleby default; if you match againstLongkeys, you lose.cheshire/jsonistahave options to read integers as longs.
Real-world
| Where the distinction matters | Why |
|---|---|
| Reagent / Re-frame re-renders | Reagent uses identical?clojure.core/identical?True if x and y are the same reference (Java identity).view on clojuredocs → first, then =, to decide if a component should re-render — value equality of large maps is cheap because Clojure short-circuits on structural sharing |
| Datomic / XTDB | Treats 1 and 1.0 as distinct; schema matters for query results |
| Memoization keys | clojure.core/memoize uses =; pass canonical types or normalize first |
clojure.set/difference and friends | Operate on = semantics — works for value-equal records and maps |
| Concurrency primitives (atoms, refs) | compare-and-set!clojure.core/compare-and-set!CAS update on an atom; returns true if the swap happened.view on clojuredocs → uses identical?clojure.core/identical?True if x and y are the same reference (Java identity).view on clojuredocs → to detect intervening writes |
| Spec / Malli validators | Compare against =-equal sample data; integer vs double mismatches surface here |
Check yourself
? quiz
What does `(= 1 1.0)` return and why?
Exercise
Construct a map keyed by integers that you'll later look up using values from a JSON payload (which become doubles). What's your strategy?
- Normalize on write (
(int k)when building the map). - Normalize on read (
(get m (long k))). - Use a custom lookup that compares with
==.
For each, sketch one downside. Which would you actually ship?