Gain of Multiversal Equality

Whats the gain of multiversal equality, if

(1::2::Nil).contains("hello") will be discarded while (1::2::Nil).contains("hello":Any) not?

If the latter will also be discarded then we have a backward compatibility break.

Not quite sure what you mean by “discarded” here – the point of multiversal equality is making it possible to prevent compilation of some comparisons that don’t make sense.

And the whole point is that some code that used to compile won’t do so now (since it never made sense in the first place) – in that sense, compatibility is a lesser concern than correctness.

5 Likes

Not quite sure what you mean by “discarded” here

Discarded by the compiler, denied, rejected.

the point of multiversal equality is making it possible to prevent compilation of some comparisons that don’t make sense

But (1::2::Nil).contains("hello":Any) doesn’t make sense, right? However, afaic it will be compiled.

Upcasting to Any allows to silence or disable also other error or warnings. For example "abc" match { case x: Int => } doesn’t compile, but instead compiler shows:

scrutinee is incompatible with pattern type;
 found   : Int
 required: String

If you instead write ("abc": Any) match { case x: Int => } then code compiles and throws a MatchError in runtime. Does that make previous scrutinee checking worthless? I don’t think so. Checking is done on a best effort basis. It’s not meant to be an undefeatable mechanism.

In Java System.out.println((String) new Integer(5)); doesn’t compile and compiler throw error: error: incompatible types: Integer cannot be converted to String
If you instead add upcast to Object then it will compile and fail at runtime, similarly to the example with match in Scala. System.out.println((String) (Object) new Integer(5)); compiles and throws at runtime following exception: java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String

4 Likes

Note that for historical reasons, contains does not rely on the equality type class (in principle it should!).

You can achieve the same with .exists(_ == e) though, which does use multiversal equality.

Neither of your adapted examples works when you enable strictEquality:

import scala.language.strictEquality

(1::2::Nil).exists(_ == ("hello":Any))
// ^ gives error: Values of types Int and Any cannot be compared with == or !=

You can try it here.

2 Likes

Maybe I just misunderstand you. My point is the following:

Given the example from Wiki

  def contains[U >: T](x: U)(given Eql[T, U]): Boolean 

If we apply contains to a list of Int and an Int, all is fine, but if we apply it to an list of Int and a single String it fails because String is neither a supertype of int nor is there a witness Eql[Int,String] supporting the equality.
Unfortunately, List[Int] and String can be subsumed to List[Any] and Any.
Moreover, there is a witness Eql[Any,Any] supporting the equality comparison.
So this call is valid anyway:

(1::2::Nil) contains "hello" /*with*/ contains:List[Any]->Any->Bool (given Eql[Any,Any])

I’m wondering why your code example still works, do you import just List[Int] into your scope s.t. List[Any] isn’t available?

Not under strict equality, there is not:

(1: Any) == ("a": Any)
// ^ error: Values of types Any and Any cannot be compared with == or !=

I provided you with a Scastie link to try the examples. Please just have a look and try it yourself: Scastie - An interactive playground for Scala.

1 Like

Not under strict equality, there is not:

(1: Any) == ("a": Any)
// ^ error: Values of types Any and Any cannot be compared with == or !=

Does it makes sense to deny comparing two elements of the same type from an idiomatic standpoint?
I mean how does Set.add can check if two elements of type Any are equal in order to omit duplicates?

Yes, it absolutely makes sense. Not all types have meaningful equality. Any is one of them. Function types also cannot be meaningfully compared for equality.

It just so happens that Java and the JVM define some equals method for everything. It falls back to comparing object identity (an implementation detail), and is pretty much just a useful hack. In a language like Haskell, equality is purely based on a type class, and some types do not implement it.

2 Likes

Another example, which mostly comes up in test code, the entire Throwable class hierarchy has no meaningful equality. A compilation error for this would have been preferable to having to bounce around the Java standard library sources to figure out why what looked superficially like a reasonable test was failing.

1 Like

Not sure what you mean by that? Isn’t an instance of Throwable equal only to itself? Would you consider that meaningless?

I’m assuming the problem with Any is that some instances are only equal to themselves, while other instances are not. Or am I missing something?

An instance of Throwable is only equal to itself, but an instance of Throwable that’s serialized and deserialized isn’t necessarily equal to it’s original instance.

Conceptually, it would be nice if two Throwable of the same type, with the same message, cause, suppressed, stack trace, and etc were equal. Or at least it would make writing certain tests easier.

Thankfully, this is something which only occasionally comes up in tests, and it’s easy enough to work around with cats.Eq or a custom matcher in the test framework, but it was a bit of a surprise the first time it happened.

1 Like

@LPTK wrote:

Function types also cannot be meaningfully compared for equality.

@morgen-peschke wrote:

Another example, which mostly comes up in test code, the entire Throwable class hierarchy has no meaningful equality.

It sounds to me you want to provide structural/semantic equality which is generally undecidable, wouldn’t be better to name it like that, maybe SEql?

Then it would be more clear which sort of equality it means and why it isn’t compatible with nonstrict.==.

Generally, I admire to have both options for equality, structural and nominal because one of them is needed in order to differentiate between two elements in a set. And because Eql doesn’t cover all elements in the Any Universe, the only remaining candidate is nonstrict.==.
Otherwise, we are not allowed to add any element to a set.

Yeah, one could argue that if a type is Serializable (which Throwable is), then two instances should be the same if the serialization is the same.

1 Like

Nah, simpler than that: at least for me it’s mostly about clarity.

Defining equality with an automatic fallback to “do x & y point to the same location in memory” can lead to unpleasant surprises because most of the time that isn’t what you want. Granted, in the mutable paradigm Java was designed for, that’s sometimes the best you can do, but even then it only causes trouble when the fallback happens silently and you end up comparing memory locations, when you think you’re comparing objects.

So, for me at least, Multiversal Equality is a huge improvement because it helps avoid accidentally mixing the two types of equality checks in the places where using something like cats.Eq isn’t feasible.

3 Likes