Reputation: 26008
It's kind of complicated to learn Scala's generic bounds. I know that:
T : Tr
- T
has type of Tr
, meaning it implements a trait Tr
T <: SuperClass
- T
is subclass of SuperClass
T :> ChildClass
- T
is superclass of ChildClass
However, there are also many more operators:
<%
and %>
=:=
<:<
and >:>
<%
and %>
<%<
and >%>
I read about them, but as I said, there was was not comprehensible explanations. Could you make it clearer?
Upvotes: 3
Views: 108
Reputation: 2401
I've used a few type constraints:
The easiest are <:
and >:
. These are simple bounds on the type hierarchy.
trait A
trait B extends A
trait C extends B
then for a method
def doSomething[T >:C <:B](data:T)
either B
or C
can be substituted instead of T
Then type constraints that involves an addition of implicit parameter to the method.
def doSmth1[T: MyTypeInfo] (data:T)
is rewritten during compilation as
def doSmth1[T] (data:T)(implicit ev: MyTypeInfo[T])
whereas
def doSmth2[T <% SomeArbitratyType] (data:T)
is rewritten as
def doSmth2[T] (data:T)(implicit ev: T => SomeArbitratyType)
Both of the methods can be called if in the scope there is an instance that fits the implicit parameter. If there is no appropriate instance then compiler issues an error.
The view bound (<%
) requires an implicit conversion that converts T
to an instance of the other type (SomeArbitratyType
).
More powerful is using "type classes". Inside the type class instance one may put many useful methods that can deal with the type T
. In particular, one may put a conversion method and achieve similar result as view bounds.
Examples:
trait MyTypeInfo[T] {
def convertToString(data:T):String
}
def printlnAdv[T : MyTypeInfo](data:T) {
val ev = implicitly[MyTypeInfo[T]]
println(ev.convertToString(data))
}
somewhere in the scope there should be implicit value of type MyTypeInfo[T]
:
implicit val doubleInfo = new MyTypeInfo[Double] {
def convertToString(data:Double):String = data.toString
}
or
implicit def convertToString(data:T):String
def printlnAdv[T <% String](data:T) {
val conversionResult = data : String
println(conversionResult)
}
somewhere in the scope there should be implicit function:
implicit def convertDoubleToString(data:Double):String = data.toString
The next weird symbols are =:=
and <:<
. These are used in methods that wish to ensure that a type has some property. Of course, if you declare a generic parameter then it is enough to have <:
and >:
to specify the type. However, what to do with types that are not generic parameters? For instance, the generic parameter of an enclosing class, or some type that is defined within another type. The symbols help here.
trait MyAlmostUniversalTrait[T] {
def mySpecialMethodJustForInts(data:T)(implicit ev:T =:= Int)
}
The trait can be used for any type T
. But the method can be called only if the trait is instantiated for Int
.
Similar use case exists for <:<
. But here we have not "equals" constraint, but "less than" (like T<: T2
).
trait MyAlmostUniversalTrait[T] {
def mySpecialMethod(data:T)(implicit ev:T <:< MyParentWithInterestingMethods)
}
Again the method can only can called for types that are descendants of MyParentWithInterestingMethods
.
Then <%<
is very similar to <%
, however it is used the same way as <:<
— as an implicit parameter when the type is not a generic parameter. It gives a conversion to T2
:
trait MyAlmostUniversalTrait[T] {
def mySpecialMethod(data:T)(implicit ev:T <%< String) {
val s = data:String
...
}
}
IMHO <%<
can safely be ignored. And one may simply declare the required conversion function:
trait MyAlmostUniversalTrait[T] {
def mySpecialMethod(data:T)(implicit ev:T => String) {
val s = data:String
...
}
}
Upvotes: 3