Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

scalaz.Equal for path dependent types

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#Bs?

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?

like image 210
Chirlo Avatar asked Sep 27 '22 21:09

Chirlo


1 Answers

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.

like image 96
Travis Brown Avatar answered Nov 01 '22 15:11

Travis Brown