I’m a believer in strong & static typing. A good type is like a math equation - just as an equation describes the equalities and how
they change, the possibilities of interactions, a type describes the invariants and the properties of what the type represents.
Good types allow us to make invalid states unrepresentable in code, transforming runtime bugs into compiler errors.
Most of use like pizza. Simplistically to make one we need to first flatten some dough,
add the base sauce, add the topping and cook it. It is important to follow all these steps and
in the right order. If we don’t follow these steps we get something which is not a pizza.
We will build an API for making pizza and we will try to design it such that the clients of the API
can make pizzas even if they are not cooks (may forget steps or do them in the wrong order) - they will be forced to follow all the steps, else the code won’t compile.
Generic Pizza recipe
To make pizza we need to first flatten some dough,
add the base sauce, add the topping and cook it.
We will model the steps with an ADT (Algebraic Data Type).
After each of the steps we get an unfinished pizza, and only following all the steps we get a finished pizza.
We model these intermediary results with another ADT
To make pizza we are left to implement the function which will read the steps and make the pizza. These functions
reading ADTs and implementing the behavior are called interpreters.
The interpreter’s result will be either a FinishedPizza or if the steps provided are in the wrong order or incomplete
it will return a PizzaError.
Our API is now complete in the sense that we can make pizzas by following pizza recipes.
But it is also very error prone, since the user of the API needs to know details about how pizza is made, else he will get a pizza error.
He needs to know to build the list with all the steps and in the right order before passing it to makePizza().
It would be best if the API would be safer to use such that the user will be guided in making valid pizzas.
Our API will provide a PizzaRecipeBuilder. It will have the responsibility of building the recipes in the right order.
We start with a definition which let’s us build recipes like this:
This is just the starting point since with it we can still build invalid recipes.
We need to enforce the builder to only add valid next steps given it’s current state.
We will use implicits and typeclasses for this. We will define a RecipeTransition[A, B] typeclass
which will be a required implicit parameter to andThen(). We will then enforce correct ordering by implementing only those RecipeTransitions which we want to allow.
Now our test won’t compile, as it is forcing us to add the sauce
The design up to here forces us to define the recipe in the correct order but it still has 2 issue.
Even though the sequence might be correct, we might forget some last steps, like Cooking,
or we might forget the first steps, like setting up the dough
To fix this we need to do a few things. First we will only allow calling recipe on a PizzaRecipeBuilder[Cook] since this is the last step
and guarantees we have all the steps.
We can do this by defining a new typeclass FinalStep[A] with a single implementation FinalStep[Cook] and require it on the recipe function,
but instead we will use the more suggestive Is[A, B] typeclass from Cats.
Second, we can not allow starting the recipe from an intermediary step. We will create an apply method in the companion object with implicit parameters
that accepts only the correct first pizza recipe step (SetupDough), and we make the constructor private - this accepts every step and we need it to make the transition from a PizzaRecipeBuilder[A] to PizzaRecipeBuilder[B].
Having all this we can go ahead and make makePizza private because we will wrap it with a safe pizza method and offer this instead to the client to build pizza
With this the API client can only make FinishedPizza(s)! no error is possible.
* Transforming an Either to an Option and calling get on it is a cheat. In the small context of makePizzaSafe
there is no type guarantee that this won’t fail - but in the larger context of the API we know the user of the client can build
only correct PizzaRecipe(s).
Given the safe PizzaRecipeBuilder, we should be able to rewrite makePizza without involving Either/PizzaError but the tricky part
is telling it to only accept a valid list of MakePizzaStep. Probably the solution would be to use Shapeless HList which remembers the types for each element
together with the RecipeTransition typeclass as evidence (implicits) that the order is correct.
I’m not familiar enough with Shapeless so I leave it for another time.