1. Introduction

Case classes and case objects are some of the most widely used features of the Scala language. They automatically implement many useful methods which makes things easier for developers.

We extensively use case classes and case objects to define ADTs. However, in some cases, we might run into a few strange type inference issues when using them, especially when working with Scala 2.x. In this tutorial, we’ll look at some of the most common reasons for this and how to solve them.

2. Type Inference Problem

Let’s define a simple ADT to model the console colors. We can use case objects in this case:

trait Color
object Color {
  case object Red extends Color
  case object Green extends Color

Now, we can define a simple usage of this ADT:

val consoleColor = if (isError) Color.Red else Color.Green

Looking at the above code, we can see that the variable consoleColor should have the type as Color since both Red and Green are its sub-types. However, when we inspect the type inferred by the Scala (2.x) compiler, we can see a very long signature:

Product with Serializable with Color

The reason for this issue is that the compiler tries to identify the most precise supertype of Red and Green. Since Red and Green are case objects, they automatically extend two traits Product and Serializable. However, since Color is a trait, it doesn’t extend Product and Serializable traits.

Because of this, when the compiler looks at the type of consoleColor, it doesn’t consider the type as just Color and adds Product and Serializable also along with it.

This inference will cause issues with Variances and Generics. Let’s look at it with a sample code:

case class Data[C <: Color](value: String, color: C)
def showColor(data: Data[Color]) = {
    println("The color is "+data.color)

Now, let’s invoke the method showColor():

val consoleColor: Product with Serializable with Color = Color.Red 
val data = Data("This is coloured text", consoleColor) 

The above code will result in a compilation error with the following message:

[error] found : Data[Product with Serializable with Color]
[error] required: Data[com.baeldung.scala.productserializable.Color]

3. Solution

There are two ways to solve this issue when using Scala 2.x.  We’ll see them below.

3.1. Explicitly Provide the Type Color

Instead of letting the compiler infer the type, we can explicitly provide the type as just Color:

val consoleColor: Color = if (isError) Color.Red else Color.Green

3.2. Extend ADT Trait With Product and Serializable

Another approach to solve this issue is by extending the super trait with Product and Serializable traits in the ADT:

trait Color extends Product with Serializable
object Color {
  case object Red extends Color
  case object Green extends Color

Now, we can use the same code as before to decide the color:

val consoleColor = if (isError) Color.Red else Color.Green

This time, the compiler will infer the type as Color. Since all the three types involved extend Product and Serializable, the compiler ignores them from inference and uses the most specific supertype, which is Color.

As a best practice, this approach is generally preferred while building ADTs as it helps to make use of the compiler inference in the best way.

4. Difference in Scala 3

Since Scala 3, we don’t need to explicitly extend the ADT traits with Product and Serializable. Scala 3 compiler is able to infer the correct type even without it.

This is solved by using a new type called Transparent traits. The compiler ignores the transparent traits while inferring the types. In Scala 3, both Product and Serializable traits are transparent traits and hence won’t  affect the type inference.

5. Conclusion

In this short article, we looked at type inference issues with ADTs and how they can be solved.

As always, the sample code used here is available over on GitHub.

Comments are closed on this article!