Deriving `CanEqual` succeeds on case class with non-`CanEqual` member. Why?

I’m relatively new with Scala. I’m not sure if this question should have gone in the general forum, but it seemed language lawyery so I put it here.

I’m not sure why the following code compiles:

import scala.language.strictEquality

case class C(x: String => String) derives CanEqual

Shouldn’t this fail because CanEqual is not defined on String => String and hence can’t be derived? It would seem to me that the most obvious way to implement the deriving clause was to apply it recursively to the sub elements. I’m guessing this is how things like Ord work also. But it seems here maybe it’s just using the Java’s .Equals() method, which is meaningless on functions. Why not implement CanEqual so it just recursively applies CanEqual and fails if the sub elements don’t have the required CanEqual trait? If this is falling back to .Equals(), when is this actually a good idea given that the whole point of the CanEqual class (as far as I understand) is to be explicit about defining equality, in contrast to the Java approach of just giving everything equality, even types where it doesn’t make sense?

Hey, thank you for your question. I have taken a look in the compiler source code and here is where derivation for CanEqual is implemented:

it seems that for a class with no type parameters CanEqual is given for free, and if there are type parameters, then a CanEqual instance is also required for each type parameter in order to provide CanEqual for a concrete instantiation of the class.

So to answer the question, you can derive CanEqual for C here because term parameters are not considered.

However, looking at the docs on desugaring it is clear that the motivation does involve proving that parameters of generic types can be compared.

So is this behaviour intended or is it a bug or do we not know?

I think it is intentionally a very lightweight implementation, because it would be difficult to provide a good solution for all cases. E.g. there is a comment in the code:

derives CanEqual is opt-in, so if the semantics don’t match those
appropriate for the deriving class the author of that class can provide
their own instance in the normal way.

If we wanted to be watertight then we could only provide CanEqual for classes with compiler synthesised equality methods, that way we would know each element being used in the comparison. If the user overrides equals then we would have to interpret that equality method to discover which fields were used so we can require the appropriate CanEqual evidence.

Some confusion may come from other languages where deriving an Equality type class means to actually generate the equality function, whereas in Scala, CanEqual provides no methods, it is only a user signal to the compiler.

So I think another way to think about it is derives CanEqual means

I am happy with how equals is already implemented, I am signalling it is safe to use, (provided I have evidence my generic fields are also safe to compare)

rather than

please create an equals method for me with good semantics

1 Like

Sorry I’m new to all this so I’m a bit confused. The Multiversal Equality page says the following:

The CanEqual object defines instances for comparing

  • the primitive types Byte, Short, Char, Int, Long, Float, Double, Boolean, and Unit,
  • java.lang.Number, java.lang.Boolean, and java.lang.Character,
  • scala.collection.Seq, and scala.collection.Set.

Shouldn’t just deriving CanEqual work on all case classes which are made up either of not only the previously mentioned types (presumably these are the compiler generated ones you are referring too) but also any type which has CanEqual defined on them? Why would one need to restrict it only to compiler generated CanEqual types only?

1 Like

This is because we are allowed to derive CanEquals for this class too:

class NeverEquals derives CanEqual:
  override def equals(that: Any) = false 

we can see by inspection that this class does not have any sensible idea of equality, yet we are allowed to derive CanEqual, this is because we don’t know what the user defined equals method could be without expensive analysis

Ah, gotcha. This makes sense now. I presume I could make my own trait to do the “equals method with good semantics” thing yes?

thats right, we have an example on the Scala 3 website for how you could do this