I'm experimenting with path-dependent types and I run into a problem when trying to write a scalaz.Equal
instance for it. I have the following structure:
class A {
class B
}
val a1 = new A
val b1 = new a1.B // type a1.B
val a2 = new A
val b2 = new a2.B //type a2.B
I first wanted to make b1
"unequalable" (is that a word?) to b2
at compile time, which I achieved with the following:
import scalaz._
import Scalaz._
implicit def BEqual[X <: A#B]: scalaz.Equal[X] = Equal.equalA
b1 === b1 //true
b1 === b2 // doesn't compile, good
b2 === b1 // doesn't compile, good
My second experiment was try to make equality less restrictive, allowing instances of A#B
to be compared to each other but not to other types, with:
implicit val BEqual: scalaz.Equal[A#B] = Equal.equalA
But it doesn't work as expected:
b1 === b2 //doesnt' compile, === is not a member of a1.B
This works however:
BEqual.equal(b1,b2) //compiles
BEqual.equal(b1,"string") //doesnt' compile, good
So, I would like to know why the ===
doesn't work and if I can write an instance of Equal
that would apply to all A#B
s?
I tried a home brew solution with implicit conversion and it worked.
implicit class abEqual(ab: A#B) {
def eqab(ab2: A#B) = ab == ab2
}
b1.eqab(b2) //ok
b2.eqab(b1) //ok
b1.eqab("String") //doesn't compile, good
So why doesn't this work with scalaz.Equal
?
In your first BEqual
you're saying that for any subtype of A#B
you want to provide an Equal
instance for that subtype. When the compiler sees b1 ===
it therefore finds the Equal[a.B]
instance, since the static type of b1
is a.B
. This makes things work out as you'd expect.
In your second BEqual
, you're defining an Equal
instance only for A#B
. This means that even b1 === b1
won't compile, since the static type of b1
is more specific than A#B
, and Equal
is invariant in its type parameter. If you upcast your values the instance will work just fine:
scala> val ab1: A#B = b1
ab1: A#B = A$B@464ef4fa
scala> val ab2: A#B = b2
ab2: A#B = A$B@2d3b749e
scala> ab1 === ab2
res1: Boolean = false
In the version where you call BEqual.equal
directly, you're essentially accomplishing the same thing—method arguments are always covariant, so when you pass something statically typed as a a.B
as a A#B
argument, everything will work just fine. In your hand-rolled implicit class, you're similarly just saying that you want to work with any old A#B
.
You can see the same kind of thing when you write Some(1) === Some(1)
vs. Option(1) === Option(1)
(or some(1) === some(1)
). Scalaz provides an Equal
for Option[A: Equal]
, but not for Some[A: Equal]
, and when the first argument has a more specific static type the Option
instance won't be found.
This isn't something you want to work around, since the invariance of Scalaz's Equal
is intentional. If you want to work with A#B
values as A#B
values in this context, you'll need to upcast them explicitly.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With