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 + SundaySealed
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: SundayEnum
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!