Tuesday, June 9, 2015

Recently I learned... Covariance and mutability

Last week I made my data generation and automated testing framework more generic.
Instead of having an engine that processed a Graph of Nodes of MySpecificData, I created an abstract engine to process over a Graph of Nodes of GenericData.

So I had an abstract method such as process(graphToProcess: Graph[Node[GenericData]])
And I had an override method such as process(graphToProcess: Graph[Node[MySpecificData]])

The compiler won't let me do this because the methods don't have the same signature, even though MySpecificData "is a" GenericData.

If you've used Java, you may tried to cast an Array<Int> to an Array<Object>, and for Arrays in Java, it allows this. The problem is you can then insert a String into this Array and then when you try and read the String from the Array<Object>, which is really an Array<Int>, you'll get a ClassCastException.

Scala will prevent this with a compilation error. Because the contents of Array can change (it's mutable), Scala forbids you from casting it to a more generic type, because it can't guarantee it type-safe at compile-time.

To say "WrapperType[SpecificWrappedType] is a WrapperType[GenericWrappedType]" requires the generic type parameter of WrapperType to be covariant in the class definition of WrapperType. If WrapperType has any mutability in it, the compiler will prevent you from declaring the generic wrapped type as covariant.

It's not always desirable to make everything immutable, unless you're a diehard Haskell geek. Sometimes in production environments, we have deeply nested data structures for which creating a new copy every time we need to do something would be an unacceptable performance hit. That is the case with my Graph[Node[NodeData]]] -- I really just need it to be mutable.

One possibility is to define a secondary type as a subtype of the original generic type, and make that type covariant, and use it in your class, but it's not very clean and makes for strange code (and I couldn't get this solution to even compile in every single situation).

The easiest thing to do is to use an annotation that will tell the Scala compiler to ignore the error due to a generic type being used in the wrong context (e.g., a covariant type used in a mutable setting).

To define a generic type as mutable, use the plus sign prefix -- e.g., "class Blah[+T <: SomeAbstraction]" defines Blah wrapped over generic type T, which is covariant and implements another type called SomeAbstraction.

Try and use it, and if you get a compiler warning related to using a covariant type in an invariant or contravariant position, just add this import:

import scala.annotation.unchecked.{uncheckedVariance => uV}

And then just put @uV after any reference to the type (except for the definition).

E.g., a method definition might pass in parameters using this type:

def someMethod(myParam1: Seq[T @uV], myParam2: (T @uV))

(If the type is by itself, wrap parentheses around it and the annotation.

And that's how you can fool the Scala compiler into allowing you to write covariant, mutable code.

The type system is there for a reason, and strong typing can be a wonderful thing. In this case, there's simply no way to prove that our code is safe at compile-time and we have to relax the compiler's default restrictions. It would be easier if there were a compiler flag or a "begin/end" kind of annotation instead of annotating every offending usage, but at least there's a way around it.

Just be smart and don't write code that isn't typesafe. Just don't try to stuff a String into an Array<Int> and you should be fine. And test your code so that that ClassCastException will occur if you happen to write bad code.

No comments:

Post a Comment