4 minutes
ADTs: Sum Types and Product Types
It has been a while since I haven’t posted a new article, and in an attempt to break the cycle I’m starting off with something pretty simple.
When listening to people talk of ADTs, you’ll often hear the words: Sum type
and Product type
. Now I have a bad habit of confusing the two so in this article I’ll attempt to make things clearer and I will hopefully never find myself re-re-researching both.
But first, ADT stands for Algebraic Data Type and they’re essentially all about combining existing types to create new ones to better model a specific domain.
A simplistic explanation of domain is “an area of knowledge”, the problem space we are working with. This includes all related entities, their behavior and the laws they obey.
ADTs allow us to model all that, including the relationships between different entities.
Product Types
Product types are created by combining two or more types together with the AND
operator. Here are a couple examples:
Street addresses are comprised of a street number and a street name.
final case class StreetAddress(streetNumber: Int, streetName: String)
A book consists of a title, and author and an ISBN.
final case class Book(title: String, author: String, isbn: Long)
Sum Types
A sum type is a type that combines other types using the OR
operator. Here are a couple examples:
A grade is a pass or fail class is a Sum type, it can either be
Pass
or can beFail
but not both.A boolean can either be
True
or it can beFalse
but not both.A dice roll can only be one out of six possibilities at a time but not more.
In our PostalAddress class, we represented a fullName as a String. But what if we wanted to push this further?
A fullName can be comprised of a firstName and a lastName, but a persons’ name can also include a middleName
So here’s one way of modeling this:
final case class FullName(
firstName: String,
middleName: Option[String],
lastName: String
)
And I guess thats okay… But we’ll have to add None
everywhere a middle name does not exist and when it does, we’ll have to wrap it in a Some
val stephen = FullName("Stephen", None, "Chbosky")
val sagan = FullName("Carl", Some("Edward"), "Sagan")
But what about people with a singe name such as : Aristotle
, Charlemagne
, Fibonacci
etc…
Should we make the firstName optional? can those really be considered their last names? or should be put some cases aside as special cases to be handled separately? If so wouldn’t we end up in a situation where we have so many exceptions of different kind that it becomes unmanageable?
If I’m asking these questions its because they best demonstrate where Sum Types really shine!
A FullName can be modeled as a first and last name OR a first, middle and last name OR a single name etc…
sealed trait FullName
object FullName {
final case class FirstLastName(
firstName: String,
lastName: String
) extends FullName
final case class WithMiddleName(
firstName: String,
middleName: String,
lastName: String)
extends FullName
final case class SingleName(name: String) extends FullName
val stephen: FullName = FirstLastName("Stephen", "Chbosky")
val sagan: FullName = WithMiddleName("Carl", "Edward", "Sagan")
val aristotle: FullName = SingleName("Aristotle")
}
I’m definitely not the best at coming up with class names, but this point aside… All three instances are of the same type: FullName
which is a Sum Type that can either be one of the three defined above.
We’re not restricted to Either Sum or Product Types. Instead we can use both together to better model our domain. Here’s our Book type better represented using the FullName Sum Type:
final case class Book(title: String, author: FullName, isbn: Long)
The last one is known as a Hybrid type or a Sum of Products
type.
If you must remember two things
- ADTs are all about combining existing types to create new ones and better model our domain.
- Sum Types combine types with the OR operator while Product Types do it with the AND operator.