~ / track A / clojure basics

Equality and identity

Basic

Clojure has three "equality" operators, each answering a different question:

OperatorQuestionExample
=Are these values structurally equal?(= [1 2 3] (list 1 2 3))true
==Are these numbers numerically equal?(== 1 1.0)true
i‌d‌e‌n‌t‌i‌c‌a‌l‌?Are these the same object in memory?(identical? :a :a)true

You'll reach for = almost every time. == shows up in numeric code. i‌d‌e‌n‌t‌i‌c‌a‌l‌? 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 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

i‌d‌e‌n‌t‌i‌c‌a‌l‌? 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

i‌d‌e‌n‌t‌i‌c‌a‌l‌? 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 d‌e‌f‌t‌y‌p‌e 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"})) ;; => 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

loading sci
press ⌘/Ctrl-↵ or click ▶ run to evaluate

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 values

For "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)       ;; => 10

Other 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 Double by default; if you match against Long keys, you lose. cheshire/jsonista have options to read integers as longs.

Real-world

Where the distinction mattersWhy
Reagent / Re-frame re-rendersReagent uses i‌d‌e‌n‌t‌i‌c‌a‌l‌? 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 / XTDBTreats 1 and 1.0 as distinct; schema matters for query results
Memoization keysclojure.core/memoize uses =; pass canonical types or normalize first
clojure.set/difference and friendsOperate on = semantics — works for value-equal records and maps
Concurrency primitives (atoms, refs)c‌o‌m‌p‌a‌r‌e‌-‌a‌n‌d‌-‌s‌e‌t‌! uses i‌d‌e‌n‌t‌i‌c‌a‌l‌? to detect intervening writes
Spec / Malli validatorsCompare 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?

  1. Normalize on write ((int k) when building the map).
  2. Normalize on read ((get m (long k))).
  3. Use a custom lookup that compares with ==.

For each, sketch one downside. Which would you actually ship?

 status: new