In this short article I will demonstrate the generalized type constraints in Scala — what they are, why we need them and how they are used.

To start, let me show you two Scala puzzles.

Puzzle number one:

def pair[S](s1: S, s2: S): (S, S) = (s1, s2)

What do you think happens if we pass two values of different type to this method? Let’s see:

scala> pair(“foo”, 1)

res0: (Any, Any) = (foo,1)

What happened is that compiler said “OK, first value is a String and second value is an Int, but that’s not really going to work out because I need those two arguments to be of the same type. I will now try to find their nearest common supertype. Here it is — it’s Any. Cool, each of these arguments is also an Any, so I will consider them to be of type Any in order to satisfy the type constraint”.

By the way, if we had a bound on type S, for example:

def pairVals[S <: AnyVal](s1: S, s2: S) = (s1, s2)

Then invoking the method with String and Int would give us a compile error, because one of the arguments is not a subtype of AnyVal (remember that String is a subtype of AnyRef, not AnyVal). Compiler says “nearest common supertype of the arguments you gave me is Any, but that violates the given type constraint”:

scala> pairVals(“foo”, 4)

<console>:12: error: inferred type arguments [Any] do not conform to method pairVals’s type parameter bounds [S <: AnyVal]

Back to pair() method. Here’s a question — what if we wanted to say “method pair() should take two arguments of the same type”? We saw that pair() easily allows us to pass in a String and an Int; compiler will simply deduce that you passed values of type Any. What if we want to allow only invocations where both arguments share the exact same type (e.g. two Ints or two Strings, but not one Int and one String)?

Before I answer that, here’s another puzzle:

Puzzle number two:

def advPair[S <: T, T](s: S, t: T): (S, T) = (s, t)

This is the same principle in a bit different situation. What do you think happens when we invoke this method with two different arguments?

scala> advPair(“foo”, 1)

res0: (String, Any) = (foo,1)

You might at first glance conclude that advPair() takes two arguments, first being the subtype of second, and for that reason invoking it with String and Int will fail (because String is not a subtype of Int). Again, you would be wrong. Just like in the first case, compiler will try to work out the mess and see if it can satisfy the type constraints by upcasting your values. (BTW don’t mind me using the term “upcasting” — I know it sounds a bit ugly, but you and I both know that there’s nothing ugly going on. Int *is* an Any, just like everything else in Scala).

So yes, we passed in a String and an Int, but Int is also an Any. When we look at things from that perspective, everything works out — we passed a String and an Any and the condition of first argument being a subtype of second is fulfilled.

So how can we put constraints on “original” types, without the compiler trying to be generous and upcasting our arguments until the type constraints are satisfied?

Answer(s):

By using generalized type constraints.

First puzzle solved:

def pair[S, T](s: S, t: T)(implicit ev: S =:= T): (S, T) = (s, t)

Second puzzle solved:

def advPair[S, T](s: S, t: T)(implicit ev: S <:< T):(S, T) = (s, t)

See those implicit parameters? They are the solution to our predicament. You can look at them as types: statement S =:= T is of same nature as, for example, Map[S, T]. It’s just that generalized type constraint (“GTC”) is infixed instead of prefixed.

So what happens when you have a GTC? Well, compiler will do its inferring magic as if the GTC weren’t there. Once everything is resolved, it will check if GTC is satisfied. See how in the pair() example we no longer have two arguments of type S, but arguments of different types, and return type is also changed from (S, S) to (S, T)? This way compiler will not need to do any upcasting magic — it will simply pair up the given arguments into a tuple. However, before proceeding with the method body, it will check if the GTC is satisfied; that is, whether types S and T are, in fact, the same type.

scala> pair(“foo”, 1)

<console>:12: error: Cannot prove that String =:= Int.

pair(“w”, 1)

^ scala> pair(“foo”, “foo”)

res0: (String, String) = (w,a)

Second method also went through some changes in the type constraints — we removed the [S <: T] part. Why? Again, to prevent the compiler from doing any upcasting magic. We saw how having [S <: T] caused the compiler to upcast the Int into Any in order to conform to the type constraint, right? Well, now compiler will happily realize that there are no type constraints, which means that S and T can be String and Int without any problems, so it will proclaim its final decision: S is String, T is Int. But then GTC kicks in and tells the compiler “OK, now that you’ve resolved S and T to concrete types, here are my demands”. And if S is not a subtype of T, invocation will fail.

scala> advPair(“foo”, 1)

<console>:12: error: Cannot prove that String <:< Int.

advPair(“foo”, 1)

^ scala> val any: Any = "any"

any: Any = any scala> advPair("foo", any)

res0: (String, Any) = (foo,any)

Conclusion

So here’s the summary:

Compiler will try to conform to type bounds by upcasting the types as needed, trying desperately to find a combination that works. If the process fails, compiler will cry. If it succeeds, compiler will assign each generic type parameter a concrete type (e.g. S and T will become String and Int).

Then, when the types are resolved, compiler will see that an implicit GTC is needed and it will provide it. Note that when you are writing a method that needs the GTC, you just need to declare it as an implicit parameter and that’s it. Compiler will provide the needed GTC at compile time and plug in the resolved types, issuing an error message in case GTC is not satisfied.

Fun with types never ends in Scala. :) As usual, contact me on sinisalouc@gmail.com with feedback or find me on Twitter.