Loose Equality violates Transitivity
Posted:In JavaScript:
"0" == 0; // true
0 == []; // true
"0" == []; // false
These three statements demonstrate that loose equality in JavaScript is not transitive and therefore is not a valid equivalence relation.
Despite this, the above is not a bug. It conforms exactly to ECMAScript: IsLooselyEqual.
# Equivalence Relations
An Equivalence Relation is the mathematically formal term for what we intuitively understand as "equals". In order for a relation to be a valid Equivalence Relation
, it must satisfy the following three properties:
- Reflexivity:
a == a
. - Symmetry: "If
a == b
thenb == a
". - Transitivity: "If
a == b
andb == c
, thena == c
.
By the way, NaN == NaN; // false
violates reflexivity. But there are reasons for that decision, which I will not get into in this post. If you are especially interested, I recommend: Wikipedia | Comparison with NaN
# The Easy Fix
The easiest way to alter loose equality (==
) to fix transitivity would be to treat all cross-type comparisons as false. This is exactly what the strict equality (===
) operator in JavaScript does. Reference: ECMAScript: IsStrictlyEqual
But that's boring. Let's entertain the question: are there ways we can preserve at least cross-type equalities, while also preserving transitivity?
# The Pitfalls
As soon as we allow even one equality across types we create a cross-type equivalence class.
An Equivalence Class
is the set of all elements which are equivalent to one another.
Once you have a cross-type equivalence class, you have to be very careful when considering new members of that class. It is not sufficient to check our intuition against just one member of the equivalence class, since equivalence with any member, implies equivalence to all members (via transitivity).
The above implies two statements:
- Distinct values of the same type must never be members of the same equivalence class.
- A maximal equivalence class has exactly one value from each type.
# A Silly Proposal
Given the boolean type has only two values, there can exist only two distinct equivalence classes including booleans. We can use this as the basis for a set of maximal equivalence classes.
The false
equivalence class:
- String:
''
- Number:
0
- BigInt:
BigInt(0)
- Boolean:
false
- Undefined:
undefined
- Symbol:
Symbol.for('')
- Null:
null
The true
equivalence class:
- String:
'true'
- Number:
1
- BigInt:
BigInt(1)
- Boolean:
true
- Symbol:
Symbol.for('true')
The false
class includes seven values, exactly one value from each primitive type.
The true
class has five values. The only missing types are undefined
and null
, which cannot be included without merging the two equivalence classes. I really don't like the use of 'true'
as the canonically true string, but this is what we get for pushing an idea to its limit.
In reality prohibiting cross-type equality is a perfectly sensible decision and very easy to reason about. It's no wonder that ===
is greatly preferred to ==
.
Take care,
Rupert