Please have a look at the followin code, where Extractor[A,B]
is part of a generic framework and everything else should be regarded as "client code" (I boiled it a down quite a bit and renamed everything. So don't mind that Extractor
doesn't seem to be too usefull).
scala> abstract class Extractor[A,B] {
| def extract(d:A):B
| def stringRepr(d:A):String
| }
defined class Extractor
scala> sealed abstract class Value
defined class Value
scala> case class IntValue(i:Int) extends Value
defined class IntValue
scala> case class StringValue(s:String) extends Value
defined class StringValue
scala> case class Data(i:Int, s:String)
defined class Data
scala> sealed abstract class MyExtractor[Value] extends Extractor[Data, Value] {
| def stringRepr(d:Data) = extract(d) match {
| case IntValue(i) => i.toString
| case StringValue(s) => s
| }
| }
defined class MyExtractor
scala> class IntExtractor(name:String) extends MyExtractor[IntValue] {
| def extract(d:Data) = IntValue(d.i)
| }
defined class IntExtractor
scala> class StringExtractor(name:String) extends MyExtractor[StringValue] {
| def extract(d:Data) = StringValue(d.s)
| }
defined class StringExtractor
so in short words Extractor[A,B]
is used to extract some value B
from A
and do some other things that are not represented in this show code. The abstract classes Value
and MyExtractor
are used for reasons of type savety in the "client code".
When I try to create a List
of MyExtractor
s, the following happens:
scala> val l = List.empty[MyExtractor[Value]]
l: List[MyExtractor[Value]] = List()
scala> new IntExtractor("test1") :: l
res5: List[MyExtractor[_ >: IntValue <: Value]] = List(IntExtractor@1fd96c5)
trying to convert an IntExractor
to a superclass
scala> new IntExtractor("test"):MyExtractor[Value]
<console>:24: error: type mismatch;
found : IntExtractor
required: MyExtractor[Value]
new IntExtractor("test"):MyExtractor[Value]
^
scala> new IntExtractor("test"):Extractor[Data,Value]
<console>:24: error: type mismatch;
found : IntExtractor
required: Extractor[Data,Value]
new IntExtractor("test"):Extractor[Data,Value]
I am aware that everything is fine, when I define IntExtractor
like this
scala> class IntExtractor(name:String) extends MyExtractor[Value] {
| def extract(d:Data) = IntValue(d.i)
| }
defined class IntExtractor
scala> new IntExtractor("test"):Extractor[Data,Value]
res17: Extractor[Data,Value] = IntExtractor@1653d7a
But I don't understand, why it doesn't work the way I tried it above. I would be thankfull for any help or hints.
As near as I can tell, the concept you are looking for is "covariance". Just because IntValue
is a subtype of Value
doesn't mean that MyExtractor[IntValue]
is a subtype of MyExtractor[Value]
. By default, there is no subtyping relation between those two types at all. To create such a relationship, you need to declare MyExtractor
to be covariant with respect to it's parameter. Scala lets you declare type parameters to be covariant by adding a "+" before the type parameters declaration. This is called a variance notation.
sealed abstract class MyExtractor[+Value] extends Extractor[Data, Value] {
}
Scala also supports contravariance over type parameters. Contravariance is just like covariance, but reversed, and is expressed with a "-" variance notation on the type parameter. Your Extractor
type provides an excellent example of a place where a contravariance notation makes sense.
abstract class Extractor[-A,+B] {
def extract(d:A):B
def stringRepr(d:A):String
}
This means that if Foo
is a subtype of Bar
, then Extractor[Bar, Baz]
is a subtype of Extractor[Foo, Baz]
, which if you think about it makes sense. If something can extract the data you want when passed an instance of a supertype, then by definition it can extract it when passed an instance of a subtype. Conversely, if Foo
is a subtype of Bar
, then Extractor[Baz, Foo]
is a subtype of Extractor[Baz, Bar]
. That also makes sense. If you've got an extractor that returns a Foo
, you can certainly use it wherever you need an extractor that returns a Bar
.
There are restrictions on when contravariance and covariance can be declared. For instance, contravariant type parameters can only be used as method arguments, and covariant parameters can only be used as method returns or vals. Neither can be used as vars. It gets more complicated with nested type parameters, but the rules basically boil down to "where it's sensible", and your example meets all of them.
Additional side note, all of your abstract classes in your example should probably be declared as traits instead. As long as your abstract classes don't require constructor arguments, declaring them as traits gives you a few more opportunities for reuse.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With