'(1 2) equals [1 2]?

= is more an equivalence (equi-valent meaning “having the same value”) than a strict equality. So yes (= x y) does not imply (= (f x) (f y))

Operations like pop are polymorphic, they are implemented separately for each collection type in a way that makes sense for that collection. The same is true e.g. for conj, which also tends to confuse people initially.

When you’re wondering about the details of a core method I find it’s most helpful to just dive and check the source. It’s the only way to get a definitive answer.

(defn =
  "Equality. Returns true if x equals y, false if not. Same as
  Java x.equals(y) except it also works for nil, and compares
  numbers and collections in a type-independent manner.  Clojure's immutable data
  structures define equals() (and thus =) as a value, not an identity,
  comparison."
  {:inline (fn [x y] `(. clojure.lang.Util equiv ~x ~y))
   :inline-arities #{2}
   :added "1.0"}
  ([x] true)
  ([x y] (clojure.lang.Util/equiv x y))
  ([x y & more]
   (if (clojure.lang.Util/equiv x y)
     (if (next more)
       (recur y (first more) (next more))
       (clojure.lang.Util/equiv y (first more)))
     false)))

Most of this code is here to deal with multiple arguments, the real implementation is in Java (you’ll find this is the case for most of the core methods). So let’s find Util.java

static public boolean equiv(Object k1, Object k2) {
    if(k1 == k2) {
        return true;
    }
    if(k1 != null) {
        if(k1 instanceof Number && k2 instanceof Number)
            return Numbers.equal((Number)k1, (Number)k2);
        else if(k1 instanceof IPersistentCollection || k2 instanceof IPersistentCollection)
            return pcequiv(k1,k2);
        return k1.equals(k2);
    }
    return false;
}

ok, here the real logic starts. First it checks if the two things you’re comparing are the exact same objects (or identical primitives). Otherwise it differentiates three cases: numbers, Clojure collections, or other objects.

If you drill down and look at the numbers check it mostly smooths over some differences between Java primitives, i.e. an Float and a Double with the same value are considered equivalent, same for Integer and Long.

The collection equivalence delegates to the actual collection implementation. This is where lists and vectors come in. If you follow the rabbit hole you’ll eventually get to the equiv method on classes like APersistentVector, where you’ll see that it work with anything that’s a Vector, a List, or Sequential.

For pop the story is similar, this one delegates to clojure.lang.RT (short for “runtime” I think) where a lot of the core stuff is implemented.

(defn pop
  "For a list or queue, returns a new list/queue without the first
  item, for a vector, returns a new vector without the last item. If
  the collection is empty, throws an exception.  Note - not the same
  as next/butlast."
  {:added "1.0"
   :static true}
  [coll] (. clojure.lang.RT (pop coll)))

Which then delegates to the concrete object

Looking around for this I also came across a reference to this article: Equal Rights for Functional Objects (Baker), which is also mentioned and explored in this old blog post by Technomancy.

The Clojure guides on Data Structures and Sequences are also worth reading.

Sorry for the long post. I know this kind of stuff is cause for a lot of initial confusing, but really deep down it makes a lot of sense :slight_smile: Hope that helps!

6 Likes