This code compiles with an error:
def f1[T](e: T): T = e match {
case i:Int => i
case b:Boolean => b
}
// type mismatch;
// found : i.type (with
You ask of your function to return a type T
, then you pattern-match against Int
and Boolean
.
Except your function has no evidence that Int
and Boolean
are also of type T
: when you pattern-match, you introduce the constraint that Int <: T
and Boolean <: T
.
You could either replace the return type T
by a fixed type like String
and return a String, or introduce a constraint that will satisfy both the case Int
and Boolean
.
//this compiles
def f1[T](e: T ): String = e match {
case _:Int => "integer"
case _:Boolean => "boolean"
}
//this compiles too, but will return AnyVal
def f1[T >: AnyVal](e: T ): T = e match {
case i:Int => i
case b:Boolean => b
}
Basically you can't just return any type T
dynamically because you need to prove at compile time that your function type-checks out.
The other function in your example avoids the issue by encapsulating type constraints within case classes IntExpr <: Expr[Int]
and BoolExpr <: Expr[Boolean]
(notice how Expr[_]
would be the equivalent of T
in the constraints I mentioned above). At compile time, T is properly identified in all case
s (e.g in the IntExpr
you know it's an Int
)
The first case is unsound because you underestimate the variety of types in Scala type system. It would make sense if, when we took case i:Int
branch we knew T
was Int
, or at least a supertype of Int
. But it doesn't have to be! E.g. it could be 42.type or a tagged type.
There's no such problem in the second case, because from IntExpr <: Expr[T]
, the compiler does know T
must be exactly Int
.
In addition to @Esardes answer, this worked by defining a type bound for T
:
scala> def f1[T >: AnyVal](e: T):T = e match {
| case i:Int => i
| case b:Boolean => b
| }
f1: [T >: AnyVal](e: T)T
scala> f1(1)
res3: AnyVal = 1
scala> f1(true)
res4: AnyVal = true