Exploring Map with Property Based Thinking
When thinking about software, it's natural to think about the things that it can do (its features like generating reports or adding an item to a cart).
But what about the properties that those actions have? Those things that are always true?
In this post, let's take a look at a fundamental tool of functional programming, the map
function.
All the code examples in this post will be using TypeScript, but the lessons hold for other languages with Map
(or Select
if you're coming from .NET).
Examining Map
In JavaScript/TypeScript, map
is a function for arrays that allow us to transform an array of values into an array of different values.
For example, let's say that we have an array of names and we want to ensure that each name is capitalized, we can write the following:
In our example, as long as we have a pure function that takes a string and returns a new type, then map
will work.
What Does Map Guarantee?
Map is a cool function because it has a lot of properties that we get for free.
-
Maintains Length - If you call
map
on an array of 3 elements, then you'll get a new array with 3 elements. If you callmap
on an empty array, you'll get an empty array. -
Maintains Type - If you call
map
on array of typeT
with a function that goes fromT
toU
, then every element in the new array is of typeU
. -
Maintains Order - If you call
map
on array with one function, then callmap
with a function that "undoes" the original map, then you end up with the original array.
Writing Property Based Tests
To prove these properties, we can write a set of unit tests. However, it would be hard to write a single test that that covers a single property.
Most tests are example based in the sense that for a specific input, we get a specific output. Property based tests, on the other hand, uses random data and ensures that a property holds for all inputs. If it finds an input where the property fails, the test fails and you know which input caused the issue.
Most languages have a tool for writing property-based tests, so we'll be using fast-check for writing property based tests and jest for our test runner
Checking Length Property
If we run this test, we'll end up passing. But what is the value of data
?
By adding a console.log
in the test, we'll see the following values printed when we run the test (there are quite a few, so we'll examine the first few).
Checking Type Property
We've proven that the length property is being followed, so let's look at how we can ensure that result
has the right type.
To keep things simple, we're going to start with a string[]
and map them to their lengths, yielding number[]
.
If map
is working, then the result should be all number
s.
We can leverage typeof
to check the type of each element in the array.
Like before, we can add a console.log
to the test to see what strings are being generated
Checking Order Property
For our third property, we need to ensure that the order of the array is being maintained.
To make this happen, we can use our identity
function from before and check that our result is the same as the input. If so, then we know that the order is being maintained.
And with that, we've verified that our third property holds!
So What, Why Properties?
When I think about the code I write, I'm thinking about the way it works, the way it should work, and the ways it shouldn't work. I find example based tests to help understand a business flow because of it's concrete values while property based tests help me understand the general guarantees of the code.
I find that once I start thinking in properties, my code became cleaner because there's logic that I no longer had to write. In our map
example, we don't have to write checks for if we have null or undefined because map
always returns an array (empty in the worse case). There's also no need to write error handling because as long as the mapping function is pure, map
will always return an array.
For those looking to learn more about functional programming, you'll find that properties help describe the higher level constructs (functors, monoids, and monads) and what to look for.
Finding properties can be a challenge, however, Scott Wlaschin (of FSharpForFunAndProfit) has a great post talking about design patterns that I've found to be immensely helpful.