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? | Are these the same object in memory? | (identical? :a :a) → true |
You will use = for ~95% of comparisons. == shows up in numeric code.
identical? is for pointer-equality optimization (caching, memoization
keys) and is rarely needed 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 types
The 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) ;; => :a
identical? 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 objects
identical? 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)).
If you implement custom equality in a deftype, you must implement hashCode
to match — or your value will work erratically in maps and sets. defrecord
handles this for you (records are value-equal field by field).
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"})) ;; => true
Two 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
When 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. contains?on collections. It returns true only if the key is present.(contains? [10 20 30] 10)isfalse! It asks "is index 10 in bounds?" — usesomeor a set for membership:((set xs) 10).
Real-world
| Where the distinction matters | Why |
|---|---|
| Reagent / Re-frame re-renders | Reagent uses identical? 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! uses identical? 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?