~ / 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
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

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

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 Double by default; if you match against Long keys, you lose. cheshire/jsonista have options to read integers as longs.
  • contains? on collections. It returns true only if the key is present. (contains? [10 20 30] 10) is false! It asks "is index 10 in bounds?" — use some or a set for membership: ((set xs) 10).

Real-world

Where the distinction mattersWhy
Reagent / Re-frame re-rendersReagent 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 / 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)compare-and-set! uses identical? 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