11 minutes read

In programs that contain different data and their transformations, you can often find special types that can have only a limited set of values. Here is a simple example: val condition: Boolean can only be false or true. When we write if condition then ??? else ??? we are sure that we don't need to add a separate else if to check that there is something else in this value. Such types are called algebraic data types.

Sum types

Let's imagine that we need to write a function that will access the user's calendar and use the event ID to find out the day of the week on which the event will take place, and let's figure out how ADT can help simplify this task.

def whenEvent(eventId: String): String

Now our function returns String. We can encode every day of the week with the strings "Sunday", "Monday", but then the user of the function will have to check the correctness of these strings. We need a type that will have strictly 7 variants:

// DayOfWeek = Monday or Tuesday or Wednesday or Thursday or Friday or Saturday or Sunday

This type is called a Sum. We can represent it using objects:

trait DayOfWeek

object DayOfWeek:
  case object Monday    extends DayOfWeek
  case object Tuesday   extends DayOfWeek
  case object Wednesday extends DayOfWeek
  case object Thursday  extends DayOfWeek
  case object Friday    extends DayOfWeek
  case object Saturday  extends DayOfWeek
  case object Sunday    extends DayOfWeek

// DayOfWeek = Monday + Tuesday + Wednesday + Thursday + Friday + Saturday + Sunday

Sealed

Since we know that there shouldn't be any other DayOfWeek in our program anymore, we need to limit the possibility of inheritance. To do this, Scala has the sealed keyword, which can be placed before a trait or class so that inheritance is available only within a single file:

sealed trait DayOfWeek

Now the return type of the function guarantees strictly 7 possible values:

def whenEvent(eventId: String): DayOfWeek

We can match this value, and the compiler will make sure that we take all the options into account:

import DayOfWeek.*

whenEvent("001") match
  case Monday    => "Hmm..."
  case Tuesday   => "Hmm..."
  case Wednesday => "Hmm..."
  case Thursday  => "Hmm..."
  case Friday    => "Yeah, Friday!!!"
  case Saturday  => "Hmm..."

  // the compiler will warn: 
  //   match may not be exhaustive. It would fail on pattern case: Sunday

Enum

But look at the structure of DayOfWeek again. So many words to define a simple listing of days of the week. Scala 3 solves this problem! We can use the keyword enum and list all options after the word case:

enum DayOfWeek:
  case Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday

Not bad, right? If we need to add data to the structure, we just extend the enumeration a bit with extends. For example, let's add to each day its display code:

enum DayOfWeek(val code: String):
  case Monday    extends DayOfWeek("M")
  case Tuesday   extends DayOfWeek("T")
  case Wednesday extends DayOfWeek("W")
  case Thursday  extends DayOfWeek("R")
  case Friday    extends DayOfWeek("F")
  case Saturday  extends DayOfWeek("S")
  case Sunday    extends DayOfWeek("U")

Product types

Let's go back to our function that accesses an external system with a calendar, which may fail:

case class CalendarError(code: Int, message: String)

How many values can this error have? Based on the fields above, the error may be a combination of all Int variants with all String variants:

// CalendarError = Int * String

This type is called Product.

Now we can combine a successful scenario and an error scenario into another enum that will visualize all the work of our function:

enum Result:
  case CalendarError(code: Int, message: String)
  case Success(day: DayOfWeek)

// Result = CalendarError + Success
// Result = CalendarError + DayOfWeek
// Result = Int * String + Monday + Tuesday + Wednesday + Thursday + Friday + Saturday + Sunday

It is worth noting that when we add parameters to the enum variants, they start working like a case class.

Let's rewrite the function type:

def whenEvent(eventId: String): Result

Now we can say for sure that the function returns either an error, which can consist of any combination of code and a message, or a successful result, which includes only 7 variants of the days of the week.

import Result.*
import DayOfWeek.*

whenEvent("001") match
  case CalendarError(code, message) =>
    s"Try again later, an error has occurred with code=$code and message=$message"
  case Success(day) =>
    day match
      case Monday => "Hmm..."
      ...
      ...

Conclusion

In this topic, you learned what ADT are. We briefly discussed Sum and Product types and saw how you can use sealed and enum keywords in Scala to make ADTs. Now you know how many different variants a type can contain, and you also found out that the Scala compiler does type checks and helps to eliminate errors. Time to practice with tasks!

3 learners liked this piece of theory. 0 didn't like it. What about you?
Report a typo