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))
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