We need to think of our types bottom-up. We need to thoughtfully and purposefully design our types to mirror our business domain. Types should only allow correct values. This of course comes at a cost, and we need to decide consciously where and how are we willing to pay that cost, if we want to.
I start with a TL;DR, just an example of how to create sum types, product types and ADTs. Then I explain an alternative way of looking at types and what does it mean to build from the bottom up. Then you will find in more details how to compose types from existing types. What are the different types of type composition? Finally, we will look at ways to restrict primary data types at compile time.
- TL;DR
- Types as Set of Terms (group of values)
- Building Types From The Bottom-Up
- Type Composition
- Resources
TL;DR
- Create Sum Types
- Create Product Types
- Create ADT
Types as Set of Terms (group of values)
We can define a type as sets of terms, think of a term as a possible value. For an example the type boolean
has two terms true
and false
. Those are the only valid terms for that type. The boolean
type is said to have a finite set of terms. The type char
has finite set of terms, which are the set of unicode characters. natural numbers
on the other hand is an infinite set of terms.Types include only valid terms. So, for the type boolean
the term word
is not valid.
A good question here is, what about the billion dollars mistake? What about null
? Scala unfortunately inherited null
from Java, a mistake that Kotlin managed to avoid. But for our purposes we will ignore the existence of null. In Scala we tend to avoid using null all together and wrap external, potentially nullable, dependencies with Option
.
Building Types From The Bottom-Up
When we usually write code we use the most appropriate data type to represent a value. For an example Int
or String
to represent HTTP response codes. There is a flow in that approach. If we use Int
, we can set the value to 0
which is not a valid value. If we use a String
, it is even worse we can set it to Hello
which again is not valid. A good alternative is to declare a new data type that could be represented by an enum
in some languages. If we only expect two values, either 200
or 404
, then these should be the only two values in our enum
. This eliminated a whole set of potential bugs.
The compiler will not allow us to use an invalid term. Using an appropriate IDE, we will get a wiggly red line informing us that we are using an inappropriate term. Yes, one can test for these sorts of things, but that means, we need to run all our tests, every time we make a change.
Testing is a poor substitute for proof. Using an enum
in the above example turns val result: HttpCode = HttpCode.Ok
into a proof, because the compiler will not let you write val result: HttpCode = "hello"
. These kinds of problems will only occur on system boundaries and will be handled in the proper place.
The best data type is the data type is defined as a set of only correct terms. It is a paradigm shift of sorts. We need to think about our domain or business values as a set of only correct terms. The question is, can we do that? and if we can, how much effort would it take? The answer as with everything in our line of business, it depends! How important is that phone number for you? Is it important enough to define a data type Digit
that has values Zero...Nine
then create an array of eight Digit
s? Or is this an overkill?
Type Composition
There are three ways we can compose types, sum composition, product composition and mixing both in what is called algebraic data types or ADT.
Sum composition:
Let’s define a type WeekendDay
as a sum type, it can be one of two values. Saturday
and Sunday
. We can concisely express that by writing WeekendDay: {Saturday, Sunday}
.
Let’s say we have another data type WorkDay: {Monday, Tuesday, Wednesday, Thursday, Friday
.
1
2
3
4
5
6
7
8
9
10
11
12
object Days{
sealed trait WeekendDay
case object Saturday extends WeekendDay
case object Sunday extends WeekendDay
sealed trait WorkDay
case object Monday extends WorkDay
case object Tuesday extends WorkDay
case object Wednesday extends WorkDay
case object Thursday extends WorkDay
case object Friday extends WorkDay
}
We can now define a new Sum type WeekDay = WeekendDay + WorkDay = {Monday, ..., Sunday}
. In the current version of Scala 2.x
we can’t create a sum type of already existing sum types. As a work around we will create WeekDay = WeekendDay + WorkDay = {WeekendDay: {Saturday, Sunday}, WorkDay: {Monday, Tuesday, Wednesday, Thursday, Friday}
We wil basically create a new sum type that contains two terms, each is a wrapper around one of our existing terms.
1
2
3
4
5
6
7
8
9
10
11
12
sealed trait WeekDay
object WeekDay {
case class WeekendDay(day: Days.WeekendDay) extends WeekDay
case class WorkDay(day: Days.WorkDay) extends WeekDay
}
val day: WeekDay = WeekDay.WorkDay(Days.Friday)
day match {
case WeekDay.WorkDay(Days.Friday) => // do something
case WeekDay.WeekendDay(Days.Saturday) => // do something
}
It is important to know that the number of terms of the new type is the sum of the number of terms in each type.
1
2
3
|WeekendDay| = 2
|WorkDay| = 5
|WeekDay| = |WeekendDay + WorkDay| = |WeekendDay| + |WorkDay| = 2 + 5 = 7
Generally, for any two types A
and B
1
2
3
4
5
6
A: {a1, a2}
B: {b1}
A + B = {Left_a1, Left_a2, Right_b1} // It keeps track of where they come from
|A + B| = |A| + |B| = 3
A + A = {Left_a1, Left_a2, Right_a1, Right_a2}
|A + A| = 4
Two ways:
- Either
- Sealed Trait sum requires a finite number of terms (vs number of values which is ok to be infinite)
Product composition:
Product composition is the type of composition present in most object oriented programming languages. When we declare a class or data structure Person
that has a name of type String
and an age of type Int
, we have composed a new type Person
that is a product of String
and Int
. We need both name and age to define that person. Remember, null
is not allowed.
Let’s formalize this a bit, for type A
that has values a1
and a2
which can be written as A: {a1, a2}
and type B: {b1}
, we can create a new product type A * B = { {a1, b1}, {a2, b1}}
. You can think of that as the cartesian product of the two sets of terms. The size of A
is 2
, which can be written as |A| = 2
and similarly |B| = 1
. The size of the new type is the product of the sizes of A
and B
. |A * B| = 2 * 1 = 2
.
If it makes it easier think of it as a graph where A
represents an axis and B
represents the other. We need the product of A
and B
to find a point on that graph. The product or the point can be found by one value from A
and B
for an example (a1, b1)
.
ADT
Notes
- Using string to represent email/URL, etc. is not correct since it will have a lot of invalid values
- We need to tell the compiler what is valid and not valid, and in that case our code becomes a proof.
Comments powered by Disqus.