scalagenericsinheritancecovariancegeneric-variance

Usage of Type parameterization, variance vs Inheritance base class


I have a question on when to use type parameterization vs base class type when defining a method/class in Scala especially when the types allowed are in same hierarchy - constrained using type bound.

for eg:

trait TypeA
case class SubTypeA1(in: String) extends TypeA
case class SubTypeA2(in: String) extends TypeA

case class NewType[T <: TypeA](inTypeA: T) 
case class NewTypeV2(inTypeA: TypeA) 

def testMethod1(in: String): NewType[TypeA] = NewType(SubTypeA1(in))
def testMethod2(in: NewType[TypeA]): Unit = print("Some functionality")
def testMethod3(in: String): NewTypeV2 = NewTypeV2(SubTypeA1(in))
def testMethod4(in: NewTypeV2): Unit = print("Some functionality")

In the above case/in general when constraining allowed types to a certain upper bound, what advantage would I get with NewType over NewTypeV2 or vice versa? They both look equivalent to me.

As I understand it, if I were to add some implicit condition check like NewType[T: Addable] thats present across types of different hierarchies then type parameterization would make sense, besides that what are the reasons I should prefer type parameterization over using Interface type or Base class type like the type of inTypeA member of NewTypeV2 case class.

Is defining type parameterized way like NewType[T] considered more "Functional" style over the other?

And Secondly, question on Variance vs Type bound. In the above code block NewType is Invariant on Type T, so NewType[SubTypeA1] is not a subtype of NewType[TypeA], they are unrelated, correct?

If my understanding on type invariance is correct as mentioned above, how is the testMethod1 compiling? As I am explicitly passing SubTypeA1 but still it gets casted to NewType[TypeA] and also it can be passed to testMethod2 without issue. What am I misunderstanding here?

scala> testMethod1("test")
res0: NewType[TypeA] = NewType(SubTypeA1(test))

scala> testMethod2(testMethod1("test"))
Some functionality

scala> NewType(SubTypeA1("tst"))
res3: NewType[SubTypeA1] = NewType(SubTypeA1(tst))

Solution

  • case class NewType[T <: TypeA](inTypeA: T) 
    case class NewTypeV2(inTypeA: TypeA) 
    

    The difference between these two is the type of inTypeA:

    In NewType the type of inTypeA is the type parameter T which can be any subtype of TypeA. The compiler knows what the actual subtype is.

    In NewTypeV2 the type of inTypeA is TypeA. The value will be a subtype of TypeA, but all the compiler knows is that it is a TypeA

    This matters if you have a method that requires a particular subtype of TypeA:

    def subtypeMethod(in: SubTypeA1) = ???
    
    val a1 = SubTypeA1("string")
    
    val n = NewType(a1)
    subtypeMethod(n.inTypeA) // Yes, inTypeA is type SubTypeA1
    
    val n2 = NewTypeV2(a1)
    subtypeMethod(n2.inTypeA) //No, inTypeA is type TypeA