Is it possible to put an upper type bound mandating that a class has a concrete definition of a member type?

72 Views Asked by At

Let's consider:

 trait Abstract { 
    type T 
    def get :T
    def set(t :T) :Unit
 }

 class Concrete[X](var x :X) extends Abstract { 
     override type T = X 
     override def get :T = x
     override def set(t :T) = x = t
 }

 class Generic[M <: Abstract](val m :M) {
     def get :M#T = m.get
     def set(t :M#T) :Unit = m.set(t)
     def like[N <: Abstract](n :N) :Generic[N] = new Generic(n)
     def trans[N <: Abstract](n :N) :N#T = like[N](n).get
 }

Class Generic rightfully won't compile here because M#T can be literally any type in existence and not m.T. This can be 'fixed' in two ways:

     def set(t :m.T) :Unit = ???

or

     class Generic[M <: Abstract with Singleton]

First is unfeasible because in practice Generic might not even have an instance of M to use its member type (which happens in my real-life case), and passing around path dependent types in a compatible way between classes and methods is next to impossible. Here's one of the examples why path-dependent types are too strong (and a huge nuissance):

val c = new Concrete[Int](0)
val g = new Generic[c.type](c)
c.set(g.get)

The last line in the above example does not compile, even though g :Generic[c.type] and thus get :c.type#T which simplifies to c.T.

The second is somewhat better, but it still requires that every class with a M <: Abstract type parameter has an instance of M at instantiation and capturing m.T as a type parameter and passing everywhere a type bound Abstract { type T = Captured } is still a huge pain.

I would like to put a type bound on T specifying that only subclasses of Abstract which have a concrete definition of T are valid, which would make m.T =:= M#T for every m :M and neatly solve everything. So far, I haven't seen any way to do so. I tried putting a type bound:

 class Generic[M <: Abstract { type T = O } forSome { type O }]

which seemingly solved the issue of set, but fails with trans:

def like[N <: Abstract { type T = O } forSome { type O }](n :N) :Generic[N] = ???
def trans[N <: Abstract { type T = O } forSome { type O}](n :N) :N#T = ???

which seems to me like a clear type system deficiency. Additionally, it actually makes the situation worse in some cases, because it defines T early as some O(in class Generic) and won't unify it when additional constraints become available:

def narrow[N <: M { type T = Int }](n :N) :M#T = n.get

The above method will compile as part of class Generic[M <: Abstract], but not Generic[M <: Abstract { type T = O } forSome { type O}]. Replacing Abstract { type T = O } forSome { type O } with Concrete[_] yields very similar results. Both of these classes could be fixed by introducing a type parameter for T:

 class Generic[M <: Abstract { type T = O }, O]
 class Generic[M <: Concrete[_]]

Unfortunately, in several places I rely on two-argument type constructors such as

trait To[-X <: Abstract, +Y <: Abstract]

and use the infix notation X To Y as part of my dsl, so changing them to multi-argument, standard generic classes is not an option.

Is there a trick to get around this problem, namely to narrow down legal type parameters of a class so that their member type (or type parameter, I don't care) are both expressible as types and compatible with each other?. Think of M as some kind of factory of values of T and of Generic as its input data. I would like to parameterize Generic in such a way, that its instances are dedicated to concrete implementation classes of M. Do I have to write a macro for it?

1

There are 1 best solutions below

1
francoisr On

I'm not sure exactly why you're saying that using a path dependent type will be annoying here, so maybe my answer will be completely off, sorry about that.

What's the problem with this?

class Generic[M <: Abstract](val m: M) {
  def get: M#T = m.get
  def set(t: m.T): Unit = m.set(t)
}

object Generic {
  def like[N <: Abstract](n: N): Generic[N] = new Generic(n)
  def trans[N <: Abstract](n :N): N#T = like(n).get
}

class Foo
val f = new Generic(new Concrete(new Foo))
f.set(new Foo)

You're saying you won't always have an instance m: M, but can you provide a real example of exactly what you want to achieve?