Is there a pattern/trick to enforce '<:' when defining type aliases with '='

108 views Asked by At

I ran into another issue of scala's subtype relation being non transitive (https://github.com/scala/bug/issues/12164) and I start to think if there is some general work around possible. Long story short:

trait Thing { thisThing =>
    type Super >: Self <: Thing {
        type Super <: thisThing.Super
    }
    type Self <: Thing {
        type Super = thisThing.Super
        type Self = thisThing.Self
    }
}

trait Wrapper extends Thing {
    type SuperAdapted >: Adapted <: Thing
    type Adapted <: Thing
}

trait Adapter[+T <: Thing] extends Wrapper {
    val thing :T
    type Super = Adapter[thing.Super]
    type Self = Adapter[thing.Self]
    type Adapted = thing.Self
}

type Universal[+T <: Thing] = Wrapper { type Adapted <: T }

And this does not compile:

val t :Thing
implicitly[Adapter[t.Super] <:< Universal[t.Super]]

Now I have T <: Thing { type General <: G } but not Adapter[T] <:< Universal[G]. Is there a way to work around it? One would be of course to unitilize <:<#liftCo, but it works only for values, not types, and prevents higher types from using Adapter[T] and retaining a proper subtyping relation - basically now everywhere I'd have to switch back and forth using implicits and it's completely unmanagable. Can anyone think of an alternate definition of Universal such that at least Adapter[t.Self] <:< Universal[t.Super] and Adapter[t.Super] <:< Universal[t.Super], but preferably one which encompasses all wrappers with a.thing.Self <: T for arbitrary T (because I don't even have a T <: Thing in the place where I need this subtyping to work). I don't know, type lambdas, braking it into partial type definitions to somehow manually enforce transitiveness?

2

There are 2 answers

0
Turin On BEST ANSWER

What I eventually did is I reversed the dependency. Instead of

 type Universal[+T <: Thing] = Adapter { type Adapted <: T }

(or similar), I added more member types to Thing:

trait Thing { thisThing =>
    /* as before */
    type Adapt = Wrapper { 
        type SuperAdapted <: thisThing.Super 
    }
    type AdaptSuper = Wrapper {
        type SuperAdapted = thisThing.Super
        type Adapted = thisThing.Self
    }
}

where the type SuperAdapted was ommitted from the original question in minimizing, but is required in my actual problem:

trait Adapter[+T <: Thing] {
    /* as before */
    type SuperAdapted = thing.Super
}

Now, instead of bounds [T <: Thing, A <: Universal[T]] and passing [t.Super, Adapter[Super]] I have [T <: Thing, A <: T#AdaptSuper] (or [T <: Thing, A <: T#Adapt], depending on the use case). It is not as flexible or elegant as before, but works and fits my use cases. That is, until Scala 3...

5
Dmytro Mitin On

I'm not sure I understand the logic encoded in types completely but shouldn't it be

trait Thing { thisThing =>
  type Super >: Self <: Thing {
    type Super <: thisThing.Super
    type Self = thisThing.Self
  }
  type Self <: Thing {
    type Super = thisThing.Super
    type Self = thisThing.Self
  }
}

https://scastie.scala-lang.org/KW1woPxLSoywdJPj779bPA

or

trait Thing { thisThing =>
  type Super >: Self <: Thing {
    type Super >: thisThing.Self/*instead of Self*/ <: thisThing.Super   
    type Self <: thisThing.Super
  }
  type Self <: Thing {
    type Super = thisThing.Super
    type Self = thisThing.Self
  }
}

https://scastie.scala-lang.org/QfV5f3RxScW29w7Kv0d6qA

?