Domain Modelling with Haskell: Generalizing with Foldable and TraversableThis is the second episode in the short series on Domain Modelling with Haskell. In this episode, we will generalize our domain model from the last episode, providing more fine-grained reporting, with less code.
In the last episode we modelled a basic project management system using Haskell data structures. We had the ability to print project data structures, and to produce a single economic report for an entire project. But the customer wants more fine-grained reporting! They want to see how individual projects are doing; if they are sticking to their budget.
If you haven’t watched the previous episode, I recommend you do so before continuing with this one.
Let’s see what we have already. The
calculateProjectReport function recursively, in a bottom-up fashion, calculates reports and folds them together, using the
What we get back is a single
Report for the entire project tree structure. What we need are reports for each individual project.
Looking at the
Project data type, we see that it’s very concrete, or specialized. There are no slots in the project constructors. In other words, there is no way of extending this data type for use cases we haven’t foreseen.
As we control the definition of the data structure, we could of course add more specialized fields that support all the use cases we know of. That approach, however, will bloat the
Project data type as our system grows, and will most likely make it fragile and hard to work with.
Deriving Functor, Foldable, and Traversable
Instead, we will open it up, making it polymorphic. This enables us to reuse the project structure for different features of our system; project configuration forms, a project comparison feature, our reporting module, a navigation tree, a project updates websocket server, just to name a few examples.
We add a type argument
a, remove the specific
ProjectId field, and add a polymorphic field
a to the
Project constructor. As the data type is recursive, we need to use
a when constructing the type for sub-projects.
Now, here comes the best part. By having a single type argument for our data type like this, we can derive some very useful instances: Functor, Foldable, and Traversable.
- Functor lets us map a function over the project leaves, retaining its structure.
- Foldable lets us fold the project data structure into a single value, in various ways, given that the element type has a
Monoidinstance, or that we can map each element to a monoid. Folding collapses the structure.
- Traversable lets us traverse the project tree and perform an action at each element. The action is usually applicative or monadic.
To derive these three typeclasses, we need to enable two language extensions:
Calculating and Folding Reports
Instead of directly calculating a single report, we will calculate reports for each leaf project, using
Traversable. Then, with a transformed tree containing reports, we can fold it into a single report.
We need to import
We write a new version of
calculateProjectReport. It is defined using
traverse, and the action that is performed at each element calculates a report using the respective project ID.
Note how we transform a pure value, the project structure of project IDs, using an impure action, and get back an impure action, returning a project structure of pure report values. This is one of the beauties of
Given that we have a project of reports, we can then fold together all those individual reports into a single report, using the
Foldable instance. The
accumulateProjectReport function is simply a specialization of
fold, a method in the
Foldable, we now have reports for all individual projects, and a way of combining them into a report for the whole project tree. And we did not have to do any explicit recursion ourselves!
Adapting the Pretty Printing and Demo Modules
We need to change our pretty printing, now that we’ve made
Project polymorphic. The
asTree function will convert any
Project a into a tree of string labels, so we need a function
a -> String. We call it
prettyValue. We use that function to print the value
x of a single
prettyValue function along when recursing.
prettyReport will need the same type of function as an argument, to apply
Our test data in the
Demo module need some changes too. We have changed the order of fields in a single project, so let’s fix that.
We can now calculate a project report data structure, and print it nicely.
*Demo> pr <- calculateProjectReport someProject *Demo> putStrLn (prettyProject prettyReport pr) Sweden | +- Stockholm: Budget: 2082.92, Net: 1338.98, Difference: 3421 | +- Gothenburg: Budget: -3909.57, Net: -447.81, Difference: +3461 | `- Malmö | +- Malmö City: Budget: 6544.71, Net: 1146.83, Difference: -5397.88 | `- Limhamn: Budget: -3571.50, Net: 53.15, Difference: +3624.65
We can fold the value into a single report, and print that:
*Demo> putStrLn (prettyReport (accumulateProjectReport pr)) Budget: -3019.27, Net: 2091.15, Difference: +5110.43
OK, it’s time to wrap up! With
Traversable, we can structure our computations using domain-specific data types, and reuse those data types for multiple use cases. We don’t need to handle recursion explicitly, and thus we can focus only on the transformations that we care about. In the next episode, we will get new customer requirements, and evolve our project management system further, using new techniques.
If you like these videos, hit the like button, and don’t forget to subscribe to the Haskell at Work YouTube channel.
Thank you for watching!
The source code for the full series is available at github.com/haskell-at-work/domain-modelling-with-haskell.