'(1 2) equals [1 2]?

Feeling confused why they are eval?

cljs.user=> (= '(1 2) [1 2])
true
cljs.user=> (pop '(1 2))
(2)
cljs.user=> (pop [1 2])

For SEO: why a list equals a vector in Clojure?

They are equal because they are both sequences/seqable. There are many types of these, e.g. (range 5) returns a clojure.lang.LongRange, while (range) returns a clojure.lang.Iterate, and map returns a LazySeq. This is fine, as long as their contents are the same Clojure will treat them as the same.

I can accept that a vector and a sequence can be equal since they have same items in the same order. But as I apply pop on them, it feels strange.

It looks like:

x = y
f(x) /= f(f)

pop behaves different depending on which type of collection it is applied to. Because List does prepending efficiently, pop works the same way, it removes the first item of a list, unlike in Vector, which is an indexed collection. This kind of differences are everywhere in Clojure between lists and vectors.

(pop [1 2 3]) ;; [1 2]
(pop (list 1 2 3)) ;; (2 3)

= 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

Learnt that before when I found that it was so slow to manipulate a list from its tail. They are quite obvious after I run into troubles for so many times.

@plexus Thanks for the explanation, it does help. I’m convinced that it is by design. Is there another function for detecting equality, which matches the = I described?(not including identical? since it detects by reference.)

If I do need that (almost never) I tend to do (and (= (type x) (type y)) (= x y)).

(type is identical to class, except if a value has a :type metadata, then it uses that)

server.core=> (type [])
clojure.lang.PersistentVector
server.core=> (type ^{:type :thingie} [])
:thingie
2 Likes

Nice tip. I think I may try using it as a replacement of Record. I used to use record to tell a special data structure from nomal maps. https://github.com/Cumulo/recollect/blob/master/src/recollect/types.cljs#L7-L9

I didn’t know about the metadata feature. Any example where it could be useful?

I remember Friend uses it to recognize the identity map. That’s where I first saw it used.

In general I imagine it could be good combined with multimethods for lightweight polymorphism.

Good explanations but I do want to chase this rabbit a little bit further.

 (= #{1 2 3} '(1 2 3))
false

Now why is that?

Interestingly, the set implementation of equiv checks if the comparable object is a set, otherwise it returns false

if (!(obj instanceof Set))
return false;

Which led me to the horrifying thought: What if = treats the left and right arguments differently?

Looking a bit deeper though, there’s an interface Sequential that lists and vectors implement that sets don’t.

It’s a good decision, since if we compared element by element we’re not supposed to have any guarantees of ordering by set, but it’s a little strange in light of the vector vs seq comparison.

2 Likes

Let’s think about the ways in which this could work. When comparing two things of disparate types you basically have two options, either you coerce one thing into the type of the other, or you find some underlying abstraction they both share and use that as a basis for comparison.

I believe that e.g. JavaScript does the coercion. When you say A == B it’ll turn B into an A and then compare A == B-as-A.

There are two downsides to this: first, it’s slow. All this coercing takes extra time, and will make it hard to have predictable performance. Second: it opens the door to asymmetry, when A-as-B == B is not the same as A == B-as-a.

Clojure takes a different approach, its collections are based on underlying abstractions like Associative, Counted and Sequential. Vectors and lists are Sequential, sets are not. Note that this is different from both Seq and Seqable

I used seq/seqable a bit loosely in my other post, because for a lot of operations they’re more or less interchangeable, but in this case it’s important to distinguish all three: seq, seqable, sequential.

(map (fn [coll] [(seq? coll) (seqable? coll) (sequential? coll)]) [[] '() #{} {}])
;; seq?   seqable?   sequential?
([false     true        true]     ;; []
 [true      true        true]     ;; ()
 [false     true        false]    ;; #{}
 [false     true        false])   ;; {}

Only the list is a seq, it’s inherent structure is a tuple of <first, rest>. Lists and vectors are sequential?, they represent a collection of elements in a specific order. All of them are seqable?, you can traverse them element by element.

So when comparing (= [1 2 3] '(1 2 3)) Clojure will find the common ground between these collections: the fact that they are both Sequential, and use that as a basis for comparison.

In the case of (= #{1 3 2} (sorted-set 1 2 3)) the common ground is that they’re both sets, so the comparison still works.

(= #{1 2 3} [1 2 3]) these two have no common ground. They’re both seqable, so they can be turned into a seq , but that would mean doing coercion, which in this case could be really problematic, because when you turn a set into a seq you have no idea of the order the elements will be in, so in some cases this could return true, and in others false.

HTH :slight_smile:

6 Likes