Showing posts with label variance. Show all posts
Showing posts with label variance. 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 :)

Wednesday, March 31, 2010

Variant Positions 2

This is a continuation of post: Variant Positions 1

...

My first attempt at the Verified was to make it a mutable object (my Java tourettes kicking in). But it cannot be covariant and mutable. Look at the code to see why:
  1. class Verified[+A <: V,V](assertion : (V) => Booleanprivate var value : A){
  2.     assert(assertion(value))
  3.     
  4.     def a = value
  5. // update is illegal.  See the example below
  6.     def update[ B >: A <: V](a : B) = value = a
  7. }
  8. def notNull(obj : AnyRef) = obj != null
  9. val v = new Verified(notNull, "hi")
  10. /*
  11. Up to this point everything looks ok but the next line
  12. will assign an object to value which is a reference to a String
  13. */
  14. update (new Object())

For Verified to be mutable A must be invariant. If you look at the Mutable collections in Scala they are all invariant.

Here is an interesting example of both invariant and covariant type parameters in a class hierarchy:
  1. scala> class X[+A](val x :A)
  2. defined class X
  3. scala> class Y[A](var a: A) extends X[A](a)
  4. defined class Y
  5. scala> val x: X[Any] = new Y[String]("hi")
  6. x: X[Any] = Y@1732a4df
  7. scala> x.asInstanceOf[Y[String]].a="ho"

This example is perfectly legal because no matter how X[Any] is used no illegal assignment in Y can occur. The interesting thing is that the object can be used in covariant usecases when only X is required. This is now the collections in Scala can work.

Here is a little example of collections invariance and covariance in action. In List the parameter is covariant but in Buffer it is invariant
  1. scala> def printList(l : List[Any]) = print(l mkString " :: ")
  2. printList: (l: List[Any])Unit
  3. scala> val buffer = Buffer(1,2,3)
  4. buffer: scala.collection.mutable.Buffer[Int] = ArrayBuffer(1, 2, 3)
  5. scala> printList(buffer)
  6. 1 :: 2 :: 3
  7. /*
  8. ++ is part of Iterable.  Since Iterable is covariant ++ 
  9. returns a new buffer it does not modify the existing buffer
  10. All mutators are only defined on invariant traits
  11. */
  12. scala> buffer ++ List(4)
  13. res16: scala.collection.mutable.Buffer[Int] = ArrayBuffer(1, 2, 3, 4)
  14. scala> res16 eq buffer
  15. res17: Boolean = false
  16. /*
  17. buffer defines += to add an element to the buffer
  18. so res27 is the same buffer with an extra element
  19. */
  20. scala> buffer += 10
  21. res27: buffer.type = ArrayBuffer(1, 2, 3, 10)
  22. scala> res27 eq buffer
  23. res28: Boolean = true

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.

Wednesday, March 24, 2010

In- and Co- variance of type parameters

In Java most parameterized types are considered to be "invariant". What does that mean? Here is an example to explain:
  1. /*
  2. This is an example of a parameterized class that with an invariant parameter B
  3. In both Scala and Java parameters are invariant by default.
  4. */
  5. scala> class Invariant[B]
  6. defined class Invariant
  7. scala> var x : Invariant[Object] = new Invariant[Object]
  8. x: Invariant[java.lang.Object] = Invariant@2e0c5575
  9. /*
  10. Note: Invariant[String] cannot be assigned to Invariant[Object]
  11.       even though logically it seems like it should be.
  12.       This is the effect of invariance.  Covariant parameters do not have
  13.       this restriction.
  14. */
  15. scala> var x : Invariant[Object] = new Invariant[String]
  16. < console>:6: error: type mismatch;
  17.  found   : Invariant[String]
  18.  required: Invariant[java.lang.Object]
  19.  var x : Invariant[Object] = new Invariant[String]
  20.                              ^
  21. scala> class Sub[A] extends Invariant[A]   
  22. defined class Sub
  23. /*
  24. Since Sub is a subclass of Invariant it can be assigned
  25. (but not Sub[String])
  26. */
  27. scala> val x : Invariant[Object] = new Sub[Object]
  28. x: Invariant[java.lang.Object] = Sub@26ced1a8

Assignment compatibility has multiple dimensions: the object type and the types of the parameters. Unlike object type the compatibility of the type-parameters can be covariant, contravariant and invariant. Java has invariant parameters and that is demonstrated by the previous example. Covariant parameters allow subclassing. Contravariant parameters need their own topic.
  1. // The '+' indicates the parameter is covariant
  2. scala> class Covariant[+B]
  3. defined class Covariant
  4. scala> var x : Covariant[Object] = new Covariant[Object]
  5. x: Covariant[java.lang.Object] = Covariant@315cb235
  6. // Now this is legal
  7. scala> var x : Covariant[Object] = new Covariant[String]
  8. x: Covariant[java.lang.Object] = Covariant@26e2e276
  9. /*
  10. Warning: The following is not legal because 
  11.          you cannot supply an invariant parameter 
  12.          with a covariant value.
  13. */
  14. scala> class Sub[+A] extends Invariant[A]
  15. < console>:7: error: covariant type A occurs in invariant position in type [+A]Invariant[A] with ScalaObject{def this(): Sub[A]} of class Sub
  16.        class Sub[+A] extends Invariant[A]
  17.              ^
  18. scala> class Sub[+A] extends Covariant[A]
  19. defined class Sub
  20. scala> class Sub[A] extends Covariant[A] 
  21. defined class Sub

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)