Today I was enjoing the puzzle of figuring out which existing Haskell functions I could compose to make a specific new function. I suddenly realized that function design in a strongly typed language has a lot in common with dimensional analysis from physics.
Dimensional analysis is really useful for solving problems where you don’t have a direct conversion between units available. For a very simple example, suppose I walk 10000 steps per day and want to know how far I walk in a year. I don’t know the conversion from steps per day to km per year, but I can build one like so:
10000 steps/day * 0.7m/step * 365 days/year * 1km/1000m = 2555km/year
Similarly, you can often solve functional programming problems by composing
simple functions in the same way that these small units are composed into a
bigger conversion. Today I had a list of Int
and a function that produced
Maybe Int
. I wanted to find out if the value returned from the function was
in the list. You can do something like this:
f x `elem` [Just 1, Just 2, Just 3]
It seemed inelegant to have to keep repeating Just
for each value. Something
like this gets you a little closer, but it still feels like I’m typing out the
implementation details instead of writing the computation I want:
f x `elem` (map Just [1, 2, 3])
So then I backed up and asked how can I break down my original problem? I need
a function that combines a Maybe a
with [a]
and returns Bool
. Let me
look in the Maybe monad documentation for a function that looks like that.
Aha, maybe
is pretty close. It looks like
maybe :: b -> (a -> b) -> Maybe a -> b
It has that extra b
in there to represent the default value when Nothing
is
enountered, but that’s the price of generality. And then for the a -> b
function, we can go from Int
to Bool
based on whether the argument is in
the list. That function is called (`elem` [1, 2, 3])
. Putting this all
together, we can write:
maybeInList = maybe False (`elem` [1, 2, 3]) (f x)
If we want to check Maybe
values from different sources, we can leave off the
(f x)
part or pass it in as a parameter. Beautiful! This is at least as
clear as the original version, and it leaves the compiler and library
implementor free to do fancy optimizations later.
Of course, this is all just the basic rules of function composition, but I still thought it was fun to notice the similarity to dimensional analysis. It just goes to show that the same underlying mathematical mechanisms show up everywhere.