In this article I will try to show how Typed Functional Programming can be used to represent a domain. I would run through a step by step example where different domains are modelled with types and subtypes and functions are used to transform one domain to the other.
Let’s consider the process to make a cake that is described in a recipe. The first thing is the list of ingredients.
sealed trait Ingredient case object Flour extends Ingredient case object BakingPowder extends Ingredient case object Water extends Ingredient
This list is written in the recipe but we are not sure that we have all the ingredients in our kitchen. To express this situation we create another type.
sealed trait NeededIngredient {
val ingredient: Ingredient
}
case class MissingIngredient(ingredient: Ingredient)
extends NeededIngredient
case class PresentIngredient(ingredient: Ingredient)
extends NeededIngredient
With these two types we can define the first operation needed to start the cake.
def getIngredients(ingredient: Ingredient*): List[NeededIngredient]
The result of this function is the list of needed ingredients where can be present or missing. We could have used the Option type from the Standard library but it wouldn’t have had the same expressivity. In this case is clear we would like to know which ingredient is missing while None would have hidden this information.
The next part of the recipe is mixing the list of needed ingredients.
def checkAndMix(ingredients: List[NeededIngredient[Ingredient]]):
Either[MissingIngredients, Mixture]
The checkAndMix function result can be either a mixture or a list of missing ingredients.
case class Mixture(ingredients: List[Ingredient]) case class MissingIngredients(ingredients: List[Ingredient])
We could’ve have chosen another way to represent the result of the function. Instead of Either we could have created another type.
sealed trait Mixture
case class FullMixture(ingredients: List[Ingredient])
case class PartalMixture(present: List[Ingredient],
missing: List[Ingredient])
This solution can look correct and even more elegant than the first one but it is not valid for domain: if you don’t have all the ingredients for a cake you don’t start mixing, it would be a waste!
Either is actually the right solution because represent a disjunction of domains. A disjunction means that two completely differents domains have only one thing in common: they are both valid result of the function.
The last thing to do with the mixture will be baking.
def bake(mixture: Mixture): Cake
As you can see this function takes only the mixture as input and, from the domain point of view, is the right definition. In fact what we want to express is that, after we successfully mixed the ingredients, we can bake the mixture. The Either type from the standard library offers a solution that is perfect for this kind of situations: map on right.
val neededIngredients = getIngredients(Flour, Water, BakingPowder) val possibleMixture = checkAndMix(neededIngredients) val possibleCake = possibleMixture.right.map(bake)
The function is applied only if the result is correct (if it is right) and the left value remains as is.
Another possible implementation of the bake function could have possible baking issues.
def bake(mixture: Mixture): Either[BakingIssue, Cake]
Once again this solution can be applied only to the a mixture but the result has another domain disjunction.
val possibleCake1: Either[MissingIngredients, Cake] // or val possibleCake2: Either[MissingIngredients, Either[BakingIssue, Cake]]
If we compare the final result type of the two implementations we can see that the type is more complex and not so easy to read. Instead, it would be nicer something that clearly shows the final result, the cake, and the possible problems that we can have during the preparation.
val idealCake: Either[OneOfTwo[MissingIngredients, BakingIssue], Cake]
This ideal solution type would combine the positive and negative transformation paths where the issues are listed in a single type but only one can be present.
sealed trait OneOfTwo[+A, +B]
case class First[A](a: A) extends OneOfTwo[A, Nothing]
case class Second[B](b: B) extends OneOfTwo[Nothing, B]
// convert
def convert[A, B, C](either: Either[A, Either[B, C]]):
Either[OneOfTwo[A, B], C] = either.fold(
a => Left(First(a)), {
case Left(b) => Left(Second(b))
case Right(c) => Right(c)
}
)
This one is a possible simple implementation that converts the nested Either in a flat one with all the issues or domain disjunctions on the right hand side.