When working with Big Data, sometimes it’s useful to remember that powerful products wouldn’t work properly without the tools that build them. It’s possible to start programming in Scala with a few case classes and a bunch of for-comprehensions, but those are only little scratches in a huge ice surface like Scala is. It may not be enough to make your code clean and comprehensible. I’ve been developing with this programming language for almost 4 years, and every day I discover a new feature that surprises me. That acknowledgement, in the end, is the main reason to keep digging deeper into Scala.
For instance, working with generics can be sometimes really messy, especially when we want to play a little bit with inheritance (which turns out to be very common).
It’s very usual to clash with variance issues in that case, but not as usual as getting a clear idea about what it really implies.
Just to set the tone, we will propose a simple example.
Invariance
Let’s suppose we’re defining the behavior of some servers’ connection handler…
case class ConnectionHandler[C](connections: List[C])
…and connection’s definition (we could have two types of connections: user and system connections):
trait Connection class UserConnection extends Connection class SystemConnection extends Connection
If we want to list all connections given a connection handler, we probably need a logger to print them all and a function for using this logger for each connection:
case class FunctionalLogger(traces: Seq[String]= Seq()) { def print(trace: String): FunctionalLogger = this.copy(traces :+ trace) }
Why this freak logger? It’s quite common to invoke ‘println’ and similar methods in Java, and it can be done as well in Scala, but functional purists don’t support this idea.
Let’s focus for a moment on the main ideas of functional programming:
- a function may have one (or more) input parameters and an only parameter type
- same input will always generate the same output.
- instances shall not be modified: state mutation is provided by a new instance of same type.
Having this in mind, what we have provided here is a pretty functional logger that doesn’t perform IO operations that would break the pure nature of functions.
If we continue implementing our method…
def listConnections( handler: ConnectionHandler[Connection])( logger: FunctionalLogger): FunctionalLogger = { (logger /: handler.connections)( (logger,connection) => logger.print(connection.toString)) }
…what we have is a method that performs a foldLeft
over the logger, printing on it all connections that belong to given handler.
Easy peasy, but what happens if we try to list the connections of the following connection handler?
val handler: ConnectionHandler[UserConnection] = ConnectionHandler(List(new UserConnection)) listConnections(handler)(FunctionalLogger())
The compiler will complain with the following “wookie-incomprehensible” error:
<console>:19: error: type mismatch; found : ConnectionHandler[UserConnection] required: ConnectionHandler[Connection] Note: UserConnection <: Connection, but class ConnectionHandler is invariant in type C. You may wish to define C as +C instead. (SLS 4.5)
If we translate this into human, it means that UserConnection
is a subtype of Connection
, but we cannot make this condition extensive to connection handler (ConnectionHandler[UserConnection]<:ConnectionHandler[Connection]
). How do we fix this?
Covariance
In order to make this inheritance relation flexible, we have to slightly modify the signature of ConnectionHandler
:
case class ConnectionHandler[+C](connections: List[C])
Note the plus sign: it means that any ConnectionHandler[T]
will be a subtype of ConnectionHandler[C]
if T
is a subtype of C
.
Now ConnectionHandler
is covariant in C
and the code that wasn’t working before should work now. We can prove it by the following simple line
implicitly[<:<[ConnectionHandler[UserConnection],ConnectionHandler[Connection]]]
which tries to find an implicit evidence of first type is a subtype of the second.
If we tried this with unrelated types like Int
and String
….
implicitly[<:<[ConnectionHandler[String],ConnectionHandler[Int]]]
we would get a compilation error like:
Error: Cannot prove thatConnectionHandler[String] <:<ConnectionHandler[Int]. implicitly[<:<[ConnectionHandler[String],ConnectionHandler[Int]]] ^
Contravariance
On the other hand, we could define a contravariant class C[_]
in its parameter type using the minus sign, just in front of parameter type.
This means, given two types T
and V
, if T
is a supertype of V
, then C[T]
will be subtype of C[V]
.
Confusing? The real question here is, who in his right mind would need this? Maybe the following example is self-explanatory.
Without going too far…Scala’s functions are defined like:
trait Function1[-Parameter,+Result] extends AnyRef
Not so inspiring so far, but think about the following class hierarchy and the following higher-order function:
trait Vehicle { def wheels: Int } class Car extends Vehicle { val wheels = 4 } class Truck extends Car { override val wheels = 6 } val myFunction: Car => Any = ???
Remember that myFunction
signature is syntactic sugar for
val myFunction: Function1[Car,Any] = ???
So if you had a Truck
, that is a subtype of Car
, you couldn’t define your function as follows:
val myFunction: Function1[Car,Any] = (t: Truck) => println(t.wheels)
because it is contravariant in Parameter
, Function1
expects a supertype of Car
, that’s why you could define your function using Vehicle
instead:
val myFunction: Function1[Car,Any] = (t: Vehicle) => println(t.wheels)
And also don’t forget that covariance in type Result
is being satisfied due to Unit
(the result type of println
) is a subtype of Any
. A little bit tricky in the end, but essentially, that was the main idea 🙂
“May the functional programming be with you”…