I wrote a blog post called Some(“F#”) is better than None back in 2015. I’ve grown quite a bit as a developer since then. Growth is what we strive for in this industry. Looking back over the blog post, I would like to improve upon the code to make it more expressive and bug free. The code I am focused on is comparing assembly versions to indicate whether an update would be required. The original code looked similar to this:
let checkForUpdate (rMaj, rMin, rBld) userVer = match (uMaj, uMin, uBld) with | (x, _, _) when x < rMaj -> "Major" | (rMaj, x, _) when x < rMin -> "Minor" | (rMaj, rMin, x) when x < rBld -> "Build" | _ -> "None”
My first instinct of change would be to return a Discriminated Union instead of a string. Using a discriminated union would give me type safety as well as better pattern matching later on in in other functions. I would define the discriminated union as:
type UpdateType = | Major | Minor | Build | NoUpdate
checkForUpdate function would then look like:
let checkForUpdate (rMaj, rMin, rBld) userVer = match (uMaj, uMin, uBld) with | (x, _, _) when x < rMaj -> Major | (rMaj, x, _) when x < rMin -> Minor | (rMaj, rMin, x) when x < rBld -> Build | _ -> NoUpdate
This is a minor change with big benefits. Other functions handling the result can now pattern match on the type safe discriminated union instead of the infinite variations of potential string values.
Expanding the guards
I decided to add some tests for the function. I used FsCheck (a property based testing library) to battle test the function. This turned out to be a useful exercise as an issue was identified. The patterns were not handling the case correctly for a user who has an alpha or beta version which is considered a higher version than the current release.
To resolve the issue I ended up having to expand the guard clauses. I made the mistake of shadowing the release version labels
rMin. I had thought they would match the value contained in the labels, similar to an erlang pattern match. I was wrong. Shadowing a label means the system creates a new label of the same name within the current/nested scope.
let checkForUpdate (rMaj, rMin, rBld) userVer = match userVer with | (uMaj, _, _) when rMaj > uMaj -> Major | (uMaj, uMin, _) when rMaj = uMaj && rMin > uMin -> Minor | (uMaj, uMin, uBld) when rMaj = uMaj && rMin = uMin && rMin > uBld -> Build | _ -> NoUpdate
After the guard expansions all my tests passed. Although it works, this pattern match isn’t as expressive as we may like. Going forward, we should look into using Active Patterns.
There are some things to be aware of when using pattern matching. We can’t compare bound values directly in the pattern, we have to use a guard clause to compare them. Attempting to match bound values in the pattern will not match and will create a shadowed value. Use discriminated unions (DU) when given the choice for patterns. DUs improve the type safety of our functions and limit the number of variations our functions have to account for. Passing strings into functions have an infinite number of variations and for the most part be avoided. Sometimes making some small changes can make for more robust functions.