Last modified on February 3, 2020 by Alex

Scala’s default equality == is a complete disaster. It is neither reflexive, symmetric, transitive, nor does it satisfy congruence or extensionality laws. It is inconsistent with equals. It does not respect types, allowing you to compare completely unrelated types.

Basically, it does not satisfy any laws at all.

Comparing unrelated types?

Let’s start with the fact that == allows you to compare unrelated types:

It is not necessarily a bad thing on its own. However, it interacts badly with non-parametric top type and cooperative equality.

Cooperative equality

Scala has to do extra work for generic parameters or abstract types deriving from Any due to implicit widening of numerical types and cooperative equality: a == b calls BoxesRunTime.equals, which results in observable performance losses.

If cooperative equality were to be removed, it would result in rather bizzare behavior of type ascription:

And speaking of bizzare behavior due to type ascription, null can be ascribed to Int, which leads to a number of puzzlers:

Another consequence of cooperative equality is that Scala’s hash code method ## is different from Java’s hashCode:

Implicit numerical widening makes == incompatible with equals even though it uses equals in its implementation for reference types:

A slightly different example of incompatibility with equals that relies on IEEE 754 instead:

IEEE 754 is a widely used technical standard for floating-point computation. The standard defines binary formats, rounding rules, and floating-point operations. Unfortunately, most major languages (but not Rust) adopted IEEE 754 equality as the implementation for == on floating-point types, which leads to all sorts of peculiar behavior (including side-effects).

In mathematics, equality is an equivalence relation, which means that it has to satisfy three axioms:

Is Scala’s == equality?

No. It is not reflexive:

And it is not transitive:

Furthermore, since it relies on equals for reference types, there are probably some non-symmetric equals in the wild as well.

Another desirable quality of equality is congruence with respect to the chosen subset of the language. In practice this means that if a equals to b, then f(a) should be equal to f(b) for any f : A => Boolean that can be implemented using only features from a certain subset of the language features.

Scala’s == is not congruent even with respect to Scalazzi, a limited subset of the language:

And it is definitely not congruent with respect to the entirety of Scala, due to a number of non-parametric methods:

The opposite of congruence is extensionality, which means that if f(a) == f(b) for every choice of f : A => Boolean in some subset of the language, then a must be equal to b.

Even extensionality is somewhat broken because == is universal:

This leads us to the next point

Side-effectful and non-terminating equality

Scala’s == is defined on all types, including functions, mutable types such as Array[Int], and infinite streams, which makes == both impure and potentially non-terminating. What is even more outrageous is that some equals methods are side-effectful, like URL.equals, and == being defined in terms of equals for reference types may also have side-effects.