Mark Whitfield Mark Whitfield - 2 months ago 12
Scala Question

Scala -- multiple F-bounded types in inheritance tree

I have a generic F-bounded trait in Scala. Lets me write methods that return the same underlying implementation type, super! But now lets say a sub-trait also defines methods that require F-bounding. Scala is sending me back compile errors that don't make sense:

package sandbox

import sandbox.ComplexImpl.AnyComplexImpl

import scala.language.existentials

trait FBounded[IMPL <: FBounded[IMPL]] { self: IMPL =>
def foo: IMPL
}

trait FBoundedUser[F <: FBounded[F]] {
def bar(value: F): F = value.foo
}

trait SimpleImpl extends FBounded[SimpleImpl] {
override def foo: SimpleImpl = this
}

object SimpleUser extends FBoundedUser[SimpleImpl]

// A-OK so far...

trait ComplexImpl[IMPL <: ComplexImpl[IMPL]] extends FBounded[IMPL] { self: IMPL =>
def baz: IMPL
}

object ComplexImpl {
type AnyComplexImpl = ComplexImpl[T] forSome { type T <: ComplexImpl[T] }
}

object ComplexUser1 extends FBoundedUser[ComplexImpl[_]]
object ComplexUser2 extends FBoundedUser[AnyComplexImpl]


Trying to compile with either of
ComplexUser1
or
ComplexUser2
results in:

Error:(32, 29) type arguments [sandbox.ComplexImpl.AnyComplexImpl] do not conform to trait
FBoundedUser's type parameter bounds [F <: sandbox.FBounded[F]]


Which doesn't make sense to me.
AnyComplexImpl
definitely implements
FBounded
. Am I missing something, or is the type checker just failing me here?

EDIT:

class Concrete() extends ComplexImpl[Concrete] {
override def baz: Concrete = this

override def foo: Concrete = this
}
object ComplexUser3 extends FBoundedUser[Concrete]


Compiles just fine. So why won't the generic version?

Answer

The constraint requires AnyComplexImpl to implement FBounded[AnyComplexImpl], which it doesn't, not just FBounded[T] forSome { type T <: ComplexImpl[T] }.

It works if you make FBounded and ComplexImpl covariant. The compiler reasons that:

  1. AnyComplexImpl is a supertype of any ComplexImpl[T], where T <: ComplexImpl[T];

  2. So FBounded[T] forSome { type T <: ComplexImpl[T] } is a subtype of FBounded[AnyComplexImpl];

  3. So AnyComplexImpl is a subtype of FBounded[AnyComplexImpl].

But I suspect trying to mix F-bounded types and existential is likely to lead to other problems down the line. The reason ComplexUser1 and ComplexUser2 don't compile is precisely that they are not generic. Consider using actually generic version instead:

def complexUser4[T <: ComplexImpl[T]] = new FBoundedUser[T] {}