Search code examples
scalacontravariance

Compound types, contra variance, etc. in Scala


I'm working on some fairly generic code (yet another CSV file reader) and I've run into some trouble with contravariance. I've stripped the code down to something that I think demonstrates the problem here (I copied this from my Scala worksheet):

def f1(x: Int)(s: String) = Try(x+s.toInt)
val f1a: Int=>String=>Try[Int] = f1 _
val r1 = f1a(3)("2")
def f2(x: String)(s: String) = Try(x.toInt+s.toInt)
val f2a: String=>String=>Try[Int] = f2 _
val r2 = f2a("3")("2")
val fs: Seq[String with Int=>String=>Try[Int]] = Seq(f1a, f2a)
val xs: Seq[Any] = Seq(3,"3")
val r3 = for {(f,x) <- fs zip xs} yield f(x)("2")

Last line edited to avoid irrelevant issues after @Lee and @badcook commented. I've tried to simplify things for the reader by providing the types for the vals although of course that's not required by the compiler. Each of the expressions r1 and r2 evaluates to Success(5) as expected. The expression for r3 does not compile. The error is:

type mismatch;  found   : x.type (with underlying type Any)  required: String with Int

The problem essentially lies with the type of x, the parameter of f(x) in the for-comprehension. This is a contravariant position (the argument to a function) but xs and fs are covariant (the element type of a Seq). Thus x has type Any, but f requires a type String with Int.

This is an uninhabited compound type and I have not been able to find a clean way to solve this problem. I can cast the values f1a and f2a to Any=>Int=>String but that's not the way we like to do things in Scala!


Solution

  • Based on what I assume you are trying to do, the Scala compiler is saving you from one mistake. Your for comprehension is a Cartesian product of your elements and your functions. This means your function expecting an Int is going to get called once with a String and your function expecting a String is going to get called once with an Int.

    What you probably want is to zip your two Seqs together and then map over the resulting pairs, applying each function to its paired element. Unfortunately, as you've noted, contravariance will prevent this from typechecking. This is fundamentally because all elements of a Seq must be the same type, whereas you want to preserve the fact that your elements have different types and that those types match the different types of your functions.

    In short, if you want to go down this path, you're looking for heterogeneous lists, also known as HLists, rather than ordinary Scala collections. shapeless has an implementation of this and provides map and zip methods to go along with it. There's a bit of a learning curve there (I would recommend getting familiar with higher-rank types and how shapeless implements them) and HLists are a bit unwieldy to use compared to ordinary collections, but this will solve this particular kind of problem.