This trick enables overloading `==` for opaque types

FWIW…

That was my first instinct, if only for the fact that we can write custom implementations of other operators the same way. Then I read very nice blogs on the Internet showing that I was wrong.

I would love that world but unless I’m missing a critical piece of puzzle (in which case I’ll make a fool of myself) we’d have to consider how using type classes compose in generic contexts, as I suspect the following wouldn’t work:

class SomeNewSet[T, Eq[T]]

def f[T](elements: Seq[T]) =
  val s = SomeNewSet(elements.head)
  ...

Maybe libraries and compilers are different, since libraries have less knowledge of the context. I count 4 instances of overloaded == in the dotty compiler itself, and maintain this was the right naming choice.

=== won’t help in the Set[T] scenario. Either way, contains falls back to equals. So I am not really sure what === gains you? To me it always seemed like unwelcome complexity, with a whiff of inelegance.

=== buys you clarity. You immediately know that it is not the same thing as ==, which Sets and Maps and other indexOf/contains/etc. use. Using == for something semantically different brings confusion.

4 Likes

From my experience, there is great confusion for users of DSL-based libraries in Scala to have both “===” and “==”.

1 Like

=== makes it clear it is something different, and makes it clear that it won’t work in Set.contains. Overloaded == makes it looks like it should work, but it doesnt.

== can also be problematic because overload resolution can be unpredictable and dependent on type inference, so depending on how the types are inferred, you get wildly different runtime behavior.

e.g. in database query libraries, the difference between a == b returning an Expr[Boolean] and a == (b: Any) returning a hardcoded false can be very surprising and hard to debug, especially since every library out there will implicitly lift false to Expr(false)

I’d avoid using “code I wrote myself” as justification that something is broadly used. Of the libraries I know, Spark, SLICK, Squeryl and Scalatest use ===. The MongoDB drivers use a static equals method. Only macro-based “direct” libraries like Quill or Scalatest’s assert macros use ==

My own Scalasql uses ===, overrides (not overloads!) == to throw an exception, and provides a separate method for comparing JVM object equality. Not pretty, but it’s the least foot-gun-y approach i could come up with and at least ensures you don’t do the wrong thing accidentally

3 Likes

I completely agree with @soronpo here. Having === and == is a recipe for confusion. People will reach for == and if that’s not the right equality this is worse than worrying about contains (which btw in a properly designed system should use equals instead of ==).

=== was a kludge in languages like Jacascript that have the wrong definition of ==. There you learn from the ground up that there’s a difference but it’s not pretty.

== can also be problematic because overload resolution can be unpredictable and dependent on type inference, so depending on how the types are inferred, you get wildly different runtime behavior.

Yes, I get that. But here you are arguing from the very specific use case of database query libraries that lift results of == to something different from Boolean. I agree in that case you can’t have both == as the query-level equality and have automatic lifting of Boolean to Expr[Boolean]. In my preferred design, I would have == and no automatic lifting of Booleans. Standard SQL has no Boolean datatype anyway (even though some implementations have it). But that design choice is arguable.

My main point is that there are a large number of use cases where maintaining a unique equality == is better than the alternatives. === is an admission of defeat. Sometimes defeat is unavoidable because of technical reasons with overloading resolution that come up in specific cases like query libraries. But I don’t want to elevate it to a feature!

I haven’t claimed so far that == is broadly used. I have said it’s used four times in the compiler and that I maintain it’s the right naming choice. if you disagree, provide arguments why it isn’t.

But then I got curious and did a search on github: def == appears 1.9k times, def === 3k times. Anyway this should be enough to validate my claim, which was:

still people use overloaded ==.

The true number is probably even higher, as this omits the instances where === is used by way of something like cats.Eq and a single def === in an extension method is effectively providing an === overload to many classes.

One of the handy things === gains you is that it can be (and usually is) defined to accept a more strongly typed argument, so you don’t have to deal with the type checking required by == accepting Any.

You can already define a more restrictive overloading == that does not cater to Any, and strict equality can be set so only the specific types you support can be applied in ==.

Many of those def == seem to fall in two categories:

  • “Clones” of the Scala stdlib that GitHub does not understand as clones (copies)
  • More efficient versions of equals when the right-hand-side is statically known to have the right type → these don’t have the pitfalls discussed above, because they are still consistent with the def ==(that: Any) inherited from Any.
1 Like

What if we do the following (!= the same as ==, but neglected for brevity):

  1. Change def == inside Any to be protected[Any] def ==.
  2. Add a global inline def == (that: Any): Boolean extension method that directly calls the internal protected == def.

Will this break anything for source or binary compatibility? By the looks of things it will keep both compatibilities, while enabling both the classic inheritance overloading, and additionally the possibility of using extension methods for overloading.

Yes, I would expect that many uses of == would be more efficient versions of equals. That does not invalidate my point that people use overloaded ==. And I think it would be great if these overloads could be extension methods.

Hashed Sets have been mentioned a few times in this thread. I find it interesting that scala programmers will frequently use Ordering and it may be the best example of a type-class for teaching to newcomers to typeclasses, scala.util.hashing.Hashing and scala.math.Equiv haven’t caught on.

One could imagine that .## and .== could require one of those typeclasses in scope and be extension methods. If you want the jvm ones, .hashCode and .equals could remain on AnyRef for java compatibility. We could potentially have per-file opting out of rewriting == to equals to allow migration.

We could implement HAMT data structures using the Hashing typeclass.

Such HAMT datastructures were done in cats-collections for those that want to avoid issues of confusion of jvm equals/hashCode and use a fully parametric approach based on typeclasses:

3 Likes