Understand use of type member constraints

I have the following example that fails to compile:


class BaseToCompare
class IsA extends BaseToCompare
class IsB extends BaseToCompare
class IsC extends IsA

trait UseBase:
    type TT <: BaseToCompare

    def do1[T <: TT](t:T): Unit 
    def do2(t:TT): Unit 
  
object UseBaseA extends UseBase :
    type TT <: IsA 

    def do1[T <: TT](t:T): Unit = ()
    def do2(t:TT): Unit = ()

UseBaseA.do1(IsC())
UseBaseA.do2(IsC())

I have always though that:

TT <: IsA

meant that I can use a type that is a subtype of IsA where TT is expected. Apparently this is not so. The explanation given to me is that:

" it means that TT is some abstract type which is known to be a subtype of IsA"

Does this simply mean that I must always assign a type in UseBaseA using for example?

TT = IsA

TIA.

There is one specific (though abstract) type TT, and you (or rather: the compiler) know that TT <: IsA and that IsC <: IsA - but not IsC <: TT, as would be required for unifying IsC with T when calling #do1().

To me it’s a bit surprising that abstract types in concrete objects are possible at all in Scala, but obviously that’s only news to me.

So you don’t have to assign the abstract type, but then you can’t assume any relationship with concrete external types (other than what’s explicitly given through the bounds).

2 Likes

To answer your direct question: No, it doesn’t make much sense to have type TT <: IsA in an object and type TT = IsA probably does indeed exactly what you want.

But I sense some hesitation, where you seem to think that that’s somehow less useful in some scenario’s? What causes your hesitation to just use type TT = IsA?

Think I understand. If I am reading this correctly, given that type TT <: IsA the compiler knows that TT could be IsC or any other subtype of IsA. More concretely I could have a IsD <: IsC that could be used. So compiler cannot in effect tell what that lower bound is (IsC, IsD ?).

So in practical terms I really need to set an explicit class to interact with the rest of the system.

Thank you

I wanted to describe the case that the methods I define in UseBaseA can only use TT of a given subtype. More concretely I was thinking of using a type TT <: IsA so that the the calls would work for any subtype of IsA.

Honestly, I am overthinkling this. As you said the type TT = IsA does what I want (standard OO relations). This question came up because when upgrading to 3.0.0-RC2, compilation broke on some code like this.

At least I learned something.

Thank you

I wouldn’t even pull in further assumptions about the IsA hierarchy. TT already is a proper type that doesn’t have any relationship with IsC. You constrain the (method-level) T to be a subtype of TT, and IsC doesn’t match this constraint - that’s it. The same holds for any other hypothetical subtype of IsA, such as IsD, but their (potential) existence is not required for this reasoning. (We already know that TT is neither IsC nor IsD.)

Yes, I’d think so. There probably is some esoterical “phantom type” use case for this construct, but I cannot come up with one off my head.

That’s what you would get for TT = IsA already, from vanilla subtyping rules. do2(IsC()) works because IsC is_a IsA, and for do1(IsC()), T could either be bound to IsA or IsC.

Yep. As I said, overthinking it. Thanks again.