darkjh
darkjh

Reputation: 2861

Abstract type and path dependent type in scala

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!

Upvotes: 1

Views: 196

Answers (1)

Joe K
Joe K

Reputation: 18424

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())
}

Upvotes: 1

Related Questions