Given a wrapper class case class Foo(x: String), define a custom Equality for strings:
implicit val upperCaseEquality =
new Equality[String] {
def areEqual(a: String, b: Any): Boolean =
b match {
case s: String => a.toUpperCase == s.toUpperCase
case _ => false
}
}
As expected, the following holds true:
"hello" should equal ("HELLO") // this is fine
However, this fails:
Foo("hello") should equal (Foo("HELLO")) // this fails!
By the principle of least surprise, the second test should hold true as well.
Hi Daniel,
There is also a risk of surprise in doing equality recursively by default for case classes, which is that people can override equals and hashCode in class Foo itself. Then it might be surprising to users that Foo's equals method equality is not used by ScalaTest/Scalactic by default.
The other way this shows up is that collections don't pick up an implicit Equality instance by default. That has long bothered me, as well as the lack of type errors for always-false or always-true comparisons, but to do that requires a lot of implicits that frankly would be confusing for users to figure out. Here are a few examples:
I got it working in master two summers ago, but ended up deciding that was the wrong direction for two reasons. One is the implicit lookups slowed the compile, even though 99% of the time the equality that ultimately got used was the equals method, so to me that was not worth it. The other reason was just that the complexity was unsatisfying, and didn't seem worth it, again because 99% of the time what would get used was the underlying equals method.
To do it right, you need to really do recursive equality for collections and types like Option, in Java and in Scala, and because that's not the default way of doing that in Scala, it would require a huge number of implicits hidden somewhere in Scalactic. (If such implicits were part of Scala, they could be spread out among all the Scala API, but that wouldn't work for the Java API.) So what I ended up with is what we had before, which is by default === calls ==, and you can customize it for a type. It means that if you want Foo's Equality to pick up a custom String equality, you need to define a custom equality for Foo.
The one thing I did customize by default was Array equality, because it was very inconsistent with Scala collections and seemed error prone. But you end up with an inconsistency internally that two arrays can be compared with === and you get deep equality, but arrays inside collections or Scala Option does not compare them deeply, for the reason described above. If I had to do it over again, I would probably _not_ do that for Array just so === in Scalactic/ScalaTest always calls ==. SuperSafe now doesn't allow arrays to be compared with ==, and we could have done the same for ===.
This makes sense. Thanks for the illuminating answer.
Most helpful comment
Hi Daniel,
There is also a risk of surprise in doing equality recursively by default for case classes, which is that people can override equals and hashCode in class Foo itself. Then it might be surprising to users that Foo's equals method equality is not used by ScalaTest/Scalactic by default.
The other way this shows up is that collections don't pick up an implicit Equality instance by default. That has long bothered me, as well as the lack of type errors for always-false or always-true comparisons, but to do that requires a lot of implicits that frankly would be confusing for users to figure out. Here are a few examples:
https://github.com/scalatest/scalatest/blob/master/scalactic/src/main/scala/org/scalactic/EqualityConstraint.scala#L45
I got it working in master two summers ago, but ended up deciding that was the wrong direction for two reasons. One is the implicit lookups slowed the compile, even though 99% of the time the equality that ultimately got used was the equals method, so to me that was not worth it. The other reason was just that the complexity was unsatisfying, and didn't seem worth it, again because 99% of the time what would get used was the underlying equals method.
To do it right, you need to really do recursive equality for collections and types like Option, in Java and in Scala, and because that's not the default way of doing that in Scala, it would require a huge number of implicits hidden somewhere in Scalactic. (If such implicits were part of Scala, they could be spread out among all the Scala API, but that wouldn't work for the Java API.) So what I ended up with is what we had before, which is by default === calls ==, and you can customize it for a type. It means that if you want Foo's Equality to pick up a custom String equality, you need to define a custom equality for Foo.
The one thing I did customize by default was Array equality, because it was very inconsistent with Scala collections and seemed error prone. But you end up with an inconsistency internally that two arrays can be compared with === and you get deep equality, but arrays inside collections or Scala Option does not compare them deeply, for the reason described above. If I had to do it over again, I would probably _not_ do that for Array just so === in Scalactic/ScalaTest always calls ==. SuperSafe now doesn't allow arrays to be compared with ==, and we could have done the same for ===.