Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Inheritance and (automatic?) type conversion

Tags:

scala

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 MyExtractors, 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.

like image 473
Agl Avatar asked Sep 12 '10 18:09

Agl


1 Answers

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.

like image 79
Dave Griffith Avatar answered Oct 20 '22 15:10

Dave Griffith