A designer knows he has achieved perfection not when there is nothing left to add, but when there is nothing left to take away
- Antoine de Saint-Exupery
On my journey of growing as a developer, I am consistently inspired by language features which seem incredibly simple but yield remarkable benefit. As I try to master F#, I am frequently surprised by how powerful the language is for expressing ideas while having so few features. Discussions frequently pop up about the need for ever more powerful abstractions, yet I find myself amazed by how far you can take the language with what is already there.
I am no programming language expert, but I admire languages that maintain a lean feature set. Every new feature added to a language makes it just a little bit more difficult to fully understand and a little more intimidating for new developers. It is an impressive design feat when a language can remain approachable for beginners but enable the flexibility that library authors need.
I am an Industrial Engineering turned Machine Learning Engineer, and I focus on the problem of maximizing the profitability and efficiency of companies. Often the solution involves a Mathematical Planning Model (aka Mathematical Programming). What I hope to do in the next few paragraphs is illustrate to you how some of the most basic features of F#, Discriminated Unions and Units of Measure, eliminate the most pernicious bugs when developing these models.
The Domain of Mathematical Planning
The domain of Mathematical Planning is made up of Decisions, Constraints, and Objectives. A Decision is a choice that a business needs to make. It can be how many of Item X do we buy, do we build in Location A or Location B, or how many people do we assign to each job. Constraints are the rules we need to abide by. They are the limitations on what is possible. A Constraint could be that we only have 10 people available, or we can only build in Seattle or Portland, or we only have $1,000,000 to invest. The Objective is how we measure success. It is the function we want to maximize or minimize. We could minimize waste, maximize profit, or minimize cost.
Many of my colleagues are building their models with Python. Python is a great language and I have been productive with it in the past. Here is a snippet of what a mathematical planning model may look like in Python:
|
|
This is the beginning of a straightforward assignment problem. We have a list of items, items
. For each item
in items
, we must decide how many we send to each location
in locations
. There is a limit on how much of each item
is available for us to send. There is a revenue associated with sending a particular item
to a given location
. In this problem we want to maximize our revenue which is calculated by multiplying the decision
for a given item
and location
by the revenue
associated with it. Finally, we create a constraint for each item
in items
which states that the total number of a given item
that is allocated cannot exceed the total that is available.
This is only part of the problem. Normally there would be more constraints that would make it more interesting. This is enough of a problem to illustrate my case though. There are two errors in this model already. If you were paying close attention you may have found one. I promise you cannot detect the second.
The Power of Domain Modeling Using Discriminated Unions
F# provides two simple but powerful features which help ensure against the errors in the Python code. The first is Discriminated Unions. If we were to reformulate this problem using F#, the first thing we would do was define some simple types to model our domain.
|
|
Instead of just using strings to describe our Items and Locations, we create simple, single case Discriminated Unions (DU). These DUs provide context around what the strings are meant to represent. Let’s go ahead and create our item
and locations
lists again. This time, wrapping them in DUs.
|
|
We will also update our availability
information to use these new types.
|
|
We will create the Decisions for each item
and location
. We store these Decision
types in a Map
which is indexed by an (Item * Location)
tuple.
|
|
We now attempt to create the same constraints we did in Python with a direct translation.
|
|
Except, the compiler is gives us an error on the indexing of allocation
.
What some of you may have noticed in the Python code is that the allocation
collection is indexed by an Item
then Location
. The original code was trying to access it by location
then by item
. This would have thrown an error at runtime due to a missing value. In F# this becomes a compiler error. The type system itself it is helping you. This may seem small, but this is one of the most painful types of errors when debugging a Mathematical Planning model.
Someone may say that this can be accomplished in other languages and I would agree. I believe where F# is unique is in the simplicity and ease of using single case Discriminated Unions for wrapping primitives. It is virtually no additional effort.
Units of Measure: The Achilles Heel of Numbers
There is an underappreciated problem in software development, numbers are rarely just numbers. They represent something: cm
, feet
, kg
, or meters
. Normally we do not care about a raw number. Our primary concern is with what the number represents. In most languages there are no easy mechanisms for tracking the Units of Measure associated with a number. F# on the other hand has baked the concept of a Unit of Measure into the type system.
The Units of Measure feature will reveal the second problem with the Python code that otherwise may remain undetected. Let’s update our domain with some new types to track the units on our numbers.
|
|
We now have units to represent Servings
and Kg
. Let’s update our availability
collection to store numbers with these units attached.
|
|
We have now provided more context around our availability numbers. We now know they are stored in units of Kg
. The F# compiler will enforce correct algebra as we work with them. We now update our Decisions to be in units of Servings
.
|
|
With our Decisions updated, we go back to our constraint definition and we now see a new bug.
The important part of this message is at the bottom. The compiler is complaining that the left-hand is in units of Servings
and the right-hand side is in units of Kg
. It does not make sense to compare values that are in different units, so the compiler is throwing an error. In other languages this error would go undetected. Worse, it may not even be caught in unit testing because the math will still work, it just won’t give correct results.
Let’s go ahead and add some conversion data so that we can fix this.
|
|
We now have data which will allow us to convert from Serving
to Kg
. Let’s incorporate it into our constraint creation expression.
|
|
Now the compiler is happy because the units are in Kg
on both sides. This simple feature of ensuring correct Units of Measure eliminates what is possibly the most nefarious bug in Mathematical Planning. It would be hard to calculate the number of hours wasted on badly formulated models due to mismatched Units of Measure.
Simple Building Blocks
F# is an incredibly expressive language while staying lean on the number of features. Other languages have taken the approach of throwing every possible feature in. F# is relatively slow to incorporate new features and they are always purposeful. Most of the time the feature is orthogonal to the rest of the language. This is keeping the language approachable for newcomers so the climb to mastery is not nearly as steep. I believe these two simple features, Discriminated Unions and Units of Measure, uniquely position F# as an awesome language for Mathematical Planning.