Showing posts with label contravariance. Show all posts
Showing posts with label contravariance. Show all posts

Tuesday, April 6, 2010

Variant Positions 3

The last few topics all discussed variance in its different forms. The following is a cheat sheet of the where the different variances exist within a class.

See Variant positions 1 for a discussion on one position in a class that is a contravariant position.

The example is:
  1. scala> class Output[+A] {def write(a : A) = () /*do write*/ }

In this example A in the method write is an contravariant position. Which means the previous definition is not legal because A is defined as Covariant in the class definition. In a class there are several positions with different variance characteristics. Here's the example from Programming in Scala:
  1. abstract class Cat[-T, +U] { 
  2.     def meow[W<sup>-</sup>](volume: T-, listener: Cat[U<sup>+</sup>, T<sup>-</sup>]-): Cat[Cat[U<sup>+</sup>, T<sup>-</sup>]-, U+]+
  3. }

If you remove the superscript + and - the above example actually compiles. The + and - indicate if the position is a covariant or contravariant position. Its not critical to memorize the positions (in my opinion). Just look it up as needed. The rule of thumb is that each nested position is inverted (flipped) value of it enclosing position.

That is all I will say about that :)

Tuesday, March 30, 2010

Variant Positions 1

An additional topic on variance to finish up the major points on the topic. The previous two posts: contain required information for following this post.

In-, co- and contra- variance are the three types of variance expressible in Scala. I showed how this affects assignments and arguments being pass to methods in the last two topics. This looks at how the different types of variance influences how classes can be defined. In the last post we saw how the compiler complained about a method definition in a covariant class because the compiler recognized that such a definition was inherently dangerous and must be prohibited. The example was:
  1. scala> class Output[+A] {def write(a : A) = () /*do write*/ }
  2. < console>:5: error: covariant type A occurs in contravariant position in type A of value a
  3.        class Output[+A] {def write(a : A) = () /*do write*/ }
  4.                                    ^

For an class like Output it does not make sense to have A be covariant so we changed A to be contravariant. However suppose we have a collection type class.
  1. class Verified[+A] (assertion : (A) => Boolean, value : A){
  2.     assert(assertion(value))
  3.     
  4.     def a = value
  5.     def a_=(a : A) = new Verified(assertion, a)
  6. }

The previous definition is not legal because value and a in the parameter of a_= "occur in a contravariant position." What to do? Making A contravariant isn't an option:
  1. class Verified[+A <: V,V](assertion : (V) => Booleanval value : A){
  2.     assert(assertion(value))
  3. /*
  4. this is the key.  Restrict possible types of
  5. A Since B is a super (or equal) type of A
  6. */
  7.     def update[ B >: A <: V](a : B) = new Verified(assertion, a)
  8. }
  9. // example useage
  10. scala> def notNull(obj : AnyRef) = obj != null
  11. notNull: (obj: AnyRef)Boolean
  12. scala> val v = new Verified(notNull, "hi")
  13. v: Verified[java.lang.String,AnyRef] = Verified@307b37df
  14. scala> val newV = v update (new Object())
  15. newV: Verified[java.lang.Object,AnyRef] = Verified@36f72f09
  16. // 3 is not legal because the type parameter 'V' is AnyRef.  Int is a subclass of Any NOT AnyRef
  17. scala> val newV = v update (3)           
  18. < console>:8: error: inferred type arguments [Any] do not conform to method update's type parameter bounds [B >: java.lang.String <: AnyRef]
  19.        val newV = v update (3)
  20.                   ^

Saturday, March 27, 2010

Contravariance

Continuing on with variance and type parameters, this topic will discuss contravariance. See the post In- and Co- variance of type parameters for the intro material required for this topic.

Covariant parameters allow for an additional dimension of type compatibility:
  1. val l : List[Object] = List("this is legal")

Contravariance provides the opposite:
  1. // If the type parameter of list was contravariant this would be legal:
  2. val l : List[String] = List(new Object())

As covariance is indicated by a '+' before the type contravariance is indicated by a '-'
  1. scala> class X[-A]
  2. defined class X
  3. scala> val l : X[String] = new X[Object]
  4. l: X[String] = X@66201d6d

I can almost hear the "cool... but why?". Following the lead in the Programming In Scala book. Consider OutputStream first and a method in a Collection second. (The following code is illegal but consider it)
  1. class Output[+A] {def write(a : A) = () /*do write*/ }
  2. def writeObject(out : Output[Object]) = out.write("hello")
  3. /*
  4. Uh oh you this only is for outputting lists not Objects 
  5. (certainly not the String that is actually written)
  6. Runtime error for sure!
  7. */
  8. writeObject(new Output[List[String]])

The previous example (if it would compile) would explode because an Output that can only write lists is passed to the method. In the example a String is written to the Output object. The Output[List[String]] cannot handle that.

Fortunately the compiler sees the definition of the class and recognizes this is an error waiting to happen and makes it illegal:
  1. scala> class Output[+A] {def write(a : A) = () /*do write*/ }
  2. < console>:5: error: covariant type A occurs in contravariant position in type A of value a
  3.        class Output[+A] {def write(a : A) = () /*do write*/ }
  4.                                    ^

Consider the implications of making A contravariant?
  1. // The definition of object is now legal
  2. class Output[-A] {def write(a : A) = () /*do write*/ }
  3. // this is now a safe method definition since the parameter of Output must be a Object or a super class
  4. def writeObject(out : Output[Object]) = out.write("hello")
  5. // Now this is illegal as it should be
  6. scala> writeObject(new Output[List[String]])
  7. < console>:8: error: type mismatch;
  8.  found   : Output[List[String]]
  9.  required: Output[java.lang.Object]
  10.        writeObject(new Output[List[String]])
  11.        
  12. // this is legal... 
  13. scala> writeObject(new Output[Any])

In this example Output[Any] can be passed to the method. This makes sense. If the Output object knows how to write Any oject then it knows how to write an Object; its all good.

Friday, January 29, 2010

Overcoming Type Erasure in Matching 2 (Variance)

A commenter (Brian Howard) on the post Overcoming Type Erasure in Matching 1 made a very good point:

Is there a way to deal with some type arguments being contravariant? Try the following:

class A

class B extends A

val AAFunction = new Def[Function1[A,A]]

((a:A) => a) match {case AAFunction(f) => Some(f(new A)); case _ => None} // this is OK

((a:A) => new B) match {case AAFunction(f) => Some(f(new A)); case _ => None} // this is OK

((b:B) => b) match {case AAFunction(f) => Some(f(new A)); case _ => None} // gives a ClassCastException, since new A is not a B

There is a way to do this, however the information is not captured in the Manifest. A manifest is not designed to do full reflection it is by design very light and has little impact on performance. So to provide the functionality requested by Brian one needs to add that information to the Extractor Definition. Have have a possible solution below.
  1. scala> class A
  2. defined class A
  3. scala> class B extends A
  4. defined class B
  5. scala> object Variance extends Enumeration {
  6.      |     val Co, Contra, No = Value
  7.      | }
  8. defined module Variance
  9. scala> import Variance._
  10. import Variance._
  11. scala> class Def[C](variance:Variance.Value*)(implicit desired : Manifest[C]) {
  12.      |     def unapply[X](c : X)(implicit m : Manifest[X]) : Option[C] = {
  13.      |         val typeArgsTriplet = desired.typeArguments.zip(m.typeArguments).
  14.      |                                                     zipWithIndex
  15.      |         def sameArgs = typeArgsTriplet forall {
  16.      |             case ((desired,actual),index) if(getVariance(index) == Contra) => 
  17.      |                 desired <:< actual 
  18.      |             case ((desired,actual),index) if(getVariance(index) == No) => 
  19.      |                 desired == actual 
  20.      |             case ((desired,actual),index) => 
  21.      |                 desired >:> actual
  22.      |         }
  23.      |         
  24.      |         val isAssignable = desired.erasure.isAssignableFrom(m.erasure) || (desired >:> m)
  25.      |         if (isAssignable && sameArgs) Some(c.asInstanceOf[C])
  26.      |         else None
  27.      |     }
  28.      |     def getVariance(i:Int) = if(variance.length > i) variance(i) else No
  29.      | }
  30. defined class Def
  31. scala> val AAFunction = new Def[Function1[A,A]]
  32. AAFunction: Def[(A) => A] = Def@64a65760
  33. scala> ((a:A) => a) match {case AAFunction(f) => Some(f(new A)); case _ => None}
  34. res0: Option[A] = Some(A@1bd4f279)
  35. scala> ((a:A) => new B) match {case AAFunction(f) => Some(f(new A)); case _ => None}
  36. res1: Option[A] = None
  37. scala> ((b:B) => b) match {case AAFunction(f) => Some(f(new A)); case _ => None}
  38. res2: Option[A] = None
  39. scala> val BAFunction = new Def[Function1[B,A]](Contra,Co)
  40. BAFunction: Def[(B) => A] = Def@26114629
  41. scala> ((b:A) => b) match {case BAFunction(f) => Some(f(new B)); case _ => None}
  42. res3: Option[A] = Some(B@15dcc3ca)