Work In Progress

Work on this post is underway... It will be done "soon..."


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

  • 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 Digits? 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.

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.

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.

|WeekendDay| = 2
|WorkDay| = 5
|WeekDay| = |WeekendDay + WorkDay| = |WeekendDay| + |WorkDay| = 2 + 5 = 7 

Generally, for any two types A and B

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.

Resources

  1. Functional Scala by John A. De Goes (Toronto Edition)
  2. Kinds of types in Scala, part 1: types, what are they?
  3. refined: simple refinement types for Scala