Search code examples
scalatypespath-dependent-typeabstract-type

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!


Solution

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