Large redaction of the original question: now I present the whole code upfront, without showing the variants explaining my motivation. Apologies for the confusion.
I need a simple type class implementing a projection on one of type's member types - for the purpose of this example lets make it a straightforward cast:
trait Subject {
type E
type Const
}
object Subject {
implicit def projection :Projection[Subject] { type Project[X] = Subject { type E = X } } = ???
}
abstract class Projection[S <: Subject] {
type Project[X] <: Subject { type E = X }
}
implicit class ProjectSubject[S <: Subject](private val self :S) extends AnyVal {
def project[X](implicit p :Projection[S]) :p.Project[X] = ???
}
class Box[X] extends Subject { type E = X }
object Box {
implicit def projection[A] :Projection[Box[A]] { type Project[X] = Box[X] } = ???
}
class Adapter[S <: Subject] extends Subject { type E = S#E }
object Adapter {
implicit def adapterProjection[S <: Subject](implicit p :Projection[S])
:Projection[Adapter[S]] { type Project[X] = Adapter[p.Project[X]] } = ???
}
val res = new Adapter[Box["E"]].project["F"]
In the example above, it is clear that the projection should be recursive, with Subject
subclasses declaring their own rules. Obviously, I'd like the projection to be contravariant in effect:
class Specific extends Adapter[Box["E"]]
val spec = (new Specific).project["F"] //doesn't compile
If Specific
does not provide its own projection, the one for Adapter
should be used, with the last expression evaluating to Adapter[Box["F"]]
. This works nicely if I declaer Projection[-S <: Subject]
, but the problem is that I need the projections to preserve some properties, here expressed as the Const
member type:
class Projection[S <: Subject] {
type Project[X] <: Subject { type E = X; type Const = S#Const }
}
I dropped this constraint from the code above for clarity, as it doesn't contribute to the problem.
In the previous example, the compiler will complain about the lack of an implicit Projection[Specific]
, without trying to upcast the value. How to make it compile with usage site variance?
Not with existentials:
implicit class ProjectSubject[S <: Subject](private val self :S) extends AnyVal {
def project[X](implicit p :Projection[_ >: S <: Subject]) = ???
}
My guess was that the wildcard here is equivalent to Subject
and no implicits other than Projection[Subject]
will be searched for from the compiler -Xlog-implicits
logs of the unabridged problem (which had a large Subject hierarchy with more implicit projection declarations).
I then tried the trick with an intermediate contravariant implicit, which sometimes works:
abstract class ProjectionAvailable[-S <: T, T <: Subject] //extends (S => T)
implicit def ProjectionAvailable[S <: Subject](implicit p :Projection[S]) :ProjectionAvailable[S, S] = ??? //(s :S) => s
implicit def ProjectionSubject[S <: T, T <: Subject](s :S)(implicit witness :ProjectionAvailable[S, T]) =
new ProjectionSubject[T](s)
class ProjectionSubject[S <: Subject](private val self :S) extends AnyVal {
def project[X](implicit p :Projection[S]) :p.Project[X] = p.asInstanceOf[p.Project[X]]
}
This looked promising, but unfortunately the compiler goes about this exactly as before: looks at the available implicit, instantiates type parameters as ProjectionAvailable[Specific, T]
and complains for the lack of Projection
, without taking advantage of its contravariance. I tried a variant with
class ProjectionAvailable[S <: T, T <: Subject]
without any real difference apart for a more clear error. I tried integrating
the ProjectionAvailable
into Projection
, but it also changed nothing:
class Projection[-S <: T, T] { /* as before */ }
My hunch is that it probably is doable, but requires crafty guiding the compiler by hand in type inference and for now I am out of new avenues to explore.
I can't reproduce behavior you mentioned (that's why I asked in comments how you test that
def project[X](implicit p :Projection[_ >: S <: Subject]) = ???
or approach withProjectionAvailable
do not work for you).With your approach with existential
Projection
inProjectSubject
I additionally definedProjection[Specific]
and the code doesn't compile with errorso the implicit for
Projection[Specific]
is among candidates and I can't see how the following can be trueIf I make
adapterProjection
of lower priority than my additional implicitProjection[Specific]
thenprints
so it's
Projection[Specific]
that is selected.Scala 2.13.3.
https://scastie.scala-lang.org/Ts9UOx0aSfWuQJNOoVnSAA
Behavior for your original contravariant
Projection
(without type typeConst
) and the firstProjectSubject
(with non-existentialProjection
) is the same.(My answer in In scala 2.13, how to use implicitly[value singleton type]? can be relevant.)
By the way, with invariant
Projection
andProjectionAvailable
I don't have to prioritize implicits andProjection[Specific]
is selected.https://scastie.scala-lang.org/nePYqjKGSWm8IRGLmYCwAA
And when I don't define additional implicit
Projection[Specific]
your approach with existentialProjection
seems to work,adapterProjection
is selected. What's wrong with this behavior?https://scastie.scala-lang.org/GiLoerYgT0OtxKyechezvA