Multiversal equality in Scala 3

I was making good progress in my conversion to Scala 3 – then I ran into equality testing.

As you may recall, I developed a Scalar class that represents physical scalars.

I want to be able to test a non-dimensional scalar for equality with a real number or an integer. For example,

(6 * sec) / (3 * sec) == 2

I have read the online material at

https://dotty.epfl.ch/docs/reference/contextual/multiversal-equality.html

and I have tried to follow the examples, but I just can’t get it to work. For example, I have

override def equals(that: Any): Bool = that match {
  case that: Scalar => num == that.num && (num == 0 || units == that.units)
  case that: Real  => this == Scalar(that)
  case that: Int   => this == Scalar(that)
  case _ => throw new RuntimeException(s"$this cannot = $that")
  }

given CanEqual[Scalar, Scalar] = CanEqual.derived
given CanEqual[Scalar, Real]  = CanEqual.derived
given CanEqual[Scalar, Int]  = CanEqual.derived

But I still get many errors like this:

[error] 68 | if (det != 0) { // check for intersection
[error] | ^^^^^^^^
[error] | Values of types scalar_.Scalar and Int cannot be compared with == or !=

I have tried with and without

import scala.language.strictEquality

Any ideas about what the problem might be? Thanks.

I have no experience with Scala 3 at all, yet, but from what I see I’d guess this isn’t possible.

The intent of CanEqual seems to be to disallow equality comparisons between different types (e.g. Scalar/Int). It only “accidentally” has two type parameters - conceptually it’s intended to be only one, and the two that are there for pragmatic reasons are expected to have an “isAssignable” relationship.

It looks like you cannot have a meaningful CanEqual[Int, Scalar] instance, since you cannot define this logic in Scalar#equals() and CanEqual is sealed on purpose, so you can only derive instances, but not define any from scratch.

So CanEqual[Scalar, Int] just seems to be a “leak” in the intended type safety. But my understanding may be wrong…

It works for me:

scala> Scalar(1) == 3                                                           
1 |Scalar(1) == 3
  |^^^^^^^^^^^^^^
  |Values of types Scalar and Int cannot be compared with == or !=

scala> given CanEqual[Scalar, Int]  = CanEqual.derived                          
lazy val given_CanEqual_Scalar_Int: CanEqual[Scalar, Int]

scala> Scalar(1) == 3
val res0: Boolean = false

Did you define given CanEqual[Scalar, Int] in the implicit scope (e.g. in the companion object) of Scalar?

1 Like

Seems to me the given reference has examples of different types being compared to each other.

Yes, for U >: T, i.e. with an “isAssignable” relationship. And it seems possible to implement CanEqual[T, U] and CanEqual[U, T] for arbitrary reference types T and U if you control their #equals() implementation. But I don’t see how to implement a semantically sound CanEqual[Int, Scalar].

However, maybe I read too much into the question and the OP doesn’t require the symmetry property.

True, in this case you can’t/shouldn’t define CanEqual[Int, Scalar] which means the equality relationship isn’t reflexive. However the compiler does seem to allow non-reflexive CanEqual[Scalar, Int] to work. Whether that’s a good idea is a different question…

Thanks for the suggestions. I had the “given CanEqual” statement in the wrong place (inside the class constructor). After I moved it to the enclosing package object, the compiler suggested that I use

import scalar_.given_CanEqual_Scalar_Int

When I added that import statement to a client file, I was able to test equality between a Scalar and an Int.

However, that seems like an arbitrary name, and it applies only to the one file. I would have to add this import statement to every file in which I want that functionality, and that is onerous. I didn’t have to do that in Scala 2. I hope there is a better way.

Yeah, putting it in the companion object of your type (Scalar) so it is always on the implicit scope.

Check: https://stackoverflow.com/questions/5598085/where-does-scala-look-for-implicits/5598107#5598107

I don’t understand why, if I already have

import scalar_._

I also need

import scalar_.given_CanEqual_Scalar_Int

Shouldn’t that already be imported with the wildcard import? What am I missing?

The underscore wildcard doesn’t capture givens. See
https://dotty.epfl.ch/docs/reference/contextual/given-imports.html

Alternative, as already suggested: Place the given in the companion object.

2 Likes

My Scalar class is currently in a package object. I tried putting the “given” in the package object outside of the class definition, but that didn’t work. Shouldn’t that have the same effect as putting it in the companion object? If not, I guess I will have to break the package object up into a class and companion object.

You shouldn’t be using a package object on the first place, so yeah split it would be the best solution.

It is different from scala2. Now you need import scalar.given to import givens/implicits.

2 Likes