I'd like to use abstract type and type refinement to encode something like a functional dependency between two types.
trait BaseA {
type M
type P <: BaseB.Aux[M]
def f(m: M): Unit
}
trait BaseB {
type M
def m(): M
}
object BaseB {
type Aux[M0] = BaseB { type M = M0 }
}
It means that a A
only works with a B
if they have the same type M
inside.
With the following concret classes
class A extends BaseA {
type M = Int
type P = B
def f(m: Int): Unit = {
println("a")
}
}
class B extends BaseB {
type M = Int
def m(): M = 1
}
So now they have both Int
type as M
, but the following code does not compile
val a: BaseA = new A
val b: BaseB = new B
def f[T <: BaseA](a: T, b: T#P): Unit = {
a.f(b.m())
}
The compiler tells me that a.f
here expect a path dependent type a.M
but it got a b.M
.
My question here is how can I express the fact that I only need the M
type matched in the type level, not the instance level? Or how can I prevent the M
in def f(m: M): Unit
becoming a path-dependent type?
Thanks!
I think the issue comes from b
being related to a type T
, and it being possible for a
to be a subclass of T
that could override M
to be something else, making the two objects incompatible. For instance:
class A2 extends BaseA {
type M = String
type P = B2
def f(s: String): Unit = {
println(s)
}
}
class B2 extends BaseB {
type M = String
def m(): M = "foo"
}
val a: BaseA = new A
val b: BaseB = new B2
f[BaseA](a, b)
It seems like if your f
were to compile, then all of this should compile too.
You can either make b
's type dependent on a.P
:
def f(a: BaseA)(b: a.P): Unit
Or I think the whole thing is simplified by not having the compatible types restriction on your classes, but rather require that one is a subclass of the other at the point that they interact:
trait BaseA {
type M
def f(m: M): Unit
}
trait BaseB {
type M
def m(): M
}
class A extends BaseA {
type M = Int
def f(m: Int): Unit = {
println("a")
}
}
class B extends BaseB {
type M = Int
def m(): M = 1
}
val a: A = new A
val b: B = new B
def f(a: BaseA, b: BaseB)(implicit sub: b.M <:< a.M): Unit = {
a.f(sub(b.m()))
}
f(a, b)
Or lastly, consider whether you need these to be path-dependent types at all; could they be regular generic type parameters?
trait BaseA[-M] {
def f(m: M): Unit
}
trait BaseB[+M] {
def m(): M
}
class A extends BaseA[Int] {
def f(m: Int): Unit = {
println("a")
}
}
class B extends BaseB[Int] {
def m(): Int = 1
}
def f[T](a: BaseA[T], b: BaseB[T]): Unit = {
a.f(b.m())
}